Golang的包管理一直是广大开发者吐槽的点之一。
Go 包管理简史
Golang的包管理分为三个阶段,version < 1.11、 1.11 <= version < 1.13、 version >= 1.13。
version < 1.11
在这个阶段,Golang的包管理存在以下不足:
- 必须设置GOPATH环境变量,且源代码必须存放在GOPATH下
- 拉取外部依赖包时,总是拉取最新的版本,无法指定需要的版本
之所以设置GOPATH环境变量有两个原因:
- 它规定了
go get
命令下载的依赖包的存储位置($GOPATH/src) - 通过设置GOPATH,可以方便Golang计算出import的路径
另外,由于无法指定依赖包的版本,因此容易导致“本地测试OK,但线上部署失败”的问题。这样的问题是广大开发者无法忍受的,所以,各种包管理工具开始涌现出来,典型的有dep,glide等,这里不再赘述。
1.11 <= version < 1.13
这个阶段默认使用的还是GOPATH的管理方式,但是开始支持Go Module
的管理方式。
Go Module解决了上述的阶段存在的不足:
1.它不再需要GOPATH,即你的项目代码可以随意存放
2.它通过go.mod + go.sum解决依赖包的版本问题(后面会讲到)
如果需要迁移到Go Module,需要设置以下环境变量
vim ~/.bash_profile
export GO111MODULE=on
复制代码
version >= 1.13
从这个阶段开始,Golang的包管理默认使用的是Go Module。
使用GOPATH进行包管理
注:为了完整性,这里尝试使用go 1.11复现之前使用GOPATH进行包管理的情况。
1.下拉docker镜像
$ docker pull ubuntu:16.04
$ docker run -itd --name golang-lab ubuntu:16.04 /bin/bash
$ apt-get update && apt-get install wget
复制代码
2.安装go 1.11
$ wget https://dl.google.com/go/go1.11.10.linux-amd64.tar.gz
$ tar -zxvf go1.11.10.linux-amd64.tar.gz
$ go/bin/go version
go version go1.11.10 linux/amd64
复制代码
3.新建项目
3.1 这里我们假定/home/go-projects
为我们的工作区
3.2 新建bin目录用于存放可执行文件; 新建pkg目录用于存放静态链接库文件; 新建src目录用于存放的我们源码文件, 一般我们写的代码都会放到这个目录下。
3.3 git.own.com
名称可自定义,这里只是个人编程习惯,表示这里存放的都是个人项目
$ mkdir /home/go-projects
$ cd /home/go-projects && mkdir src && mkdir pkg && mkdir bin
$ cd src && mkdir git.own.com && cd git.own.com
$ mkdir gopath-lab && cd gopath-lab && touch main.go
复制代码
4.目录树
root@ebca4ae962aa:/home/go-projects# tree -L 4
.
|-- bin
|-- pkg
`-- src
`-- git.own.com
`-- gopath-lab
`-- main.go
复制代码
5.设置环境变量
- GOPATH:工作区路径,存放源代码。
- GOBIN:当使用go install xx.go 时, 生成的可执行文件就会放在此目录
- GOROOT:Go的安装位置,用于寻找标准库,这里是/home/go
$ vim ~/.bashrc
export PATH=$PATH:/home/go/bin
export GOPATH=/home/go-projects
export GOBIN=/home/go-projects/bin
export GOROOT=/home/go
复制代码
如果没有设置GOBIN,会报错
$ go install main.go
go install: no install location for .go files listed on command line (GOBIN not set)
复制代码
6.main.go 代码如下:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
复制代码
可以看到,直接go run
并不能自动下载依赖
$ go run main.go
main.go:3:8: cannot find package "github.com/gin-gonic/gin" in any of:
/home/go/src/github.com/gin-gonic/gin (from $GOROOT)
/home/go-projects/src/github.com/gin-gonic/gin (from $GOPATH)
复制代码
7.手动下载并测试
# 居然奇迹般下载成功了,一般这个时候需要设置代理
$ go get -v github.com/gin-gonic/gin
# 可以看到,源码已经下载到src目录了
$ ls /home/go-projects/src/
git.own.com github.com golang.org gopkg.in
# 再次执行,运行成功
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
复制代码
使用Go Module进行包管理
本节翻译自《Using Go Modules》
Module 是一系列依赖包的集合,通过go mod init xxx
可初始化一份空的go.mod和go.sum,这两份文件存放于项目的根路径下。
对于go.mod,它不仅存储了这些依赖包的路径及其版本,同时也指定了import的根路径,对于go.sum,它存放了依赖包内容的预期校验和,保证前一次下载的代码和现在下载的代码是一致的。
配置代理
由于Golang大部分依赖包都在国外,直接下载非常缓慢,在没有Go Module的时候,需要自己配置代理,比如socks;但是有了Go Module,就可通过设置环境变量来配置代理了,具体参考:goproxy.io/zh/。
配置时有几个注意点:
1.如果你有私有仓库和公共仓库,则需要加上direct
参数,并配置GOPRIVATE
(针对Go1.13)
# 有了direct,GOPRIVATE指定的仓库不会使用代理
go env -w GOPROXY=https://goproxy.io,direct
# 设置不走代理的私有仓库,多个用逗号相隔
go env -w GOPRIVATE=*.corp.example.com
复制代码
2.如果你使用的是Golang IDE,则注意该IDE也要配置
3.如果你的~/.bash_profile或~./bashrc 文件存在GO111MODULE等环境变量,则go env 写入时会冲突
warning: go env -w GOPROXY=... does not override conflicting OS environment variable
初始化项目
1.新建文件夹
mkdir go-module-lab && cd go-module-lab
2.初始化Go Module项目,git.own.com/go-module是自定义的
go mod init git.own.com/go-module
3.查看go.mod
module git.own.com/go-module
go 1.13
复制代码
添加代码测试
1.自定义库
mkdir hello && touch hello/hello.go
hello.go 内容
package hello
func Hello() string {
return "Hello, world."
}
复制代码
2.新建main.go测试,内容如下
package main
import (
"fmt"
// 前面提过,go.mod 指定了import时的根路径
"git.own.com/go-module/hello"
)
func main() {
fmt.Println(hello.Hello())
}
复制代码
添加外部依赖
1.更新hello.go文件,引入rsc.io/quote
依赖
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
复制代码
2.执行go run main.go
,会自动下载依赖
➜ go-module-lab go run main.go
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
Hello, world.
复制代码
3.查看go.mod
module git.own.com/go-module
go 1.13
require rsc.io/quote v1.5.2
复制代码
可以看到,使用Go Module的包管理方式,Golang会自动帮我们处理包的依赖关系,并把缺失的包添加到go.mod,并使用rsc.io/quote
的最新版本。(这里的最新版本应理解为最新并打了tag的版本,如果没有打tag,则会使用一种pseudo-version
的方式标识,下文会说到)
4.借助go list命令查看所有依赖
$ go list -m all
git.own.com/go-module
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
复制代码
补充:
pseudo-versions(伪版本)
一般情况下,go.mod使用语义化版本
来标志依赖包的版本号,比如v1.0.0、v1.0.1。
它包含三个部分:
- 主版本号:当你做了不兼容的 API 修改,比如v1.5.2的1
- 次版本号:当你做了向下兼容的功能性新增,比如v1.5.2的5
- 修订号:当你做了向下兼容的问题修正,比如1.5.2的2
语义化版本规定,同一个主版本号的必须向下兼容,比如v1.5.2必须向下兼容v1.1.0;如果代码不兼容,则必须使用新的版本号。
但是语义化版本是基于项目有打tag的情况下,如果一些项目没有打tag,则Golang会使用一种pseudo-version
来标识,类似v0.0.0-yyyymmddhhmmss-abcdefabcdef
的形式。
其中,yyyymmddhhmmss使用的是UTC时间,abcdefabcdef对应的是你这次commit的哈希值(前12位),
对于前缀v0.0.0,则有三种情况:
1.当你的项目一个tag都没有的时候,形式为v0.0.0-yyyymmddhhmmss-abcdefabcdef
2.当你项目最近打的tag的名称为vX.Y.Z-pre的时候,形式为vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
3.当你的项目最近打的tag的名称是vX.Y.Z的时候,形式为vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
go.sum
之所以有go.sum文件,是因为单纯地通过语义化版本(v1.5.2)无法确定每次通过v1.5.2标签下载的都是同一份代码。
比如发布者在 GitHub 上给自己的项目打上 v1.5.2 的tag之后,依旧可以删掉这个tag ,提交不同的内容后再重新打个 1.5.2 的 tag。
为了确定是否是同一份代码,go.sum存放了特定模块版本的内容的预期校验和,如果该代码有改动,则预期校验和不匹配,就会导致编译错误。
verifying xxx/base@v1.3.0: checksum mismatch
downloaded: h1:T2eK+D0jzzeu4+S+oP9KvGgovPnl4FjxYShqdNSPrjc=
go.sum: h1:Crwm2FliMjZ3BABjnydOpoJiFPaKcod/zYNOtcB9Xkw=
复制代码
更新外部依赖
更新次版本号
更新次版本号比较简单,直接使用go get即可,比如更新golang.org/x/text
go get golang.org/x/text
通过查看go.mod的变化,我们可以看到golang.org/x/text
的版本号由v0.0.0-20170915032832-14c0d48ead0c升级到v0.3.2。(indirect表明该依赖包在源码中没有用到,是间接依赖的)
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote v1.5.2
)
复制代码
除此之外,我们还可以更新到特定版本,在此之前,我们先看看该模块有哪些可用版本(以rsc.io/quote为例)
$ go list -m -versions rsc.io/quote
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1
复制代码
更新到特定版本:
go get rsc.io/quote@v1.4.0
如果想要使用特定的分支,只需要把版本号换成分支名即可(如果分支名包含特定符号,如"/",可用双引号将分支名括起来):
go get rsc.io/quote@dev
更新主版本号
如果需要更新主版本号,需要在代码中手动指定,因为不同主版本号相当于一个新的依赖(库)。
1.添加新函数
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
复制代码
2.自动下载依赖
package main
import (
"fmt"
"git.own.com/go-module/hello"
)
func main() {
fmt.Println(hello.Hello())
fmt.Println("proverb", hello.Proverb())
}
复制代码
3.查看go.mod
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
复制代码
从上面可以看出,Go Module每一个主版本号使用不同的路径表示,如v1,v2,v3;另外,Golang允许同时存在多个主版本号,因为路径不同,相当于是一个新的库,这样做的目的是保持增量迁移。
比如我一开始使用rsc.io/quote
,后面有改动,且与之前不兼容,这是我就可以使用新的主版本号,比如rsc.io/quote/v3
,但是Hello这个函数暂时还不能迁移到V3版本,这是多版本的作用就凸显出来了
删除多余依赖
当过了一段时间,我们已经把把rsc.io/quote
的代码全部迁移到新版本rsc.io/quote/v3
, 类似下面的代码
package hello
import (
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quoteV3.HelloV3()
}
func Proverb() string {
return quoteV3.Concurrency()
}
复制代码
这时之前的go.mod里面的rsc.io/quote
是多余的,我们可以通过go mod tidy
删除多余的rsc.io/quote
$ go mod tidy
$ cat go.mod
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
复制代码
总结
1.go mod init: 初始化一个Go Module项目,同时生成go.mod和go.sum文件
2.go build/go test/go run: 会自动下载依赖,并更新go.mod和go.sum文件
3.go list -m all:打印目前的所有依赖包
4.go get:手动下载依赖包,或者更改依赖包版本
5.go mod tidy:增加缺失的依赖,删除没有用到的依赖
其他命令
go env
配置一些环境变量。
# 环境变量说明文档
go help environment
# 环境变量配置文件路径
$ go env GOENV
/Users/xxx/Library/Application Support/go/env
# 列出所有环境变量
go env
# 列出所有环境变量(以json格式)
go env -json
# 修改某个环境变量
go env -w GOPROXY=https://goproxy.io,direct
# 重置某个变量
go env -u GOPROXY
复制代码
推荐阅读
Go语言包管理简史
初窥Go module
Go modules:版本是如何选择的?
谈谈go.sum
有疑问加站长微信联系(非本文作者)