[golang] 仓库,模块和包

Go 中代码的组织管理通过仓库,模块及包三个概念来完成,其中

  • 仓库(repository)就是大伙熟知的代码仓库
  • 模块(module)是库(library)或程序(app)的根节点,存放于仓库,可包含多个包
  • 包(package)同一目录下的源码文件,编译后会归到一起

使用其他包的代码前,要确保自己的项目已经初始化成了模块,只有模块之间才会形成引用与依赖。类似 Java中的包使用 com.companyname.prjectname.library 来标识每一个包,Go 中每个模块也有唯一的标识,以它的仓库地址来唯一确定,比如 github.com/wayou/hello

go.mod

通过 go mod init <module_path> 来初始化模块,

$ go mod init github.com/wayou/hello
go: creating new go.mod: module github.com/wayou/hello
go: to add module requirements and sums:
        go mod tidy

该命令将当前目录初始化为 Go 模块,同时生成一个 go.mod 文件,其内容如下:

module github.com/wayou/hello

go 1.16

该文件保存了项目中使用的 Go 版本,模块名,如果有安装三方依赖,也会存放在这里进行管理。

三方依赖通过 go get <module_path> 来安装:

$ go get github.com/subosito/gotenv

添加了一些依赖后,go.mod 中便有了依赖列表:

module github.com/wayou/hello

go 1.16

require (
        github.com/lib/pq v1.10.2 // indirect
        github.com/subosito/gotenv v1.2.0 // indirect
)

除了上述内容,该文件中还可指定 replace 自定义定依赖模块的位置,本地开发调试模块时会用,还可配置 exclude 字段,排除某个包的某个版本,防止被引入。

导入导出

包中使用大写字母开头的名称(变量,方法,常量,类型及结构体中的字段)会被导出,对应地,小写或下划线开头的名称只能包内使用。

首先创建一个文件夹,存放需要被调用的模块

$ mkdir greetings && cd $_
$ go mod init github.com/wayou/grettings

# 再创建源码文件
$ touch greetings.go

其中源码内容中包含一个导出的方法,作用是将输入的字符串进行格式化后打印:

// grettings/grettings.go

package greetings

import "fmt"

// Hello returns a greeting for the named person.
func Hello(name string) string {
    // Return a greeting that embeds the name in a message.
    message := fmt.Sprintf("Hi, %v. Welcome!", name)
    return message
}

创建另外个文件夹,在其中调用上面的模块:

$ mkdir hello && cd $_
$ go mod init github.com/wayou/hello

# 创建源码文件
$ touch hello.go

其中导入并调用的逻辑为:

package main

import (
    "fmt"

    "github.com/wayou/greetings"
)

func main() {
    // Get a greeting message and print it.
    message := greetings.Hello("Gladys")
    fmt.Println(message)
}

因为我们的模块是在本地,所以直接引用 github.com/wayou/grettings 是安装不到的,因为我们并没有发布到 github。这里需要使用前面提到的 -replace 指定模块的位置,映射到本地:

$ go mod edit -replace github.com/wayou/greetings=../greetings 

此时需要执行一下 go mod tidy 清理一下依赖,即可正确安装依赖的模块:

$ go mod tidy
go: found github.com/wayou/greetings in github.com/wayou/greetings v0.0.0-00010101000000-000000000000

再看 github.com/wayou/hello 模块的的 go.mod 已经更新成了如下:

module github.com/wayou/hello

go 1.16

replace github.com/wayou/greetings => ../greetings

require github.com/wayou/greetings v0.0.0-00010101000000-000000000000

至此模块就能被正确调用到了,

$ go run .
Hi, Gladys. Welcome!

注意,如果是 GoLand 中,需要开启 Enable Go moduels integration 以正确加载模块,否则编辑器中会飘红。

image

包名冲突的解决

当引入不同的模块后,其中的包名内导出的名称产生冲突时,可通过在 import 时指定别名来解决。

比如用于生成随机数的 math/randcrypto/rand ,同时引入时可这样操作:

import (
	crand "crypto/rand"
	"math/rand"
)

这一处理也适用于引入的包里包含与本地已经存在的名称冲突的情形。

godoc

文档注释没有特别的语法,就是普通注释。

包的注释直接写在包名上面,如果是很长的文档注释,建议创建 doc.go 文件,单独写在这里面。可查看 fmt 包的注释以参考。

image

其他名称(identifier)的注释,默认要以该名称开头,空格后跟对应的注释。比如下面的结构体及方法:

// Money represents the combination of an amount of money
// and the currency the money is in.
type Money struct {
	Value decimal.Deciaml
	Currency string
}

