golang基础-包与依赖管理

介绍

Go语言中支持模块化的开发理念,在Go语言中使用包(package)来支持代码模块化和代码复用。一个包是由一个或多个Go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmtosio等。

定义

我们可以根据自己的需要创建自定义包。一个包可以简单理解为一个存放.go文件的文件夹。该文件夹下面的所有.go文件都要在非注释的第一行添加如下声明,声明该文件归属的包。

package packagename
  • package:声明包的关键字
  • packagename:包名,可以不与文件夹的名称一致,不能包含 - 符号,最好与其实现的功能相对应。

另外需要注意一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下。包名为main的包是应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。

标识符可见性

在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("Hello world!"),就是指调用fmt包中的Println函数。

如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。

例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。

package demo

import "fmt"

// 包级别标识符的可见性

// num 定义一个全局整型变量
// 首字母小写,对外不可见(只能在当前包内使用)
var num = 100

// Mode 定义一个常量
// 首字母大写,对外可见(可在其它包中使用)
const Mode = 1

// person 定义一个代表人的结构体
// 首字母小写,对外不可见(只能在当前包内使用)
type person struct {
	name string
	Age  int
}

// Add 返回两个整数和的函数
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
	return x + y
}

// sayHi 打招呼的函数
// 首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
	var myName = "七米" // 函数局部变量,只能在当前函数内使用
	fmt.Println(myName)
}

同样的规则也适用于结构体,结构体中可导出字段的字段名称必须首字母大写。

type Student struct {
	Name  string // 可在包外访问的方法
	class string // 仅限包内访问的字段
}

包的引入

要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整的引入声明语句格式如下:

import importname "path/to/package"
  • importname:引入的包名,通常都省略。默认值为引入包的包名。

  • path/to/package:引入包的路径名称,必须使用双引号包裹起来。

  • Go语言中禁止循环导入包。

同时引入多个包

import "fmt"
import "net/http"
import "os"

批量引入

import (
    "fmt"
  	"net/http"
    "os"
)

包的别名

当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname指定一个在当前文件中使用的新包名

import f "fmt"

这样在当前这个文件中就可以通过使用f来调用fmt包中的函数了。

f.Println("Hello world!")

匿名引入

如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。

一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。

被匿名引入的包中的init函数将被执行并且仅执行一遍。

import _ "github.com/go-sql-driver/mysql"

匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。

需要注意的是,Go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。

init初始化函数

在每一个Go源文件中,都可以定义任意个如下格式的特殊函数:

func init(){
  // ...
}

这种特殊的函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。

一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的init函数都将被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的init函数再执行当前包中声明的init函数。确保在程序的main函数开始执行时所有的依赖包都已初始化完成。

包初始化函数执行顺序示意图

每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中我们就可以看出包级别变量的初始化会先于init初始化函数。

package main

import "fmt"

var x int8 = 10

const pi = 3.14

func init() {
  fmt.Println("x:", x)
  fmt.Println("pi:", pi)
  sayHi()
}

func sayHi() {
	fmt.Println("Hello World!")
}

func main() {
	fmt.Println("你好,世界!")
}

输出结果:

x: 10
pi: 3.14
Hello World!
你好,世界!

go module

Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。Go module 提供了以下命令供我们使用:

命令介绍
go mod init初始化项目依赖,生成go.mod文件
go mod download根据go.mod文件下载依赖
go mod tidy比对项目文件中引入的依赖与go.mod进行比对
go mod graph输出依赖关系图
go mod edit编辑go.mod文件
go mod vendor将项目的所有依赖导出至vendor目录
go mod verify检验一个依赖包是否被篡改过
go mod why解释为什么需要某个依赖

GOPROXY

GOPROXY 的默认值是:https://proxy.golang.org,direct

国内加速地址https://goproxy.cnhttps://goproxy.io

go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。

GOPRIVATE

设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。

GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:

go env -w GOPRIVATE="git.mycompany.com"

这样在拉取以git.mycompany.com为路径前缀的依赖包时就能正常拉取了。

此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none,允许通内部代理拉取私有仓库的包。

项目创建和依赖相关

初始化项目

我们在本地新建一个名为holiday项目,按如下方式创建一个名为holiday的文件夹并切换到该目录下:

mkdir holiday
cd holiday

holiday 文件夹下使用下面命令初始化项目

go mod init holiday

这个命令会在当前目录下创建一个 go.mod 的文件,内容如下

module holiday

go 1.24.0
  • module holiday:定义当前项目的导入路径
  • go 1.24.1:标识当前项目使用的 Go 版本

