目录结构

Go的编译与构建#构建过程|编译是按目录(Directory)来管理代码的,当写下 import "github.com/shinerio/my-awesome-project/pkg/validator" 时,Go 会去做两件事:

  1. 找到这个目录下所有.go 文件。
  2. 把这些文件里的代码全部合并在一起看作一个整体,一起加载

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 的编译器有一套查找依赖的优先级逻辑:

  1. 开启 Vendor 模式: 如果你使用了 -mod=vendor 参数(或者在 Go 1.14+ 版本中,如果根目录存在 vendor 目录且 go.mod 版本号 \ge 1.14),Go 会优先从 vendor 目录里找包。
  2. 默认模式(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.modexample/ 目录下放置一个独立的 go.mod 文件(使用 replace 指向本地主模块),这样使用者克隆代码后,无需安装即可直接在例子目录下运行。同时,避免example目录下import无关的三方库,污染要发布的module。
  • 保持简单: 例子应该专注解决一个具体问题,不要把 example 写成复杂的工程。

评论