项目结构
个人在阅读《GO语言圣经》时,并没有发现有关GO项目结构的过多介绍,但是我觉得在开始学习一门语言前,应该先学好如何如何管理好自己的代码,尤其是对经验丰富的程序员,这是非常重要的。对于刚接触这门语言的人,也很重要,一方面这是提前养成良好的习惯,其次也可以培养自己的分类,分层,模块化设计思想,这在一个大型项目中是非常重要的。
这里不以大型项目的项目结构设计来展开,是在有兴趣的话或者有需求的话可以参考高分开源项目 project-layout,本文重点介绍一下GO语言本身提供的一些项目分层的设计。
1 module
GO的基本项目粒度管理单位是module,而单个module又是由多个package组成。module是go 1.11版本引入的依赖管理机制,是一个包含go.mod文件的目录,用于定义项目的依赖关系和版本信息。
我们可以实际来创建一个module,观察下文件目录的结构,首先在一个指定目录下,使用 go init mod 模块名的形式创建一个module,使用tree命令查看目录得到输出如下:
mod1
├── go.mod
└── pkg1
└── add.go根据上面的信息,我们可以看到,在module的根目录下,有一个go.mod, 它是用来定义当前module的直接依赖、版本约束和模块路径的。
2 package
package是Go语言中代码组织的基本单位,一个包由一个或多个.go文件组成,这些文件位于同一目录下,且开头都声明了相同的包名。在上一节的输出示例中,有一个pkg1目录,这就是一个package。package是为了便于项目内部的一个模块化结构,便于进行代码封装和代码复用,一般来说,package和一个文件夹是等价的。
提示
通常情况下,一个目录下只能有一个package,但是为了方便区分测试代码和功能代码,允许出现一个test包,这个包名的组成是 目录名_test的文件,但是声明这个package的文件必须是*_test.go这种带特殊命名后缀的文件, 例如下面这个结构
mod2
├── go.mod
├── go.sum
├── pkg2
│ ├── multi.go
│ └── multi_test.go
└── test.go其中 multi.go 声明自己所属的包是 pkg2, multi_test.go声明自己所属的包是pkg2_test
3 什么是workspace
workspace是多个module的集合,并且通过workspace, 可以维护这些module间的一个依赖关系。
一个简单的示例如下:
首先,我们在之前的基础上,使用go init命令再创建一个module,存在在mod1目录下,整个项目的目录结构如下:
├── mod1
│ ├── go.mod
│ └── pkg1
│ └── add.go
└── mod2
├── go.mod
├── go.sum
├── pkg2
│ ├── multi.go
│ └── multi_test.go
└── test.go每个模块都有自己独立的go.md, 为了将他们集中管理,我们再创建一个workspace
wayne@server:~/source/practice/go$ go work init mod1 mod2
wayne@server:~/source/practice/go$ tree
.
├── go.work
├── mod1
│ ├── go.mod
│ └── pkg1
│ └── add.go
└── mod2
├── go.mod
├── go.sum
├── pkg2
│ ├── multi.go
│ └── multi_test.go
└── test.go
4 directories, 8 files我们会发现,在和module同级的目录多了一个go.work文件,其内容如下:
go 1.25.0
use (
./mod1
./mod2
)如果项目中,有多个内部的module依赖,这是必要的,只有在workspace定义依赖关系,实际import时才能正确导入所依赖的package,不然go的编译器会在GOROOT的路径下面去找,或者是在远程仓库(例如github)去找,这样就达不到我们的目的。
4 依赖引用
代码完整目录结构如下(完整代码示例查看):
.
├── go.work
├── mod1
│ ├── go.mod
│ ├── pkg1
│ │ └── add.go
│ └── pkg2
│ └── add_test.go
└── mod2
├── go.mod
├── go.sum
├── pkg1
│ ├── multi.go
│ └── multi_test.go
└── test.go4.1 同module的package间导入
创建一个名为mod1的module,其工程目录如下:
├── go.mod
├── pkg1
│ └── add.go
└── pkg2
└── add_test.gogo.mod是模块名的定义:
module mod1
go 1.25.0add.go在名为pkg1的package下,我们在其中定义一个Add函数(大写开头自动导出),文件内容如下:
package pkg1
func Add(a, b int) int {
return a + b
}我们在另外一个package目录下再创建一个add_test.go,用于引用pkg1的Add方法进行测试,文件内容如下:
package pkg2
import (
"testing"
"mod1/pkg1" // 导入mod1的pkg1
)
func TestAdd(t *testing.T) {
a := pkg1.Add(1, 2)
if a != 3 {
t.Errorf("Add(1, 2) = %d; want 3", a)
}
}注意在导入时,不能使用相对路径导入,只能是module名 + 包路径信息的形式。
4.2 跨module的package导入
场景一:从非远端仓库import,即引用项目内的module的package
方式和4.1小节相同,关键在于要合理配置workspace,需要将相互依赖的模块添加到工作区,假如mod1和mod2有依赖关系,定义go.work如下:
go 1.25.0
use (
./mod1
./mod2
)场景二:从远端仓库import
需要在go.mod里面使用require关键字定义依赖,并且还需使用go get拉取相关代码缓存,下面是以 github远端仓库的uuid使用为示例github.com/google/uuid。
go.mod配置如下:
module mod2
go 1.25.0
require github.com/google/uuid v1.6.0 // 写明依赖的模块名和版本信息然后还需手动使用go get安装这个依赖包,示例命令如下:
go get -u github.com/google/uuid一般在国内可能存在访问异常的问题,出现问题时大家可以按照如下进行配置了GOPROXY后再重新执行go get
go env -w GOPROXY=https://goproxy.cn,direct