本节摘要:包是 Go 的编译与可见性单元,首字母大小写决定导出与否;模块是版本化的一组包,由 go.mod 声明、go.sum 校验。本节覆盖包的声明导入、init 初始化顺序、别名与空白导入,模块的版本选择与最小版本算法、replace 本地替换与 vendor 离线构建,最后用一个最小 HTTP 服务把包与模块串成实战。
阅读完本节,你应当能够:
每个文件第一行声明所属包,同目录的所有文件必须同包。可见性规则全语言只有一条:标识符首字母大写即导出,小写即包内私有。没有 public/private 关键字,没有友元,读代码时一眼可判。
package counter var count int // 包内私有 func Add() { count++ } // 导出 func reset() { count = 0 } // 私有
导入用路径,调用带包名前缀。冲突或名字太长时起别名;只想要包的副作用(比如某驱动包的 init 自动注册)时用空白标识符:
import ( "fmt" str "strings" // 别名 _ "image/png" // 只为注册PNG解码器 )
包可以有多个 init 函数(甚至同文件多个),它们在包被导入时自动执行,无参数无返回值。执行顺序是确定性规则链:先按导入拓扑(被依赖者先初始化),同包内按文件名排序、文件内按出现顺序。完整链条是:包级变量初始化 → 该包 init → 依赖逐层就绪 → 最后 main 包的 main 执行。
⚠️ 常见坑:init 里做太多事(连数据库、读配置、可能失败的操作)会让包"导入即有副作用",测试与复用都变难。原则:init 只做注册与轻量初始化,可失败逻辑交给显式的初始化函数。
第 1 章用 go mod init 生成了 go.mod。引入第三方依赖后它会多出 require 块:
module example.com/myapp go 1.22 require github.com/gin-gonic/gin v1.9.1
每条依赖是"模块路径加语义化版本"。Go 的版本选择用最小版本算法:不挑"最新",挑"所有依赖声明中满足要求的最低版本"——可重现且不被上游新版本突袭。go.sum 是 accompanying 的校验文件,记录每个依赖的哈希,任何模块内容被篡改,构建立刻报错。两条纪律:go.sum 提交进版本库;go.mod 也提交,两者都不要手工编辑,让 go get 与 go mod tidy 代劳。
常用命令速查:
| 命令 | 作用 |
|---|---|
| go mod init | 建模块 |
| go get 包路径@版本 | 加依赖或升降级 |
| go mod tidy | 按实际 import 清理依赖 |
| go mod download | 拉依赖到本地缓存 |
| go mod vendor | 产出到 vendor 目录 |
调试本地修改的库时,不想先发版。replace 指令把网络路径指到本地目录:
replace example.com/mylib => ../mylib
go.mod 里这一行让你直接用工作区旁边的源码调试,上线前删掉。多模块联调还有工作区模式:一个工作区文件统一编排若干本地模块,不用逐个 replace。
离线或供应链强管控场景用 vendor:go mod vendor 把全部依赖源码拷进项目的 vendor 目录,构建时加标志走 vendor,彻底不碰网络。代价是仓库变大,CI 缓存策略要跟着调整。
一个多文件结构的 HTTP 服务骨架示意:入口包只做启动,业务在子包,配置在独立包——导入关系单向向下:
// 入口包 package main import ( "log" "net/http" "example.com/myapp/internal/handler" ) func main() { http.HandleFunc("/ping", handler.Ping) log.Fatal(http.ListenAndServe(":8080", nil)) }
// handler子包 package handler import ( "encoding/json" "net/http" ) type resp struct{ Status string } func Ping(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp{Status: "ok"}) }
注意 internal 目录的妙用:Go 编译器强制规定,internal 下的包只有其父目录树内的代码能导入——这是语言级的封装,比口头约定硬得多。
💡 关键直觉:包的粒度按"会一起变化的东西"划分,而不是按"文件类型"划分。所有 handler 堆一个包、所有 model 堆一个包,是两个方向相同的错误。
问答三则。循环导入怎么办? 编译器直接拒绝,这是在告诉你包边界划错了——抽出公共部分下沉,或用接口反转依赖。internal 能嵌套吗? 能,规则按最近一层 internal 判定。go.mod 里的 indirect 是什么? 标记间接依赖:不是你直接 import 的,是你的依赖需要的,tidy 自动维护。
下一节直面失败路径:错误处理。