go.mod文件会记录项目使用的第三方依赖包信息,包括包名和版本,由于我们的holiday项目目前还没有使用到第三方依赖包,所以go.mod文件暂时还没有记录任何依赖包信息,只有当前项目的一些信息。

接下来,我们在项目目录下新建一个main.go文件,其内容如下:

// holiday/main.go

package main

import "fmt"

func main() {
	fmt.Println("现在是假期时间...")
}

然后,我们的holiday项目现在需要引入一个第三方包github.com/q1mi/hello来实现一些必要的功能。类似这样的场景在我们的日常开发中是很常见的。我们需要先将依赖包下载到本地同时在go.mod中记录依赖信息,然后才能在我们的代码中引入并使用这个包。下载依赖包主要有两种方法。

第一种方法是在项目目录下执行go get命令手动下载依赖的包:

go get -u github.com/q1mi/hello
go get -u github.com/gin-gonic/gin

这样默认会下载最新的发布版本,你也可以指定想要下载指定的版本号的。

go get -u github.com/gin-gonic/gin@v1.9.1

如果依赖包没有发布任何版本则会拉取最新的提交,最终go.mod中的依赖信息会变成类似下面这种由默认v0.0.0的版本号和最新一次commit的时间和hash组成的版本格式

如果想指定下载某个commit对应的代码,可以直接指定commit hash,不过没有必要写出完整的commit hash,一般前7位即可

此时,我们打开go.mod文件就可以看到下载的依赖包及版本信息都已经被记录下来了。

module holiday

go 1.24.0