// Convert converts the value of one currency to another.
//
// Supported currencies are:
// 		USD - US Dollar
// 		EUR - Euro
//
// some other description goes here...
//
// some more description...
//
// See more on https://www.example.com/xxx/yyy
func Convert(from Money, to string) (Money, error){
	// ...
}

编辑器中的样子:

image

注意如果要展示时换行,使用一行空格来表示,上面方法最终展示的效果如下图:

image

使用 go doc <PACKAGE_NAME> 来展示包的文档 及列出包内导出的名称,go doc <PACKAGE_NAME.IDENTIFIER_NAME> 来展示包内具体某个名称的文档。

内部包

可创建只在项目内部使用的包,而不必导出给 外部的人使用,通过定义包名为 internal 即可。internal 包只可被直接父级包和同级的相邻包使用。

譬如有如下的包结构:

.
├── bar
│   └── bar.go
├── example.go
├── foo
│   ├── foo.go
│   ├── internal
│   │   └── internal.go
│   └── sibling
│       └── sibling.go
└── go.mod

其中 internal 包只能在 siblingfoo 中使用,在 example.gobar 中使用会报编译错误。

init 函数

包中可定义一个或多个名为 init 的函数,无入参也无返回,用于在包首次被引用时执行一些初始化的操作。

虽然可以定义多个 init ,甚至同个文件中也可以定义多个,其执行顺序有个复杂的规则,与其记住这些规则,不如尽量避免这样做。

但新的实践中已不推荐这么做,目前的 Go 实现中之所以还保留只是考虑到向后兼容。推荐的做法是显式地去注册和调用你的模块。

外部包的安装

当代码中引入了远端的三方模块时,运行 go get <module_name>go mod tidy 后会自动下载安装,更新本地的 go.mod 文件中的依赖,同时本地还会有个与模块相关的 go.sum 管理着当前模块的版本与其所依赖的文件对应的 hash。

默认会安装模块的最新版本,可在依赖中指定具体的版本。

首先可通过 go list 查看模块有哪些版本:

$ go list -m -versions github.com/shopspring/decimal
github.com/shopspring/decimal v1.0.0 v1.0.1 v1.1.0 v1.2.0

其中:

  • -m 表示列出指定的模块,因为默认情况下 go list 是列出项目的所有依赖
  • -versions 表示列出所有可用版本

安装指定版本:

$ go get github.com/shopspring/decimal@v1.0.0

版本管理

当项目中多个模块同时依赖一个模块,且依赖的版本还不同,比如,

项目中有如下 A,B,C 三个模块,分别依赖模块 D 的三个版本:

  • A → D v1.0.0
  • B → D v1.1.0
  • C → D v1.2.0

Go 的原则是只会引入一个模块 D,选择的版本是最新的 v1.2.0,这就是最小引入原则,这和 npm 不一样,后者会同时引入所有依赖的版本,增加了包大小也引入了一些包管理上的复杂度。

但因为 A,B 依赖的并不是 v1.2.0 版本,编译时出错如何解决。因为语义化版本要求小版本号需要保证向后兼容,所以讲道理,1.x 的版本之间需要互相兼容,要么是包作者去解决这个兼容问题,要么是 D 中降低版本来迁就。

升级依赖

兼容性升级

通过 go get -u <MODULE_NAME> 来升级对应依赖,通过 -u=path 指定升级到 bug fix 版本,即语义化版本中最后一位数字指定的版本。

升级到不兼容的版本

如果模块对外 API 发生不兼容的版本,此时大版本号发生变化,比如 v1.0.0->v2.0.0,在引入时要以 vN 结尾,这里 N 即新的大版本号。因为 Go 要求除了 0 或 1 的大版本号,都以这种形式结尾。即,不兼容的大版本升级,其版本号要体现在 path 中。

import (
	"github.com/shopspring/decimal/v2"
)

大版本中再进行小版本的指定,也是一样加 @

$ go get github.com/shopspring/decimal/v2@2.1.0

Vendoring

默认情况下,依赖是从远端下载安装的。为了保持稳定,也可以一次下载之后存入代码仓库,跟随项目提交。通过 go mode vendor 来完成这一操作。

一旦将依赖 vendor 到本地后,后续依赖有变更,都需要重新执行 go mode venfor 进行更新,否则其他命令比如 build, run 等会报错。

模块的发布

Go 是通过仓库来进行模块管理的,发布模块与别人共享就非常简单了,只需要将代码发布到公共 Git 仓库比如 GitHub, GitLab 等即可。

  • 首先 go mod tidy 进行一下依赖的清理
  • 然后 go test ./... 保证所有测试用例都通过
  • 打处版本 tag git tag v1.0.0
  • 提交 tag 进行发布 git push origin v1.0.0
  • 通过谷歌的代理服务同步你的模块 GOPROXY=proxy.golang.org go list -m example.com/<MODULE_NAME>@v1.0.0

以上。