概述

Golang发展至今,其依赖管理方式有了几次变革。本文对Golang的不同依赖管理方式进行总结。

  • GOPATH (最早的依赖管理方式)
  • Go vendor (v1.5)
  • Go Modules (v1.11至今,默认开启,适用于生产环境)

GOPATH (了解即可)

最早Go通过GOPATH环境变量进行依赖管理。GOPATH是一个环境变量,存储着Go的工作目录(项目目录)。在该目录下,通常包含srcpkgbin三个子目录,其中src存放项目源代码,pkg存放编译后的包对象文件,bin存放编译后生成的可执行文件。

Go 编译器在编译项目时,会在 GOPATH 下的src目录及其子目录中查找所需的依赖包。当多个 Go 项目需要使用相同的代码包时,可以将这些代码包统一放在 GOPATH 下的src目录中,实现代码的共享和复用。GOPATH模式下,执行go get命令下载依赖也是将依赖保存在GOPATH/src下。

如果GOPATH存在多个路径地址,go get会将依赖下载到第一个路径的src目录中

所以GOPATH模式下的依赖管理的缺点很明显:

  • go get会将依赖下载到src目录下,本地代码与第三方代码混合管理,使项目文件混乱
  • 当GOPATH使用多个路径地址管理不同项目,即使依赖相同的包也得分开保存在各自的src目录中,造成磁盘浪费

Go vendor (Gov1.5-v1.10,了解即可)

在Go v1.5时,Golang引入了vendor模式,即将项目的所有外部依赖包保存在项目的vendor目录中。Go v1.5-v1.10,即还没GOMODULE模式时,通常会使用第三方工具 govendor 来管理 vendor 目录。如:

1
2
3
4
5
# 初始化项目根目录下的vendor目录
govendor init

# 为项目添加依赖
govendor add github.com/example/package

想启用 vendor 模式,需要设置 GO15VENDOREXPERIMENT 环境变量为 1(较新版本的Go中已经不存在该变量)。然后使用正常的 go build 命令进行构建,此时 Go 编译器会优先从 vendor 目录中查找依赖包而不是 GOPATH/src 中。

新版本Go中在Go Modules模式下,打包程序时想加入vendor目录中的依赖,只需要使用命令go build -mod=vendor即可

Go Module

在Gov1.11时,Golang引入了Go Module模式:

  • go get下载的依赖统一保存在GOPATH/pkg/mod目录下
  • 在项目的根目录使用go.mod文件管理项目的依赖以及其版本
  • 在项目的根目录使用go.sum文件保存每个依赖包的哈希值(包括依赖包的直接依赖和间接依赖),用于验证从远程仓库下载的依赖包是否与之前使用的是同一版本

Go Modules是目前Go使用的最广泛的依赖管理方式。作用有:

  • 版本控制与依赖管理
    • 精确版本控制:Go Module 可以明确指定项目所依赖的各个模块的版本号,确保项目在不同环境下使用的是相同版本的依赖
    • 依赖自动解析:在构建项目时,Go Module 会自动解析项目的依赖关系,并下载所需的依赖模块及其所有间接依赖
    • 避免依赖冲突:当多个依赖模块依赖于同一个模块的不同版本时,Go Module 会尝试解决版本冲突,选择一个能够满足所有依赖的版本。
  • 项目隔离与可重复性
    • 环境隔离:Go Module 为每个项目提供了独立的依赖环境,使得项目的构建不依赖于全局的GOPATH设置。
    • 可重复性构建:由于 Go Module 精确地记录了项目的依赖版本,只要go.mod文件和相关的依赖文件不变,项目在任何环境下都可以进行相同的构建,保证了项目构建的可重复性
  • 模块发布与共享
    • Go Module 简化了模块的发布过程。开发者可以将自己编写的模块发布到公共的模块仓库(如proxy.golang.org),供其他开发者使用。发布时,只需要按照 Go Module 的规范设置好模块的版本号和依赖关系,就可以轻松地将模块分享给其他人。

Go Module开启设置

Golang通过环境变量GO111MODULE来控制Go Module模式的开启(默认开启),总共有三个可选的值:

  • auto: go自主根据当前项目目录进行判断是否使用Go Module模式。当目录满足以下条件时使用Go Module模式:
    • 不在GOPATH/src
    • 当前或上一级目录中存在go.mod文件
  • on:开启Go Module
  • off:使用GOPATH模式

