笔记内容来自《Go语言第一课》
Go 包
包的定义与导入
Go 程序由多个 Go 包组合而成。在一个 Go module 中,通常每个目录对应一个包,该目录下的所有 .go 源文件(不含以 _test.go 结尾的测试文件时也成立;测试文件在测试构建时一并参与编译)共同构成该包。每个源文件都需要以包声明开始:
// foo 目录下的 foo.go
package foo // 包声明
// 定义 foo 包中的类型、方法、变量和函数等
说明:目录名通常与包名保持一致是社区惯例,但并非语法强制(例如目录
foo/v2下仍可声明package foo)。另外,同一目录下除*_test.go外的非测试源文件必须声明为同一个包名;测试文件可以是package foo或package foo_test。
定义完成后,这些包就可以被其他包导入并使用。可以使用 import 导入一个包,导入路径一般是 module path + 子目录:
package bar
import "github.com/xxx/foo"
当有多个包需要导入时,可以将它们放在一个 import 块中:
package bar
import (
"fmt"
"log"
"github.com/xxx/foo"
)
导入完成后,可以使用包内导出的标识符(首字母大写的标识符)。例如在 bar 包中调用 foo 包中的 Foo 函数时,需要用包名进行限定:
package bar
import "github.com/xxx/foo"
func Bar() {
foo.Foo()
}
也可以省略每次调用时的包前缀(不推荐):
package bar
import . "github.com/xxx/foo"
func Bar() {
Foo() // 等同于 foo.Foo()
}
这种方式可能导致命名冲突。如果 bar 包自身也包含名为 Foo 的函数,那么编译器将无法确定 Foo 指向哪个标识符。
此外,还存在导入路径不同但默认包名相同导致的冲突:
package bar
import (
"github.com/bigwhite/foo"
"github.com/example/foo"
)
func Bar() {
foo.Foo() // 这里引用的是哪个 foo 包?
}
此时需要使用导入别名机制:
package bar
import (
myFoo "github.com/bigwhite/foo"
exampleFoo "github.com/example/foo"
)
func Bar() {
myFoo.Foo()
exampleFoo.Foo()
}
Go 不允许直接或间接地导入自己,即不支持任何形式的循环导入/循环依赖。
另外,import还支持一种特殊形式——空导入:import _ "github.com/lib/pq"下划线
_作为该包的“别名”,表示仅导入但不显式使用其导出标识符;这种写法通常用于触发包的init函数(例如注册数据库驱动、插件等)。
包的初始化函数
func init() {
// 包初始化逻辑
}
init 函数会在 main.main 之前执行。当一个包被导入时,Go 会在包初始化过程中自动调用该包的所有 init 函数(包括 main 包中的 init)。因此,任何需要在 main.main 执行之前完成的工作都可以放在 init 中。
init 不能被显式调用,否则会触发编译错误。
一个包中可以定义多个 init 函数,包内每个源文件也可以定义自己的 init。执行顺序为:先初始化依赖包,再初始化当前包;在同一包内,按源文件名的字典序(文件级)以及同一文件内的出现顺序依次执行。每个 init 在一次程序运行中只会执行一次。
程序的编译单元
Go 编译器以包为单位进行编译,而不是单独的文件。一个包可以包含多个源文件;以 _test.go 结尾的是测试文件,通常与同目录下的包一起在测试构建中编译。
- 由于每个 Go 源文件在开头显式列出了依赖包,编译器不必读取整个文件就能确定依赖关系。
- Go 包之间禁止循环依赖,因此包可以独立编译,也便于并行编译。
- 已编译的包对象文件记录了其依赖包的导出信息,编译器在类型检查和编译时可以直接使用这些信息。
Go module
Go 项目仓库(repo)与 Go module 的关系通常如下:
在 Go 中,一个项目仓库通常对应一个 Go module,即仓库根目录下包含 go.mod 文件。go.mod 所在目录就是该 module 的根目录;根目录及其子目录(不包括包含自己 go.mod 的独立子 module)下的所有 Go 包均属于同一个 module。当前工作目录所在 module 通常称为 main module。
Go 允许在一个代码仓库中定义多个 module(例如存在多个
go.mod),但并不常见。借助 Go module,开发者可以更灵活地管理依赖关系,无需手动下载和维护第三方包。常用命令包括go get(添加/调整依赖版本)、go mod download(预下载依赖)、go mod tidy(整理并补全/移除依赖)等。
Go 项目的代码组织结构
Go 项目的代码组织结构经历了多次演进,目前较为通行的做法与官方建议、社区共识基本一致。
社区共识
可执行程序(应用)
$ tree -F exe-layout
exe-layout
├── cmd/
│ ├── app1/
│ │ └── main.go
│ └── app2/
│ └── main.go
├── go.mod
├── go.sum
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
├── pkg2/
│ └── pkg2.go
└── vendor/
cmd/:存放要编译为可执行程序的main包源码。若包含多个可执行程序,通常为每个程序建立子目录(如app1/、app2/)。main包应尽量保持精简,负责命令行参数解析、配置与资源初始化、日志初始化、启动/关闭流程编排等。go.mod、go.sum:依赖管理文件。internal/:仅供本 module 内部使用的包(对外不可导入),用于承载不希望作为公开 API 的实现细节。pkgN/:用于存放可能被外部项目复用的库代码(是否需要该目录取决于团队习惯;很多项目也直接将导出包放在根目录或按领域分组)。vendor/:用于将依赖以源码形式落盘以便构建时直接使用。Go module 支持可重现构建后,vendor变为可选;通常只在需要离线构建、严格审计或特定交付要求时使用(可通过go mod vendor生成)。
对于仅包含一个可执行程序的项目,结构可以简化为:
$ tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
此时可移除 cmd/,将唯一的 main 包放在项目根目录,其余布局元素含义不变。
Go 库(Library)
$ tree -F lib-layout
lib-layout
├── go.mod
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
└── pkg2/
└── pkg2.go
Go 库项目通常不需要构建可执行程序,因此不需要 cmd/。是否使用 vendor/ 取决于交付与构建要求;库项目一般通过 go.mod 明确依赖及版本即可。
Go 库项目的主要目的是对外提供 API;仅供项目内部使用、不希望公开的包,可以放在 internal/ 下。
对于仅包含一个包的 Go 库项目,可进一步简化为:
$ tree -L 1 -F single-pkg-lib-layout
single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└── internal/
官方与参考布局
可参考社区布局指南:github.com/golang-standards/project-layout/
需要注意:
- 对于规模较大或复杂的项目,常会出现大量“支持包”(supporting packages),这些包不希望被外部依赖,以便未来重构优化,通常建议放在
internal/下。 - 有些官方示例会将包含可执行程序的项目称为 “command” 类,将纯库称为 “package” 类;不同语境下
cmd/、pkg/的使用会有所差异。 - 在部分官方示例的项目类型中,并不强调使用用于聚合导出包的
pkg/目录;是否引入pkg/主要取决于项目规模与团队约定。 - 当根目录下导出包过多导致结构臃肿时,可以将导出包统一放到根目录下的
pkg/目录中,以提升可读性与层次感。