require (
	github.com/bytedance/sonic v1.13.2 // indirect
	github.com/bytedance/sonic/loader v0.2.4 // indirect
	github.com/cloudwego/base64x v0.1.5 // indirect
	github.com/cloudwego/iasm v0.2.0 // indirect
	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
	github.com/gin-contrib/sse v1.1.0 // indirect
	github.com/gin-gonic/gin v1.9.1 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.26.0 // indirect
	github.com/goccy/go-json v0.10.5 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/q1mi/hello v0.1.1 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.12 // indirect
	golang.org/x/arch v0.17.0 // indirect
	golang.org/x/crypto v0.38.0 // indirect
	golang.org/x/net v0.40.0 // indirect
	golang.org/x/sys v0.33.0 // indirect
	golang.org/x/text v0.25.0 // indirect
	google.golang.org/protobuf v1.36.6 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

导入当前项目包

创建目录 summer 文件是 summer.go

.
├── go.mod
├── go.sum
├── main.go
└── summer
    └── summer.go

内容

package summer

import "fmt"

// Diving 潜水...
func Diving() {
	fmt.Println("夏天去诗巴丹潜水...")
}

此时想要在当前项目目录下的其他包或者main.go中调用这个Diving函数需要如何引入呢?这里以在main.go中演示详细的调用过程为例,在项目内其他包的引入方式类似。

package main

import (
	"fmt"

	"holiday/summer" // 导入当前项目下的包

	"github.com/q1mi/hello" // 导入github上第三方包
)

func main() {
	fmt.Println("现在是假期时间...")
	hello.SayHi()

	summer.Diving()
}

导入本地包

如果你想要导入本地的一个包,并且这个包也没有发布到到其他任何代码仓库,这时候你可以在go.mod文件中使用replace语句将依赖临时替换为本地的代码包。例如在我的电脑上有另外一个名为liwenzhou.com/overtime的项目,它位于holiday项目同级目录下:

准备本地包

mkdir overtime
cd overtime
go mod init overtime

overtime/overtime.go

package overtime

import "fmt"

func Do() {
	fmt.Println("假期结束了...")
}
├── holiday
   ├── go.mod
   ├── go.sum
   ├── main.go
   └── summer
       └── summer.go
└── overtime
    ├── go.mod
    └── overtime.go

由于liwenzhou.com/overtime包只存在于我本地,并不能通过网络获取到这个代码包,这个时候应该如何在holidy项目中引入它呢?

我们可以在holidy/go.mod文件中正常引入liwenzhou.com/overtime包,然后像下面的示例那样使用replace语句将这个依赖替换为使用相对路径表示的本地包。

holidy/go.mod 文件中正常引入liwenzhou.com/overtime

然后使用 replace语句将这个依赖替换为使用相对路径表示的本地包。

require liwenzhou.com/overtime v0.0.0
replace liwenzhou.com/overtime  => ../overtime

holiday/main.go下正常引入并使用overtime

package main

import (
	"fmt"

	"holiday/summer" // 导入当前项目下的包

	"liwenzhou.com/overtime" // 通过replace导入的本地包

	"github.com/q1mi/hello" // 导入github上第三方包
)

func main() {
	fmt.Println("现在是假期时间...")
	hello.SayHi()

	summer.Diving()

	overtime.Do()
}

go.mod文件

go.mod文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式如下:

require module/path v1.2.3

其中:

  • require:声明依赖的关键字
  • module/path:依赖包的引入路径
  • v1.2.3:依赖包的版本号。支持以下几种格式:
    • latest:最新版本
    • v1.0.0:详细版本号
    • commit hash:指定某次commit hash

引入某些没有发布过tag版本标识的依赖包时,go.mod中记录的依赖版本信息就会出现类似v0.0.0-20210218074646-139b0bcd549d的格式,由版本号、commit时间和commit的hash值组成。

go module生成的版本信息组成示意图

go.sum文件

使用go module下载了依赖后,项目目录下还会生成一个go.sum文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum文件内容通常是以类似下面的格式出现。

<module> <version>/go.mod <hash>

或者

<module> <version> <hash>
<module> <version>/go.mod <hash>

不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了go.sum机制来对依赖包进行校验。

依赖保存位置

Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。

mod
├── cache
├── cloud.google.com
├── github.com
    	└──q1mi
          ├── hello@v0.0.0-20210218074646-139b0bcd549d
          ├── hello@v0.1.1
          └── hello@v0.1.0
...

如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令。

发布包

首次发布

打包代码发布到 github.com

创建仓库 hello

git clone https://github.com/xxx/hello
cd hello

初始化

go mod init github.com/xxx/hello

创建文件 hello.go

package hello

import "fmt"

func SayHi() {
	fmt.Println("你好,很高兴认识你。")
}

然后将该项目的代码 push 到仓库的远端分支,这样就对外发布了一个Go包。

其他的开发者可以通过github.com/xxx/hello这个引入路径下载并使用这个包了。

一个设计完善的包应该包含开源许可证及文档等内容,并且我们还应该尽心维护并适时发布适当的版本。

github 上发布版本号使用git tag为代码包打上标签即可。

git tag -a v0.1.0 -m "release version v0.1.0"
git push origin v0.1.0

经过上面的操作我们就发布了一个版本号为v0.1.0的版本。

Go modules中建议使用语义化版本控制,其建议的版本号格式如下:

语义化版本号示意图

  • 主版本号:发布了不兼容的版本迭代时递增(breaking changes)。
  • 次版本号:发布了功能性更新时递增。
  • 修订号:发布了bug修复类更新时递增。

发布新版本

现在我们的hello项目要进行与之前版本不兼容的更新,我们计划让SayHi函数支持向指定人发出问候。更新后的SayHi函数内容如下:

package hello

import "fmt"

// SayHi 向指定人打招呼的函数
func SayHi(name string) {
	fmt.Printf("你好%s,很高兴认识你。\n", name)
}

由于这次改动巨大(修改了函数之前的调用规则),对之前使用该包作为依赖的用户影响巨大。因此我们需要发布一个主版本号递增的v2版本。在这种情况下,我们通常会修改当前包的引入路径,像下面的示例一样为引入路径添加版本后缀。

// hello/go.mod

module github.com/xxx/hello/v2

go 1.24.0

提交代码

git add .
git commit -m "feat: SayHi现在支持给指定人打招呼啦"
git push

打tag

git tag -a v2.0.0 -m "release version v2.0.0"
git push origin v2.0.0

这样在不影响使用旧版本的用户的前提下,我们新的版本也发布出去了。想要使用v2版本的代码包的用户只需按修改后的引入路径下载即可。

go get github.com/xxx/hello/[email protected]

在代码中使用的过程与之前类似,只是需要注意引入路径要添加 v2 版本后缀。

package main

import (
	"fmt"

	"github.com/xxx/hello/v2" // 引入v2版本
)

func main() {
	fmt.Println("现在是假期时间...")

	hello.SayHi("张三") // v2版本的SayHi函数需要传入字符串参数
}

废弃已发布到版本

如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用retract声明废弃的版本。

例如我们在hello/go.mod文件中按如下方式声明即可对外废弃v0.1.2版本。

module github.com/xxx/hello

go 1.24.0


retract v0.1.2

用户使用go get下载v0.1.2版本时就会收到提示,催促其升级到其他版本。

其他补充

fmt

package main

import "fmt"

func main() {
	var f float32
	var g float64
	f = 1.4444444123231
	g = 3.45554633

	fmt.Printf("f=%[1]f, g=%.2[2]f, g=%[2]g,f=%[1]e", f, g)
}