go mod 命令

Golang提供了一系列go mod命令进行依赖管理,常用的有:

命令 作用
go mod init <module_name> 在当前目录下生成go.mod文件,并设置包名为
go mod tidy 会扫描当前项目所有源文件中导入语句,更新go.mod和go.sum:添加未声明的依赖,移除未使用的依赖
go mod download 根据项目的 go.mod 文件中列出的依赖信息,将所需的依赖包及其相应版本下载到本地的模块缓存(GOPATH/pkg/mod)中
go mod verify 校验模块是否被篡改过
go mod vendor 将依赖的模块保存在项目的vendor目录下
go mod why <module_name> 输出项目中引入某个包的原因
go mod graph 输出项目中的依赖列表

go.mod格式

go.mod文件的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 双斜杠表示注释
// module <name> 指明当前模块名,不使用双引号
module github.com/DopamineNone/test

// Golang SDK版本
go 1.23.2

// 声明项目所依赖的外部包及其相应的版本信息
require (
github.com/example/package v1.2.3
github.com/another/package v2.1.0
)

// 允许你排除特定版本的依赖包,防止它们被引入到项目中
exclude (
github.com/example/package v1.5.0
)

// 替换依赖的源路径或版本,常见于代理情况
replace (
github.com/example/package v1.2.3 => github.com/myfork/package v1.2.4
)

// 当项目作为模块被引用时,用于标记本模块不应该被使用的版本,即使它们可能已经在 require 部分列出
retract (
v1.0.0 // 有 bug,不建议使用
v1.1.0-rc.1 // 预发布版本,不稳定
)

外部包引用规则

引用前需要使用go get命令将外部包保存在本地缓存中。

无论是哪种模式下,直接import "包名"就可以实现外部包的引用。如:

  • import "github.com/example/package"

本地包引用规则

  1. Golang 不允许在同一个目录下有两个不同的包(子目录除外)
  2. Golang 建议包名与包所在的目录名一致
    如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .
    |-demo
    | |-foo
    | | |-foo.go // package foo
    | |-bar.go //package demo
    | |-baz.go //package demo
    |-main.go
    |-go.mod // module test
    ...

上述目录结构是没有错误的;如果baz.go的包名改成demo2,就会产生编译冲突,因为 Go 编译器会将一个目录下的所有 .go 文件作为一个整体进行编译和链接操作,bar.go和baz.go的包名不同就会造成编译冲突。

在不同依赖管理模式下,包的引用方式也略有不同:

  • GOPATH模式下,可以通过GOPATH/src的相对路径导入本地包(了解即可)
  • Go Module模式下,通过模块名+目录路径导入本地包(重点)

以Go Module模式为例,在main.go中可以通过import "test/demo"引用demo包(bar.gobaz.go),通过import "test/demo/foo来引用foo包(foo.go

以下是一个正确的本地包引入例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// demo/bar.go
package demo

import "fmt"

func Bar() {
fmt.Println("Hello, Bar!")
}
...

// main.go
package main

import "test/demo" // 模块名test + 模块下的目录路径/demo

func main() {
demo.Bar()
}

Golang 允许包名与目录名不一致,但可能会导致代码混淆,因为导入语句使用到的是目录名,而引用该包时使用的是包名,以下是一个正确的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// demo/bar.go
package bar

import "fmt"

func Bar() {
fmt.Println("Hello, Bar!")
}
...

// main.go
package main

import "test/demo" // 使用的demo是目录名

func main() {
bar.Bar() // 引用的bar是包名
// demo.Bar() 是错误的调用
}

有趣的是,笔者在写这篇博客时发现,即使包名与目录名不一致,也可以使用别名导入的方式避免混淆(当然保持包名与目录名一致仍是最佳实践),以下是一个正确的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// demo/bar.go
package bar

import "fmt"

func Bar() {
fmt.Println("Hello, Bar!")
}
...

// main.go
package main

import demo "test/demo" // 对demo目录下的bar包使用别名`demo`

func main() {
demo.Bar() // 引用的demo是bar包的别名
bar.Bar() // 通过原包名调用也是正确的(尽管别名存在),但既然定义了别名就尽量使用别名引用
}