【Go】Go项目的代码组织结构
编辑
笔记内容来自《Go语言第一课》
Go包
包的定义与导入
Go程序由多个Go包组合而成。在Go项目中,每个目录代表一个Go包,并且该目录的名字要与包名保持一致。包目录下的所有Go源文件都属于该Go包的一部分,并且每个源文件都需要以包声明开始:
// foo目录下的foo.go
package foo //包声明
// 定义foo包中的类型、方法、变量和函数等
定义完成后,这些包就可以被其他包导入并使用。可以使用import+modulepath+子目录的方式导入一个包:
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的函数,那么Go编译器会无法区分Foo函数调用的意图是访问foo.Foo还是bar.Foo。
此外,还有一种包冲突的情况:
package bar
import (
"github.com/bigwhite/foo"
"github.com/example/foo"
)
func Bar() {
foo.Foo() // 这里引用的是哪个foo包?
}
这种情况下编译器也无法确定使用的是哪个foo包,因此,Go引入导入别名机制:
package bar
import (
myFoo "github.com/bigwhite/foo" // 为第一个foo包指定别名myFoo
exampleFoo "github.com/example/foo" // 为第二个foo包指定别名exampleFoo
)
func Bar() {
myFoo.Foo() // 调用github.com/bigwhite/foo中的Foo函数
exampleFoo.Foo() // 调用github.com/example/foo中的Foo函数
}
Go不允许直接或间接地导入自己,即不支持任何形式的循环导入或循环依赖。这意味着如果包a导入了包b,而包b又导入了包c,那么包c不能反过来导入包a。此外,Go import关键字还支持一种特殊的包导入方式—空导入,代码示例如下。
import _ "github.com/lib/pq"在上述代码中,下画线“_”被用作pq包的“别名”,这表示这是空导入。空导入意味着虽然导入了该包,但并不使用其导出的任何标识符。
这种写法通常用来触发包中的
init函数。
包的初始化函数
func init() {
// 包初始化逻辑
…
}
init函数会在main.main函数之前执行。当一个包被导入时,如果包中定义了init函数,或者main包中定义了init函数,Go程序会在进行包初始化过程中自动调用这些init函数。因此,任何需要在main.main函数执行之前完成的工作都可以放在init函数中。
init函数不能显式手动调用,否则会导致编译器错误。
Go的包中可以定义多个init函数,每个组成包的源文件也可以定义自己的init函数。在包初始化过程中,Go会按照一定的顺序一次调用这些init函数。一般来说,先被传递给Go编译器的源文件中的init函数会优先执行;在同一个源文件内,init函数按照出现的顺序执行。每个**init**函数在包初始化过程中只会被执行一次。
程序的编译单元
Go编译器会以包为单位进行编译,而不是单独的文件, 一个包中可以包含多个源文件,一般带有_test后缀的为测试代码,这些文件都属于同一个包。
- 由于每个Go源文件在开头显式列出了所有依赖的包,因此编译器不必读取整个文件就可以确定依赖关系。
- Go包之间禁止循环依赖,包可以独立进行编译,也可以并行编译。
- 已编译的Go包目标文件记录了其所依赖包的导出符号信息,因此编译器在读取目标文件时不需要进一步访问依赖包的目标文件。
Go module
Go项目仓库(repo)与Go module之间的关系:
在Go中,一个项目仓库通常对应一个Go module,即每个Go项目仓库包含一个位于根路径下的go.mod文件。这个go.mod文件所在的顶层目录就是module的根目录,而根目录及其子目录(不包括包含自己go.mod文件的独立子模块)下的所有Go包均属于同一个Go module,这个module通常成为main module或work module。
虽然Go允许在一个项目代码仓库中定义多个Go module,例如,etcd项目就属于这种情况,但这种做法并不常见。借助Go module,开发者可以更灵活地管理和更新项目的依赖关系,无需手动处理第三方包的下载和管理。Go提供了一系列专门针对Go module的操作命令,如go get、go mod download/tidy等,简化了依赖项的安装、删除和更新过程。基于上述对Go包和Go module的理解,接下来我们将深入讨论Go项目的代码组织结构。
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包源文件。如果项目包含多个可执行程序,每个文件的main包会放在各自的子目录中,例如app1、app2等。通常main包应包吃简洁,用于命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等任务。 go.mod、go.sum:Go包依赖管理使用的配置文件。pkgN:用于存放项目内部使用的库文件,这些库既可以被main包依赖,也可以被外部项目引用。vendor:用于缓存特定版本的依赖包。随着Go module本身支持可重现构建,已经变成可选项,只有在必要时才保留vendor目录。
对于仅包含一个可执行程序的项目,目录结构可以进一步简化:
$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
这种情况可以移除cmd目录,并将唯一的main包放置在项目根目录下,其余的布局元素的功能保持不变。
Go库
$tree -F lib-layout
lib-layout
├── go.mod
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
└── pkg2/
└── pkg2.go
和Go可执行程序项目相比,Go库项目要简单一些,因为它们不需要构建可执行程序,不需要cmd目录。此外,不推荐使用vendor目录缓存第三方依赖,库项目应仅通过go.mod文件明确表述对其他module或包的依赖及其版本要求。
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/
官方指南与社区共识基本一致,但需要注意以下几点:
-
“带有支持包”的项目:对于一些复杂或规模较大的
package项目,许多功能会被拆分到支持包(supporting package)中。这些支持包通常不希望被外部使用,因此不作为公开API的一部分,以便后续重构和优化,Go官方建议将这些包放在internal目录下。 -
官方指南称Go可执行程序项目为
command类项目,Go库项目为package类项目,cmd和pkg目录的应用有所不同。 -
在官方指南中的7中项目类型中,并未设计用于聚合其它包的
pkg目录。 -
cmd目录仅出现在“兼有命令和包”的项目中,布局如下:project-root-directory/ ├── go.mod ├── modname.go ├── modname_test.go ├── auth/ │ ├── auth.go │ ├── auth_test.go │ └── token/ │ ├── token.go │ └── token_test.go ├── hash/ │ ├── hash.go │ └── hash_test.go ├── internal/ │ └── trace/ │ ├── trace.go │ └── trace_test.go └── cmd/ ├── prog1/ │ └── main.go └── prog2/ └── main.go
对于包含大量导出包的多包类型项目,如果根目录下的导出包过多,会使项目结构布局显得臃肿。在这种情况下,可以将所有导出包统一放置在项目根目录的pkg目录下。
- 1
-
分享