CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
模块与包管理
这一课你将能做到
用 go mod init 创建模块,说出 go.mod 每一行的含义
把项目拆成多个包,正确运用「首字母大写导出、小写包内私有」规则和导入路径
熟练使用 go get、go mod tidy、go build、go run、go vet 等日常命令
为什么要学
前面的代码都是单文件单包,玩具阶段。真实项目一定是多包目录 + 若干第三方依赖 :net/http 封装在哪个包、别人的库怎么引入、版本冲突谁说了算、为什么别人的包你能 import 而有的不行——这些全部由 Go 的模块系统(go modules,Go 1.11+ 引入,1.16 起默认开启)管理。
包(package)是代码组织单位,模块(module)是依赖与版本单位。理清这两层,项目结构、依赖管理、团队协作就都有了秩序。
先建立图像
一个典型 Go 项目的目录树与它的模块文件:
calculator/ ← 项目根目录 = 模块根
├── go.mod ← 模块身份证 + 依赖清单
├── go.sum ← 依赖内容的校验清单(哈希)
├── main.go ← package main,程序入口
└── pkg/
└── ops/
└── ops.go ← package ops,业务子包
go.mod 内容:
module example.com/calculator ← 模块路径:别人 import 你时的前缀
go 1.22 ← 语言版本要求
require (
github.com/google/uuid v1.6.0 ← 依赖 + 精确版本
)
导入路径 = 模块路径 + 子目录 。比如 example.com/calculator/pkg/ops。
跟着做
步骤 1:创建模块
$ mkdir calculator && cd calculator
$ go mod init example.com/calculator
go: creating new go.mod: module example.com/calculator
模块路径通常写「域名 + 路径」(方便将来发布到 GitHub);本地练手随便起一个不冲突的即可。打开 go.mod 确认内容。
步骤 2:拆出第一个子包
创建 pkg/ops/ops.go:
package ops // 包名惯例:小写、单个单词、与目录名一致
import "fmt"
// Add 首字母大写 → 导出(公开),任何包都能调用
func Add(a, b int) int {
return a + b
}
// multiply 首字母小写 → 未导出(私有),只在 ops 包内可见
func multiply(a, b int) int {
return a * b
}
// Double 是包内函数,演示私有函数的用法
func Double(a int) int {
return multiply(a, 2)
}
func init() {
// init 函数:包被导入时自动执行,可执行初始化
fmt.Println("ops 包已加载")
}
步骤 3:在 main 中导入并调用
项目根目录创建 main.go:
package main
import (
"fmt"
"example.com/calculator/pkg/ops" // 导入路径 = 模块路径 + 子目录
)
func main() {
fmt.Println(ops.Add(1, 2)) // 3
fmt.Println(ops.Double(5)) // 10
// fmt.Println(ops.multiply(2, 3)) // 编译错误:multiply 未导出
}
运行:
$ go run .
ops 包已加载
3
10
两点注意:导入未使用的包是编译错误 ;init() 在 main 之前自动执行。
步骤 4:添加第三方依赖 引入一个最常见的库 github.com/google/uuid:
$ go get github.com/google/uuid
go: added github.com/google/uuid v1.6.0
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
id := uuid.NewString()
fmt.Println("生成的 UUID:", id)
}
再观察 go.mod——require 块自动多了一行,同时多出了 go.sum(记录每个依赖模块的哈希,用于校验内容未被篡改)。删掉 go.mod 里的依赖后再执行 go mod tidy,它会自动增删 require 条目,保持「代码里 import 的」和「go.mod 里声明的」完全一致。
步骤 5:常用命令清单 $ go mod init <路径> # 初始化模块
$ go get <包>@<版本> # 添加/升级依赖,@latest 拿最新
$ go mod tidy # 整理依赖:补齐缺失、清除多余
$ go mod vendor # 把依赖拷进 vendor/ 目录(离线构建用)
$ go run . # 编译并运行当前包
$ go build ./... # 编译所有包;-o 指定输出文件
$ go fmt ./... # 格式化
$ go vet ./... # 静态检查可疑代码
$ go test ./... # 跑测试(第 11 课展开)
./... 读作「当前目录及所有子目录」,是 Go 命令行最常见的通配符。
步骤 6:交叉编译与版本管理 $ $env:GOOS="linux"; go build -o app . # PowerShell 下产出 Linux 二进制
$ go get github.com/google/uuid@v1.3.1 # 锁定旧版本
$ go list -m all # 列出全部依赖的最终版本
Go 用「最小版本选择(MVS)」:每个依赖固定在你声明的版本,不会像 npm 那样自动飘到最新次版本,构建高度可复现。
步骤 7:GOPROXY 与 vendor $ go env GOPROXY # 查看当前代理
$ go env -w GOPROXY=https://goproxy.cn,direct # 国内镜像(第 1 课已配)
$ go mod vendor # 生成 vendor 目录
$ go build -mod=vendor ./... # 用 vendor 构建,不联网
vendor 适合 CI 离线环境或对供应链有强管控的团队;一般项目靠 go.sum 校验 + 模块代理即可。
讲清楚要点
package 是编译与代码组织单位,module 是依赖与版本单位 :一个模块包含一个或多个包;一个目录只能有一个包。
导出规则只有一条 :标识符首字母大写即可被包外访问,小写则包内私有。没有 public/private 关键字。
导入路径由 go.mod 决定 ,与磁盘上的 $GOPATH 无关;项目放在任何目录都可以。
internal 目录是语言级私有机制 :example.com/app/internal/... 只能被 example.com/app/... 内的代码导入,第三方项目 import 会直接编译报错。
go.mod 声明依赖,go.sum 校验依赖 :前者你手动维护,后者永远自动生成,禁止手改。
循环导入是编译错误 :A 导 B、B 导 A 不允许,需要抽公共包或调整层次。
你可能会懵的地方
模块路径里的域名是真的吗?我要先注册域名吗?
不用。go get 时代码才需要真实可拉取;本地模块和私有项目随便写路径(如 example.com/myapp)。发到 GitHub 后惯例改成 github.com/你/仓库名。
go.mod 和 go.sum 的区别?
go.mod 是依赖清单(要哪些、什么版本),go.sum 是内容校验表(每个版本的哈希),防止下载被篡改。go.sum 会随 go get/tidy 自动更新。
包名一定要和目录名一致吗?
惯例一致,编译只认 package 声明行;但强烈保持一致,否则 import 路径和引用名对不上,团队会骂人。
为什么有人 import 前面加下划线 _ "net/http/pprof"?
空白导入:不直接使用包,只为触发它的 init()(副作用),比如注册驱动、注册路由。数据库驱动章节会再见到它。
依赖出现两个版本怎么办?
Go 允许不同模块各自依赖同一库的不同大版本(v2+ 路径不同),MVS 自动选满足所有要求的最低版本,通常无感知。
错法对照 错法 问题 正解 导入了包但没用 imported and not used 编译错误删除导入,或 _ "pkg" 空白导入 所有代码堆在 main 包 无法复用、无法测试 按职责拆子包,main 只做组装 手动编辑 go.sum 校验失败构建报废 go.sum 只由 go 命令自动维护 import 路径写相对路径 import "./pkg/ops" Go 不支持相对导入 写完整模块路径 忘了 go mod init 直接 go run 报 go.mod file not found 先 init 再写代码
卡点急救
no required module provides package ... :依赖没进 go.mod,执行 go mod tidy 自动补齐。
module example.com/x: reading ...: connection refused :网络问题,配置 GOPROXY 国内镜像。
package xxx is not in std :导入路径拼错,或模块路径与实际目录不匹配;用 go list ./... 核对。
cannot find main module :在项目根目录(有 go.mod 的地方)执行命令。
面试常问
go modules 解决了什么问题?与 GOPATH 时代的区别?
GOPATH 时代所有项目必须放在固定目录、依赖没有版本概念(go get 拉 master)。modules 让项目可放任意目录,依赖有精确版本、内容有哈希校验、构建可复现(MVS 最小版本选择)。
vendor 目录什么时候用?
需要离线/可重现构建的 CI、对第三方源有强审计要求的组织。日常开发用模块缓存 + 代理更轻。
internal 包的机制是什么?
编译器强制约束:internal 目录下的包只能被其父模块路径内的代码导入,用于隐藏内部实现,是语言层面的可见性边界。
Go 如何保证依赖供应链安全?
go.sum 记录模块内容哈希;GONOSUMDB/GOSUMDB(sum.golang.org)校验数据库交叉验证;vendor 与 GOFLAGS=-mod=mod 等机制配合审计流程。
自测三问
module example.com/app 下有 internal/store 包,同事在另一个仓库 import 它会怎样?
go get、go mod tidy 各做什么?什么时候必须 tidy?
一个目录下能放两个不同 package 的 .go 文件吗?为什么?
小结一句话 module 用 go.mod 圈定「我是谁、我依赖谁」,package 用首字母大小写划定「谁能看见我」;掌握 init、tidy、internal 和 ./...,就从玩具代码迈进了工程世界。
延伸