13 mod
13 包管理
包是 Go 组织代码的单元,类似 Cpp 的 namespace + module 的组合,主要包括内置包、自定义包和第三方包。
包管理
Go 1.11 以前是必须把项目放在 GOPATH 下的,不过在这之后引入了 go mod
作为官方包管理工具,可在任意目录创建项目,而不需要再关注所谓的 GOPATH。
初始化项目
在项目根目录执行:
go mod init 项目名
这会生成 go.mod
文件管理依赖,值得注意的是,如果是使用 github 托管的项目,那项目名最好是 github.com/user_name/project_name
,这是社区规范,可以让别人通过 go get 直接下载我们写的这个包。
常用命令
go mod tidy # 添加缺失依赖,移除未使用依赖
go mod download # 下载依赖到本地 cache
go mod vendor # 复制依赖到 vendor 目录
go mod verify # 验证依赖完整性
go mod graph # 打印模块依赖图
最常用的是 go mod tidy
,每次修改依赖后执行一次即可,可以帮我们添加缺失依赖并移除未使用依赖。
自定义包
一个包可以简单的理解为一个存放多个 .go 文件的文件夹,同文件夹下的每个 .go 文件第一行都要声明所属包:
package 包名
注意事项:
- 包名可以与文件夹名不同,但是建议保持一致
- 同一文件夹下所有文件必须属于同一个包,一个包的文件不能在多个文件夹下
package main
是特殊的入口包,会编译为可执行文件,其余的包则不会
可见性规则
Go 使用 首字母大小写 控制可见性,来表示一个标识符是否对其他包可见:
package calc
var age = 20 // 私有变量,仅包内可见
var Name = "Alice" // 公有变量,包外可访问
func add(x, y int) int { // 私有函数
return x + y
}
func Sub(x, y int) int { // 公有函数
return x - y
}
导入与使用
package main
import "项目名/calc" // 导入自定义包
func main() {
result := calc.Sub(10, 5) // 通过包名访问
}
导入方式
普通导入
常见的单行导入和多行导入都比较基础,大部分情况都是多行导入:
import "fmt"
import (
"fmt"
"time"
"项目名/calc"
)
别名导入
如果一个包名比较长还不好写的话,可以起一个别名:
import (
"fmt"
c "项目名/calc" // 定义别名
)
func main() {
c.Sub(10, 5) // 使用别名访问
}
匿名导入
仅执行包的 init()
函数,而不使用包内容:
import _ "github.com/lib/pq" // 常用于数据库驱动注册
init() 函数
在 Go 中导入一个包时会自动执行该包的 init 函数,这是一个完全依托于系统的函数,完全由程序自动调用,不能手动使用,可以简单理解为一个包的前置操作。
package main
func init() {
// 初始化逻辑
fmt.Println("main 包初始化")
}
func main() {
fmt.Println("begin")
}
// main 包初始化 begin
init 的执行顺序简单说来就是在导入包时判断当前包是否还有依赖,一直找到最后一个不依赖其他包的包后递归执行所有的 init 函数,但是实际上会出现很多情况,这里说一下 init 常见的执行情况:
对于单个文件:
全局变量声明与初始化 -> init() -> main()
一个文件可以有多个 init 函数,这种情况按 声明顺序 依次执行:
同样的,一个包可能有多个文件,每个文件都可以写init,这种情况下会按 文件名字典序 依次执行各文件的 init 函数:
同一个包:
a.go -> init()
b.go -> init()
z.go -> init()
多个包之间会形成树形依赖,遵循 依赖优先 原则,先执行被依赖的包的初始化函数,如 pkgA 导入了 pkgB,那么一定先执行 pkgB 的一套初始化操作。
因此一定要避免循环依赖,两个包互相导入时编译器无法处理依赖关系,会直接报错。
第三方包
类似于 xmake 的使用,Go 也提供了一套策略可以让大家分享自己写的包,并很方便的下载别人写的包,此处我们以前面提到的解决浮点数精度损失的包为例来介绍。
查找第三方包
访问 pkg.go.dev 搜索包。
安装第三方包
go get github.com/shopspring/decimal
安装后 go.mod
会自动记录依赖版本。
使用示例
import "github.com/shopspring/decimal"
func main() {
a := decimal.NewFromFloat(0.1)
b := decimal.NewFromFloat(0.2)
result := a.Add(b)
fmt.Println(result.String()) // 0.3(无精度丢失)
}
如果没有获取过此包,直接执行 go mod tidy
会自动下载依赖到本地。
包的组织建议
Go 社区对项目结构有一些约定俗成的规范,合理的目录组织能让代码更易维护和协作。
myproject/
├── go.mod # 模块定义文件
├── go.sum # 依赖版本锁定
├── main.go # 单一可执行程序入口
├── cmd/ # 多个可执行程序入口
│ ├── server/
│ │ └── main.go # 服务端程序
│ └── client/
│ └── main.go # 客户端程序
├── internal/ # 私有代码
│ ├── auth/ # 认证逻辑
│ ├── database/ # 数据库操作
│ └── utils/ # 内部工具函数
├── pkg/ # 可被外部项目导入的公共库
│ ├── api/ # API 定义
│ └── models/ # 数据模型
├── api/ # API 规范文件(如 OpenAPI)
├── configs/ # 配置文件
├── scripts/ # 构建、部署等脚本
├── test/ # 额外的测试文件和测试数据
└── docs/ # 文档
也可以看一下一个非常规范且广泛认可的 模板项目,包括我们后续会展示的用于 RESTful API架构的 Gin框架,这些目录结构都是很值得学习的。