Go的编译与构建#构建过程|编译是按目录(Directory)来管理代码的,当写下 import "github.com/shinerio/my-awesome-project/pkg/validator" 时,Go 会去做两件事:
- 找到这个目录下所有的
.go文件。 - 把这些文件里的代码全部合并在一起看作一个整体,一起加载
1. 典型目录结构
my-awesome-project/
├── cmd/ # 程序的入口(main 函数所在处)
│ ├── app-server/ # 项目的主服务器程序
│ │ └── main.go
│ └── app-cli/ # 如果有命令行工具,放在这里
│ └── main.go
├── internal/ # 私有应用代码(重点:此目录下的包无法被外部项目导入)
│ ├── auth/ # 认证逻辑
│ │ ├── service.go
│ │ └── repository.go
│ ├── config/ # 配置加载
│ │ └── config.go
│ └── db/ # 数据库连接初始化
│ └── mysql.go
├── pkg/ # 可以被外部项目引用的公共库代码
│ ├── logger/ # 自定义的日志封装
│ └── validator/ # 通用的验证器
├── api/ # API 定义文件(如 Swagger, Proto 文件)
│ └── openapi.yaml
├── configs/ # 配置文件(如 yaml, json, env)
│ └── config.yaml
├── deployments/ # 部署相关(Docker, Kubernetes, Terraform)
│ └── Dockerfile
├── scripts/ # 编译、安装、分析用的脚本
│ └── build.sh
├── test/ # 额外的外部测试数据或集成测试
├── go.mod # 模块依赖定义
├── go.sum # 依赖版本校验和
└── README.md
2. vendor
在Go Modules出现之前,依赖管理是一片“西部世界”。vendor目录的出现,本质上是为了实现依赖的本地化隔离。简单来说,vendor 目录就是一个存放项目所有外部依赖包副本的文件夹。
2.1. vendor 的核心作用
当你运行 go mod vendor 命令时,Go 会把 go.mod 中记录的所有第三方库下载下来,并全部塞进项目根目录下的 vendor 文件夹里。
它的主要意义在于:
- 离线构建: 所有的代码都在本地。即使公司内网断了,或者GitHub挂了,你依然可以编译项目。
- 版本锁死(确定性): 即使原作者在 GitHub 上删除了库,或者由于某些原因版本找不到了,你的
vendor里依然有一份拷贝,保证项目永远能跑起来。 - 无需重复下载: 在 CI/CD(持续集成)环境中,不需要每次都去远程下载几百个包,直接从本地读取,速度极快。
2.2. Go 如何对待 vendor?
Go 的编译器有一套查找依赖的优先级逻辑:
- 开启 Vendor 模式: 如果你使用了
-mod=vendor参数(或者在 Go 1.14+ 版本中,如果根目录存在vendor目录且go.mod版本号 \ge 1.14),Go 会优先从vendor目录里找包。 - 默认模式(Module Cache): 如果没有
vendor目录,Go 会去系统的全局缓存目录($GOPATH/pkg/mod)里找。
2.3. 现在还有必要用 vendor 吗?
自从 Go Modules(2018年以后)成熟以来,关于是否保留 vendor 有两派观点:
2.3.1. 应该使用 vendor 的场景:
- 企业内网开发: 无法访问外网,或者访问 GitHub 极慢。
- 极端稳定性要求: 防止依赖库被作者“投毒”或物理删除。
- 单体仓库大型项目: 方便在没有网络的环境下快速分发完整的源码包。
2.3.2. 不建议使用 vendor 的场景:
- 开源库: 如果你在写一个库给别人用,千万不要带上
vendor,这会导致引用你的用户遇到包冲突。 - 个人/普通项目: 使用 Go Proxy(如
goproxy.cn)已经足够快且稳定,没必要在 Git 里多存几万行别人的代码,让仓库变得臃肿。
2.4. 常用命令
| 命令 | 作用 |
|---|---|
go mod vendor |
根据 go.mod 创建或更新 vendor 目录。 |
go build -mod=vendor |
强制要求编译器只从 vendor 目录读取代码,不联网。 |
go mod tidy |
清理没用的依赖,但它不会自动同步 vendor,之后通常要补一个 go mod vendor。 |
2.5. 避坑指南
如果你决定使用 vendor,记得把整个 vendor 目录提交到 Git。如果不提交,这个目录就失去了“本地副本”的意义。
3. go编译包含的目录
在Go的构建体系里,只有被main 包显式 import(导入) 的代码才会被编译器链接到最终的可执行文件中。如果你在开发一个 Web 服务(比如在 cmd/server/main.go),只要你的代码里没有写 import "your-project/example",那么:
- 体积: 最终编译出的二进制文件不会变大。
- 速度: 编译器甚至不会去扫描那个目录。
不同的场景下,“打包”的含义不同:
| 场景 | example/ 是否包含其中 | 原因 |
|---|---|---|
go build 生成二进制 |
不包含 | 编译器只链接被引用的代码。 |
go get 下载库 |
包含 | 源码会被下载到本地 pkg/mod,方便开发者查看示例。 |
| 发布到 GitHub/GitLab | 包含 | 它是项目仓库的一部分。 |
| Docker 镜像 | 通常不包含 | 建议在 Dockerfile 中仅 COPY 必要的源码或编译好的二进制。 |
3.1. 特殊的例外://go:embed
如果你在代码中使用了 Go 1.16 引入的 embed 特性,并且路径匹配到了 example/ 目录,那么这些文件的内容会被当作静态资源嵌入到二进制文件中。
// 这种极少数的情况会导致 example 被打包
//go:embed example/*
var exampleFiles embed.FS
3.2. example目录
在Go项目(尤其是开源库或大型项目)中,example/ 目录是一个非常重要的约定。简单来说,它的核心作用是:通过真实、可运行的代码告诉使用者如何使用你的库。
3.2.1. 主要作用
3.2.1.1. 提供“活的”文档
相比于 README.md 中零散的代码片段,example/ 目录下的代码是完整且可运行的。
- 消除歧义: 开发者可以直接
go run其中的代码,观察输出结果。 - 上下文补全: 文档往往只展示核心逻辑,而 example 会展示如何初始化、如何处理错误以及如何关闭资源。
3.2.1.2. 与 go test 集成(示例函数)
Go 语言有一个非常强大的特性:可测试的示例(Example Functions)。
如果你在 _test.go 文件中编写以 Example 开头的函数,它们不仅可以作为测试运行,还会自动显示在生成的 pkg.go.dev 官方文档中。
3.2.1.3. 验证 API 设计
对于开发者(项目维护者)来说,编写 example/ 是自检的过程:
- 如果写一个简单的例子需要配置几十行代码,说明 API 设计太复杂了。
- 它强迫你以使用者的角度去思考问题。
3.2.1.4. 常见的目录结构
通常 example/ 目录会有以下两种组织方式:
| 模式 | 适用场景 | 示例 |
|---|---|---|
| 单文件模式 | 逻辑简单,一两个文件就能讲清楚。 | example/main.go |
| 子项目模式 | 针对不同功能模块提供多个示例。 | examples/basic/, examples/advanced/ |
3.2.1.5. 最佳实践建议
如果你正在维护一个 Go 项目,建议这样利用 example/ 目录:
- 包含
go.mod: 在example/目录下放置一个独立的go.mod文件(使用replace指向本地主模块),这样使用者克隆代码后,无需安装即可直接在例子目录下运行。同时,避免example目录下import无关的三方库,污染要发布的module。 - 保持简单: 例子应该专注解决一个具体问题,不要把 example 写成复杂的工程。
评论