包管理

Golang的包管理有四种方式,分别是:go pathgo vendorgo modulego work,其中是go pathgo vendor已经算是“上古时代”的产物,本文主要详细介绍一下后两种管理方式。

1. Go Module #

Go1.13版本开始,go module将是Go语言默认的依赖管理工具,平常我们用的最多的命令应该就是:go mod init myModulego mod tidy,这些相信大家已经很熟练了,但是到实际要引用某个包下的函数时,往往写import xxx/xxx语句时会不知道该怎么开头,以下是一个示例,包含了工作中的大部分情况:

testModule/         
|-- softwareExample
|   |-- example.go
|   |-- go.mod
|   |-- go.sum
|   |-- httpclient
|   |   |-- httpclient.go
|   |   |-- task
|   |   |   `-- taskapi.go
`-- utils.go
|-- go.mod
|-- go.sum

以上文件结构展示了一个Module中包含一个子Module的情况,另外softwareExample模块中还有多个package的情况。以上各个文件中的内容如下:

testModule/go.mod

module test
go 1.20

testModule/utils.go

package testUtil

func Util_ADD(a, b int){
    fmt.Println(a+b)
}

testModule/softwareExample/go.mod

module software
go 1.20

testModule/httpclient/httpclient.go

package httpclient

func GetHttpclient(){
}

testModule/httpclient/task/taskapi.go

package taskapi

func GetTask(){
}

我特意将Module、package的名称与文件夹的名称区分开,在引用时避免歧义。大部分会用到的情况如下:

1.1、子Module要使用父Module中的函数 #

如果testModule/softwareExample/example.go需要用到testModule/utils.go中的函数,那么应该从父Module写到最终层Package,不用关心文件夹的命名,只看文件中的Module和package的命名,类似这样:

testModule/softwareExample/example.go

package example
import "test/testUtil"

func GetExample(){
    testUtil.Util_ADD(1,2)
}

除此之外,你还需要在子Module中的go.mod增加对父Module的引用,本地包还需要用到replace关键字指明文件夹位置,请注意,在require和replace的前半部分中指明Module的名称,而replace的后半段则是要指明文件夹的位置,类似这样:

testModule/softwareExample/go.mod

module software
go 1.20

require test v0.0.0-00010101000000-000000000000
replace (
	test => ../../testModule
)

1.2、外部包要使用子Module中的函数 #

假设我有一个同层级包,名为myPackage,其中main.go文件需要用到testModule/httpclient/task/taskapi.go中的GetTask()函数,那么应该从子Module写到最终的package,不用关心父Module,同样不用关心文件夹的命名,只看文件中的Module和package的命名,类似这样:

myPackage/main.go

package main
import "software/httpclient/taskapi"

func main(){
    taskapi.GetTask()
}

由于之前子Module中引用了父Module,所以即使这个main.go中没有直接调用父Module中的函数,但也需要在go.mod中声明父Module的位置,类似这样:

myPackage/go.mod

module software
go 1.20

require software v0.0.0-00010101000000-000000000000
replace (
	test => ../testModule  
	software => ../testModule/softwareExample
)

这样就可以myPackage/main.go中顺利调用到子Module中的函数,不过有一点不方便的是:如果这个子Module引用了其他的Module,我们需要在myPackage/go.mod中加入replace指定其他Module的文件夹位置。这一点很不友好,如果已经有上百个这样的myPackage写好了,这时software又引入了新的Module,结果就导致我们需要在上百个myPackage中挨个在go.mod里增加replace,来指明那个新Module的位置。

为了解决这个问题,在Go1.18版本之后,引入了go.work来解决该问题。

2. Go Work #

go work 即工作空间,一般来说,整个项目只有一个go.work文件,由它来管理所有的包。go.work是整个工作空间的基本配置文件,go.work文件主要用于本地开发使用,不进行git提交

还是刚才的例子:

WorkSpace/  
	go.work
	
    testModule/         
    |-- softwareExample
    |   |-- example.go
    |   |-- go.mod
    |   |-- go.sum
    |   |-- httpclient
    |   |   |-- httpclient.go
    |   |   |-- task
    |   |   |   `-- taskapi.go
    `-- utils.go
    |-- go.mod
    |-- go.sum

    // 外部包 
    myPackage/
    `-- main.go
    |-- go.mod
    |-- go.sum

我们在WorkSpace目录层级下执行go work init,会在WorkSpace目录下生成一个go.work文件,我们需要在go.work中使用关键字use来指定需要被管理的文件夹,然后同样用replace来指明Module所在的文件夹:

go 1.20

use (
    ./testModule
    ./myPackage
)

replace(
    test v0.0.0-00010101000000-000000000000 => ./testModule  
	software v0.0.0-00010101000000-000000000000 => ./testModule/softwareExample
)

在go.work中声明以上内容后就可以去掉myPackage/go.mod中的replace引用了,如果有上百个类似myPackage的包,而software又新引用了一个其他的Module,这时就不用去到各个myPackage下的go.mod中增加replace,而只用在go.work中增加一行replace即可,类似这样:

go 1.20

use (
    ./testModule
    ./myPackage
    ./myPackage1
    ./myPackage2
    ./myPackage3
)

replace(
    test v0.0.0-00010101000000-000000000000 => ./testModule  
	software v0.0.0-00010101000000-000000000000 => ./testModule/softwareExample
    newmodule v0.0.0-00010101000000-000000000000 => ./newmodule
)

3. 补充:Internal包的使用 #

当一个项目下的「功能一」包依赖「功能二」包里的函数时,那么「功能二」包中的成员必须是导出函数才能被「功能一」包引用。但是这样一来,其他项目或者其他组织的代码也就都可以使用「功能二」包导出的函数了,假如包里的一些成员我们只想在指定的包之间共享而不想对外暴露该怎么办呢? Go 语言internal包这个特性可以让我们实现这个目标。

内部包的规范约定:导出路径包含internal关键字的包,只允许internal的父级目录及父级目录的子包导入,其它包无法导入。当 go 编译器在导入路径中看到带有internal/的软件包的导入时,它将验证导入包的程序文件是否位于internal/目录的父级目录,或父级目录的子目录中。

以如下目录结构为例说明:

├─ pkg1
│   ├─ internal
│   │   ├─ sub2
│   │       └─ sub2.go
│   │   └─ test1.go
│   │
│   ├─ sub1
│   │   └─ test2.go
│   └─ pkg1.go
├─ pkg2
│   └─ pkg2.go
└─ main.go
  • 可以导入internal包的代码:test1.go、test2.go、pkg1.go和sub2.go

  • 不能导入internal包的代码:main.go和pkg2.go。

  • 可以导入sub2包的代码:test2.go、pkg1.go和test1.go

  • 不能导入sub2包的代码:main.go和pkg2.go。