目 录CONTENT

文章目录
go

【Go】Go项目的代码组织结构

hxuanyu
2025-12-18 / 0 评论 / 2 点赞 / 65 阅读 / 0 字

笔记内容来自《Go语言第一课》

Go 包

包的定义与导入

Go 程序由多个 Go 包组合而成。在一个 Go module 中,通常每个目录对应一个包,该目录下的所有 .go 源文件(不含以 _test.go 结尾的测试文件时也成立;测试文件在测试构建时一并参与编译)共同构成该包。每个源文件都需要以包声明开始:

// foo 目录下的 foo.go
package foo // 包声明
// 定义 foo 包中的类型、方法、变量和函数等

说明:目录名通常与包名保持一致是社区惯例,但并非语法强制(例如目录 foo/v2 下仍可声明 package foo)。另外,同一目录下除 *_test.go 外的非测试源文件必须声明为同一个包名;测试文件可以是 package foopackage 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.modgo.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/ 目录中,以提升可读性与层次感。
2
博主关闭了所有页面的评论