Go 包与模块管理
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
Go 的「包」是编译单元,「模块」是版本单元。两者概念不同,混在一起就会遇到 package xxx is not in std 这类报错。
环境准备
go version # go1.24.x
module 与 package
- module:一组包的集合,由根目录的
go.mod定义,有统一的导入路径前缀与版本。 - package:同一目录下的
.go文件,同目录只能有一个包名。
mkdir hello && cd hello
go mod init example.com/hello
go.mod:
module example.com/hello
go 1.24
导入路径 = 模块路径 + 相对目录:
hello/
go.mod module example.com/hello
main.go package main
greet/
greet.go package greet
internal/
store/
store.go package store
// main.go
package main
import (
"fmt"
"example.com/hello/greet"
"example.com/hello/internal/store"
)
func main() {
fmt.Println(greet.Hello("世界"))
_ = store.New()
}
导出规则
标识符首字母大写才导出(对其他包可见),小写只在包内可见。没有 public / private 关键字。
package greet
// 导出
func Hello(name string) string { return "你好," + name }
// 包内可见
func normalize(name string) string { return name }
internal/ 是编译器强制的边界:只有 internal 的父目录及其子目录内的包能导入它。上面的例子中,example.com/hello/internal/store 只能被 example.com/hello/... 导入,外部模块引用会直接编译失败。这是发布库时保护实现的推荐做法。
包名约定
- 包名小写、单数、无下划线:
http、strings、user。 - 不要用
util、common、helpers这种大杂烩名字,它们会无限膨胀。 - 调用处读起来自然:
user.New()优于users.UserNew()。 - 目录名与包名通常一致;不一致时以
package声明为准(internal/store里写package storage也合法,但会让人困惑)。
依赖管理
go get github.com/google/uuid@latest # 加依赖(会写 go.mod / go.sum)
go get github.com/google/uuid@v1.6.0 # 固定版本
go mod tidy # 清理未使用依赖、补全缺失依赖
go mod download # 预下载到本地缓存
go list -m all # 列出所有依赖
go mod graph | grep uuid # 查依赖来源
go.sum 记录每个模块的校验和,必须提交;go.mod 里的 require 是最小版本选择(MVS)的结果:
require (
github.com/google/uuid v1.6.0
golang.org/x/sync v0.10.0 // indirect
)
// indirect 表示不是直接导入,而是依赖的依赖。不要手改这些行,用 go mod tidy 维护。
版本选择规则
Go 采用最小版本选择:多个依赖要求同一模块的不同版本时,选满足全部要求的最高最小版本。这带来两个后果:
- 升级某个依赖可能顺带提升其他包的版本(看
go mod tidy后的 diff)。 - 想强制某个版本,用
go get module@version显式提升,而不是replace。
replace 的用途
replace 把某个模块指向本地路径或另一个版本,适合本地联调:
replace example.com/lib => ../lib
注意:
replace不传递:你的库用replace,引入你的项目不会继承。- 提交前想清楚是否需要保留;临时调试完记得删。
- 不能用
replace绕开版本冲突,那只是把问题藏起来。
vendor 目录
go mod vendor # 把依赖复制到 vendor/
go build -mod=vendor
适合无法访问代理的构建环境,或需要审计全部依赖源码的团队。代价是仓库变大、升级要重新 vendor。有 vendor/ 时 Go 默认用它,别让它和 go.mod 不一致。
工作区(多模块)
一个仓库里有多个 module 时,用 go.work 把它们的本地路径连起来:
go work init ./api ./lib
go work use ./tools
go.work 通常不提交(每人本地路径可能不同),CI 里直接按 go.mod 构建。
循环依赖
Go 不允许包级循环导入,编译直接报 import cycle not allowed。常见解法:
- 抽公共包:把双方都依赖的类型放到第三个包(如
model)。 - 依赖接口而非实现:调用方在自己包里定义接口,实现方实现它。
- 合并:确实是同一职责,就合成一个包。
// package a 不再导入 b,只声明需要的接口
package a
type Fetcher interface {
Fetch(id string) (string, error)
}
func Run(f Fetcher) error { _, err := f.Fetch("1"); return err }
常见错误与注意事项
1. 目录里混了两个包名
store/
a.go package store
b.go package storage
编译报 found packages store and storage。同目录只能一个包名。
2. main 包不在根目录
go run ./cmd/server 而不是 go run .;一个模块可以有多个 main 包,分别放在 cmd/<name>/ 下。
3. 导入路径写成本地路径
import "./greet" 在 module 模式下无效。用完整模块路径。
4. 忘了 go mod tidy
删除代码后依赖仍留在 go.mod,或者新增 import 后构建报缺少模块。
5. 依赖私有仓库
需要配 GOPRIVATE 与凭据:
go env -w GOPRIVATE=git.example.com/*
否则会尝试走公共代理并失败。
6. 用包级变量做「全局配置」
测试之间互相污染。需要共享状态时显式传参或用结构体,别用可变包级变量。
7. 版本号写 v0 却当稳定版用
v0.x 与 v1+ 的兼容性承诺不同,对外发布的库在接口稳定后再打 v1.0.0。
小结
go.mod定义模块,目录定义包,导入路径 = 模块路径 + 相对路径。- 大写导出,
internal/由编译器强制隔离。 go.sum必须提交;go mod tidy是日常维护命令。- 版本用
go get module@vX.Y.Z显式控制,replace只用于临时联调。 - 循环依赖靠抽公共类型或依赖接口解决。