导包和包管理
一、什么是包
- 和python类似,包就是一个个包含某种功能的代码文件,为了代码以后的可重用性与可读性更高,维护更方便,才引入的一个概念。
- 与python不同的是,python中的包就是一个文件,导包就是导文件,而go中,一个包可以简单理解为一个存放
.go
文件的文件夹 - 若没有包的概念,就会把所有代码、方法全写在一个文件中,这对于一个项目来说,想想是不是就很可怕
- 属于某一个包的源文件都应该放置于一个单独命名的文件夹里。且按照 Go 的惯例(只是惯例,也可以起别的名字),应该用包名命名这个文件夹。
- 另外需要注意一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下
二、main函数和main包
-
所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中,main 包是go自带的包。而python是没有这种限制的
-
package packagename 这行代码指定了某一源文件属于一个包。它应该放在每一个源文件的第一行
-
包名为
main
的包是应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main
包的源代码则不会得到可执行文件
三、包管理
以下总体的目录结构:
src
geometry
geometry.go
rectangle
rectprops.go
bin
geometry.exe
// 执行 go build geometry 命令来编译上述程序。该命令会在 geometry 文件夹内搜索拥有 main 函数的文件。在这里,它找到了 geometry.go。接下来,它编译并产生一个名为 geometry (在 windows 下是 geometry.exe)的二进制文件,该二进制文件放置于工作区的 bin 文件夹
1. 我们的main包
// 在 geometry.go 中
package main
import "fmt"
func main() {
fmt.Println("Geometrical shape properties")
}
2. 定义包
package
:声明包的关键字packagename:包名
,可以不与文件夹的名称一致,不能包含-
符号,最好与其实现的功能相对应- 属于某一个包的源文件都应该放置于一个单独命名的文件夹里。按照 Go 的惯例,应该用包名命名该文件夹
- 注意:如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。在这里,我们需要在 main 包中访问 Area 和 Diagonal 函数,因此会将它们的首字母大写
(1)定义包的实例:
-
创建一个自定义包
rectangle
,它有一个计算矩形的面积和对角线的函数。 -
因此,我们在
geometry
文件夹中,创建一个命名为rectangle
的文件夹。在rectangle
文件夹中,所有文件都会以package rectangle
作为开头,因为它们都属于 rectangle 包 -
在创建的 rectangle 文件夹中,再创建一个名为
rectprops.go
的文件,内容如下 -
// rectprops.go package rectangle import "math" func Area(len, wid float64) float64 { area := len * wid return area } func Diagonal(len, wid float64) float64 { diagonal := math.Sqrt((len * len) + (wid * wid)) return diagonal }
3. 导包
- 要在当前包中使用另外一个包的内容就需要使用
import
关键字引入这个包,并且import语句通常放在文件的开头,package
声明语句的下方。完整的引入声明语句格式为:import importname "path/to/package"
importname
:引入的包名,通常都省略。默认值为引入包的包名path/to/package
:引入包的路径名称,必须使用双引号包裹起来- Go语言中禁止循环导入包
- 如果引入一个包的时候为其设置了一个特殊
_
作为包的别名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init
函数将被执行并且仅执行一遍
1. 单个导入
import "fmt"
import "net/http"
import "os"
2. 批量导入
import (
"fmt"
"net/http"
"os"
)
3. 给导入的包起别名
import f "fmt" // f 为 fmt的别名,类似python中的 import pands as pd
4. 匿名导入一个包
import _ "github.com/go-sql-driver/mysql" // 匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中
(1)导包实例
-
我们必须指定自定义包相对于工作区内
src
文件夹的相对路径。我们目前的文件夹结构是: -
1. 目录结构 src geometry geometry.go rectangle rectprops.go 2. **********在geometry.go中导入这个自定义的包*********** // geometry.go package main import ( "fmt" "geometry/rectangle" // 导入自定义包,只导入目录 ) func main() { var rectLen, rectWidth float64 = 6, 7 fmt.Println("Geometrical shape properties") /*Area function of rectangle package used*/ fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth)) /*Diagonal function of rectangle package used*/ fmt.Printf("diagonal of the rectangle %.2f ", rectangle.Diagonal(rectLen, rectWidth)) }
四、init 函数和包的初始化
-
在每一个Go源文件中,都可以定义任意一个
init
函数 -
这种特殊的函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行
-
func init(){ // ... }
-
init
函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。包的初始化顺序如下:
- 首先初始化包级别(Package Level)的变量
- 紧接着调用
init
函数。一个包可以有多个init
函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。
-
如果一个包导入了另一个包,会先初始化被导入的包。
-
尽管一个包可能会被导入多次,但是它只会被初始化一次
1. 实例-验证包的初始化顺序
(1)修改 rectprops.go
- 首先在
rectprops.go
文件中添加了一个 init 函数
// rectprops.go
package rectangle
import "math"
import "fmt"
/*
* init function added
*/
func init() {
fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
(2)修改geometry.go
- 其次对
geometry.go
做了如下修改:- 变量 rectLen 和 rectWidth 从 main 函数级别移到了包级别。
- 添加了 init 函数。当 rectLen 或 rectWidth 小于 0 时,init 函数使用 log.Fatal 函数打印一条日志,并终止了程序。
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 导入自定义包
"log"
)
// 1. 包级别变量
var rectLen, rectWidth float64 = 6, 7
// 2. init 函数会检查长和宽是否大于0
func init() {
println("main package initialized")
if rectLen < 0 {
log.Fatal("length is less than zero")
}
if rectWidth < 0 {
log.Fatal("width is less than zero")
}
}
func main() {
fmt.Println("Geometrical shape properties")
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
(3)运行结果
-
理论上main 包的初始化顺序为:
- 首先初始化被导入的包。因此,首先初始化了 rectangle 包
- 接着初始化了包级别的变量 rectLen 和 rectWidth
- 调用 init 函数
- 最后调用 main 函数
-
// 打印结果与理论符合 rectangle package initialized main package initialized Geometrical shape properties area of rectangle 42.00 diagonal of the rectangle 9.22
(4)再修改 geometry.go
将 geometry.go 中的 var rectLen, rectWidth float64 = 6, 7 改为 var rectLen, rectWidth float64 = -6, 7。
我们把 rectLen 初始化为负数
// 运行结果
rectangle package initialized
main package initialized
2017/04/04 00:28:20 length is less than zero
/*
会首先初始化 rectangle 包,然后是 main 包中的包级别的变量 rectLen 和 rectWidth。rectLen 为负数,因此当运行 init 函数时,程序在打印 length is less than zero 后终止
*/
五、实现导包而不用
-
在go中,导入了包或变量,却不在代码中使用它们,这在 Go 中都是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包或定义了过多的变量,从而导致编译时间显著增加
-
变量还好,但是再开发之前,我们都会习惯先将要用的包导好,再进行后续开发,但是这就与上面说的相矛盾,此时就可以使用错误屏蔽器来避免
1. 错误导包
// geometry.go
package main
import (
"geometry/rectangle" // 导入自定的包
)
func main() {
}
// 导了而不用,会报错 geometry.go:6: imported and not used: "geometry/rectangle"
2. 错误屏蔽器
// geometry.go
package main
import (
"geometry/rectangle"
)
var _ = rectangle.Area // 错误屏蔽器
func main() {
}
/*
var _ = rectangle.Area 这一行屏蔽了错误。我们应该了解这些错误屏蔽器(Error Silencer)的动态,在程序开发结束时就移除它们,包括那些还没有使用过的包。由此建议在 import 语句下面的包级别范围中写上错误屏蔽器
*/
3. 空白标识符避错
-
有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量。例如,我们或许需要确保调用了 rectangle 包的 init 函数,而不需要在代码中使用它。这种情况也可以使用空白标识符
-
// geometry.go package main import ( _ "geometry/rectangle" ) func main() { } // 运行这个的程序,会输出 rectangle package initialized。尽管在所有代码里,我们都没有使用这个包,但还是成功初始化了它
六、go mudule
-
Go语言没有python那种虚拟环境的逻辑(即每个环境中的python解释器只会使用本环境中含有的第三方包,然后不同项目使用不同的环境),而是每个项目中生成一个
go.mod
文件进行记录本项目要使用的包和该包的版本 -
在Go语言的早期版本中,我们编写Go项目代码时所依赖的所有第三方包都需要保存在GOPATH这个目录下面。这样的依赖管理方式存在一个致命的缺陷,那就是不支持版本管理,同一个依赖包只能存在一个版本的代码。可是我们本地的多个项目完全可能分别依赖同一个第三方包的不同版本,由此,Go module 概念产生了
-
Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。Go module 提供了以下命令供我们使用:
命令 | 说明 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
- Go语言在 go module 的过渡阶段提供了
GO111MODULE
这个环境变量来作为是否启用 go module 功能的开关,考虑到 Go1.16 之后 go module 已经默认开启,所以本书不再介绍该配置,对于刚接触Go语言的读者而言完全没有必要了解这个历史包袱
1. 设置拉取包的代理:GOPROXY
-
GOPROXY这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式,直接通过镜像站点来快速拉取。类似python中的镜像源
-
GOPROXY的默认值是:
https://proxy.golang.org,direct
,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cn
和https://goproxy.io
,当然如果你的公司有提供GOPROXY地址那么就直接使用。设置GOPROXY的命令为:go env -w GOPROXY=https://goproxy.cn,direct
-
GOPROXY允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取
2. 设置私有仓库的代理:GOPRIVATE
-
设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE 用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。
-
GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:
go env -w GOPRIVATE="git.mycompany.com"
。这样在拉取以git.mycompany.com
为路径前缀的依赖包时就能正常拉取了 -
此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置
go env -w GOPRIVATE=none
,允许通内部代理拉取私有仓库的包
3. go.mod文件
1. go.mod文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式有如下两种
require module/path v1.2.3
replace module/path => 相对路径表示的本地包
其中:
require:声明依赖的关键字
module/path:依赖包的引入路径
v1.2.3:依赖包的版本号。支持以下几种格式:
latest:最新版本
v1.0.0:详细版本号
commit hash:指定某次commit hash
replace:将指定的包替换为我们本地的包
// 如果想指定下载某个commit对应的代码,可以直接指定commit hash,不过没有必要写出完整的commit hash,一般前7位即可
// eg: replace liwenzhou.com/overtime => ../overtime ,但是导包时,仍写成 import "liwenzhou.com/overtime"
// 我们也经常使用 replace 将项目依赖中的某个包,替换为其他版本的代码包或我们自己修改后的代码包
4. go.sum文件
1. 使用go module下载了依赖后,项目目录下还会生成一个go.sum文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum文件内容通常是以类似下面的格式出现
<module> <version>/go.mod <hash>
或者
<module> <version> <hash>
<module> <version>/go.mod <hash>
/*
不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了 go.sum 机制来对依赖包进行校验
*/
5. 依赖保存的位置
1. Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本
mod
├── cache
├── cloud.google.com
├── github.com
└──q1mi
├── hello@v0.0.0-20210218074646-139b0bcd549d
├── hello@v0.1.1
└── hello@v0.1.0
...
2. 如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令
6. 使用go module发布包
- 比较少用这个功能,想了解详细情况请自行百度
7. go module实例
- 下面的操作是在Linux系统下
(1)创建项目
1. 在本地先创建一个项目 holiday ,并切换目录到holiday下
$ mkdir holiday
$ cd holiday
(2)初始化项目生成go.mod文件
1. 目前我们位于holiday文件夹下,接下来执行下面的命令初始化项目
$ go mod init holiday
go: creating new go.mod: module holiday // 本行为控制台打印的信息
2. 该命令会自动在项目目录下创建一个go.mod文件,其内容如下
module holiday
go 1.22
3. 分析 go.mod 中的内容含义
- module holiday:定义当前项目的导入路径
- go 1.16:标识当前项目使用的 Go 版本
/*
go.mod文件会记录项目使用的第三方依赖包信息,包括包名和版本,由于我们的holiday项目目前还没有使用到第三方依赖包,所以go.mod文件暂时还没有记录任何依赖包信息,只有当前项目的一些信息
*/
(3)下载依赖包
1. 接下来,我们在项目目录下新建一个main.go文件,其内容如下
// holiday/main.go
package main
import "fmt"
func main() {
fmt.Println("现在是假期时间...")
}
2. 然后,我们的holiday项目现在需要引入一个第三方包github.com/q1mi/hello来实现一些必要的功能。类似这样的场景在我们的日常开发中是很常见的。我们需要先将依赖包下载到本地同时在go.mod中记录依赖信息,然后才能在我们的代码中引入并使用这个包。
****************************下载依赖包主要有两种方法****************************
2.1 第一种方法:在项目目录下执行 go get 命令手动下载依赖的包
holiday $ go get -u github.com/q1mi/hello
go get: added github.com/q1mi/hello v0.1.1 // 本行为控制台打印的信息
2.11 此时mg.mod内容为:
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1 // indirect
2.12 像上面这样,默认会下载最新的发布版本,你也可以指定想要下载指定的版本号
holiday $ go get -u github.com/q1mi/hello@v0.1.0
go: downloading github.com/q1mi/hello v0.1.0
go get: downgraded github.com/q1mi/hello v0.1.1 => v0.1.0
/*
indirect :在5.11中,行尾的indirect表示该依赖包为间接依赖,说明在当前程序中的所有 import 语句中没有发现引入这个包
-u 参数:在5.12中,在执行go get命令下载一个新的依赖包时一般会额外添加-u参数,强制更新现有依赖
*/
2.13 此时mg.mod内容为:
module holiday
go 1.16
require github.com/q1mi/hello v0.1.0 // indirect
********************分隔线***************************
2.2 第二种方法:直接编辑go.mod文件,将依赖包和版本信息写入该文件,然后在 项目目录下 执行 go mod download 下载依赖包
2.21 例如我们修改holiday/go.mod文件内容如下(latest 表示最新版本):
module holiday
go 1.16
require github.com/q1mi/hello latest
********************分隔线***************************
2.22 在 项目目录下 执行 go mod download 下载依赖包
$ go mod download
// 如果不输出其它提示信息就说明依赖已经下载成功,此时go.mod文件已经变成如下内容:
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
(4)引用依赖包
1. 下载好要使用的依赖包之后,我们现在就可以在holiday/main.go文件中使用这个包了
package main
import (
"fmt"
"github.com/q1mi/hello"
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi() // 调用hello包的SayHi函数
}
/*
运行程序后打印结果:
holiday $ go build
holiday $ ./holiday
现在是假期时间...
你好,我是七米。很高兴认识你。
*/