-
Notifications
You must be signed in to change notification settings - Fork 4
Description
整理这篇内容时go的最新版本是1.12.7
。
Go的代码组织方式和代码共享方式对新手来说不太友好,其中很大一个原因是因为Go缺乏成熟的包版本化管理机制和工具。这篇内容尝试整理清楚Go代码的组织方式及其发展脉络,并解释GOPATH
,packages
,importpath
,modules
等概念, 从而使初接触Go语言的开发者对Go的代码组织机制有一个相对清晰和系统的认识。
这篇内容主要整理自Go帮助文档go help
,Modules wiki以及Russ Cox关于go package版本化的系列blog。
概述
发展到目前(1.12.7),go主要有两种代码组织方式:
- 基于
GOPATH
的方式。 - 基于
modules
的方式。
基于GOPATH
的方式是目前的主流方式,但是go在版本化、依赖管理等方面的功能缺失使其在代码组织和代码依赖管理等方面面临很多棘手的问题,有很多第三方工具尝试解决这些问题,如govendor
等。
Go 1.11
引入了modules
的概念,提供了初步的版本化和依赖管理能力, modules
的目标是最终要取代GOPATH
。
modules
模式将在Go 1.3中成为默认模式。
这篇内容中的示例在macOS
中完成,Go版本为1.12.7
。
基于GOPATH的方式
GOPATH环境变量
进行Go开发要求首先设置环境变量GOROOT
和GOPATH
。
GOROOT
: Go的安装目录,如果使用Homebrew
等包管理工具安装,会自动设置GOROOT
环境变量。GOPATH
: 值为一个或多个目录路径,用来解析import
语句来定位对应包。在*nix中,多个值使用冒号分隔,默认值是$HOME/go
; windows中,多个值使用分号分隔,默认值是%USERPROFILE%\go
。
可以使用go env GOROOT
、go env GOPATH
查看。
目录结构
Go项目代码通常放在GOPATH
目录下。GOPATH
包含三个子目录:
src
: 存放Go代码文件,src下的子目录决定了每个包的import path
和可执行文件的名字。pkg
: 存放编译好的包文件。bin
: 存放编译好的可执行文件。
go命令会自动创建pkg
和bin
目录。
一个GOPATH
目录称为一个workspace。一个典型的go代码组织方式如下:
- 在一个workspace中维护所有的Go代码。
- 一个workspace中包含一个或多个版本控制仓库(如git仓库)。
- 每个版本控制仓库包含一个或多个packages。
- 每个package就是一个目录,包含一个或多个Go源文件。
从一个简单的例子开始,这个例子来自How to Write Go Code。
开发hello world程序。
首先创建程序目录,假设我们在github上有一个名为user
的账号,我们希望hello world程序托管在这个账号下,那么新建目录并初始化仓库:
mkdir -p $GOPATH/src/github.com/user/hello
cd $GOPATH/src/github.com/user/hello
git init
之所以目录是github.com/user/xxx
的形式,有两个原因:
- 防止
import path
冲突, - 方便共享代码,和
go get
的工作方式有关,后面会有说明
新建GOPATH/src/github.com/user/hello/hellworld.go
并编写代码:
package main
import (
"fmt"
)
func main () {
fmt.Println("hello world")
}
此时workspace的目录结构如下:
.
└── src
└── github.com
└── user
└── hello
└── hellworld.go
在任意路径执行go run github.com/user/hello
,会看到输出hello world
。
执行go install github.com/user/hello
,hello会被安装到GOPATH/bin
,可执行程序的名字是包所在目录最后一级的名字。
.
├── bin
│ └── hello
└── src
└── github.com
└── user
└── hello
└── hellworld.go
如果设置了GOBIN
环境变量,可执行程序会安装在GOBIN
指定的目录。
开发库
开发一个操作string的工具包,包名stringutil
,提供一个反转字符串的函数Reserse
。
创建目录并初始化git仓库:
mkdir -p $GOPATH/src/github.com/user/stringutil
cd $GOPATH/src/github.com/user/stringutil
git init
新建文件GOPATH/src/github.com/user/stringutil/reverse.go
package stringutil
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
在helloworld.go中使用Reverse函数:
// hello.go
package main
import (
"fmt"
"github.com/user/stringutil"
)
func main () {
fmt.Println(stringutil.Reverse("hello world"))
}
此时workspace的目录结构如下:
.
├── bin
│ └── hello
└── src
└── github.com
└── user
├── hello
│ └── hellworld.go
└── stringutil
└── reverse.go
package name
Go文件中第一行代码用来定义package name,同一目录里所有Go文件的package name必须相同。
package stringutil
package name和目录名称没有关系,但按照惯例最好和最后一层目录的名称相同, 即import path
的最后一个元素。
特殊目录名
go命令会忽略testdata
目录,以及以.
或_
开头的目录。
import path
import path路径解析
import path是packages的导入路径,go使用import path在文件系统中定位package,如helloworld.go中:
import (
"fmt"
"github.com/user/stringutil"
)
go会依次使用GOROOT/src/<import-path>
和GOPATH/src/<import-path>
查找package。
对于fmt
, go首先使用GOROOT/src/fmt
定位查找,GOROOT/src
下有fmt
包,查找结束。
GOROOT/src/
├── fmt
│ ├── doc.go
│ ├── example_test.go
│ ├── export_test.go
│ ├── fmt_test.go
│ ├── format.go
│ ├── gostringer_example_test.go
│ ├── print.go
│ ├── scan.go
│ ├── scan_test.go
│ ├── stringer_example_test.go
│ └── stringer_test.go
对于github.com/user/stringutil
,go使用GOROOT/src/github.com/user/stringutil
查找,查找失败,然后使用GOPATH/src/github.com/user/stringutil
查找,成功定位到该package。
可以通过错误信息查看package的查找路径,将stringutil
改为stringutils
,然后执行go run github.com/user/hello
。
还有一个特殊的包查找路径:vendor
目录,后面会介绍。
相对路径
有两种情况下可以使用相对路径(以./
或../
开头的路径)。 其一,在go命令中可以使用相对路径,如:
go run ./helloworld.go
其二,如果代码不在workspace中,那么import语句可以使用相对路径,如:
import "../stringutils"
这可以用来快速验证某些功能。换言之,在workspace中代码的import语句不能使用相对路径。
通配符
通配符...
可以匹配任意字符串,包括空字符串,但是不匹配vendor
目录,这和vendor
目录的作用有关系,后文会说明。 例如net/...
匹配目录net
和其下级目录,如net/http
,./...
不匹配./vendor
, ./mycode/vendor
等。
保留的path
有4个保留的path: main
, all
, std
, cmd
。
main
: 可独立执行文件的顶层包。all
: GOPATH中的全部package。使用modules
方式时,all
指main module以及依赖module所包含的全部package。std
: Go标准库里的全部package。cmd
: Go仓库的命令和内部库。
~ go list all
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
cmd/asm
...
~ go list std
archive/tar
archive/zip
bufio
bytes
compress/bzip2
compress/flate
...
~ go list cmd
cmd/addr2line
cmd/api
cmd/asm
cmd/asm/internal/arch
cmd/asm/internal/asm
...
internal目录
最初,Go并没有提供代码的可见性控制,所有包都可以被其他包import。但是在开发中,尤其是代码共享时,代码的可见性控制变得尤为重要。我们开发的一些包总有一些代码是私有的,不希望被包的使用者直接使用,比如各种helper等,虽然可以在API文档中声明哪些是public的,但是根据海勒姆法则,那些私有实现总会被一些使用者依赖。为解决这个问题,Go 1.4中引入了internal
目录。
internal
目录及其下级目录中的packages只对internal目录的父目录以及父目录的下级目录可见。。
假设有如下代码目录:
./src
├── foo
│ ├── f.go
│ ├── fooa
│ │ ├── bar
│ │ │ ├── bara
│ │ │ │ └── a.go
│ │ │ └── x.go
│ │ ├── internal
│ │ │ └── baz
│ │ │ └── z.go
│ │ ├── quux
│ │ │ └── y.go
│ │ └── t.go
│ └── foob
│ └── s.go
包baz的import path是foo/fooa/internal/baz
,其对a.go
, x.go
, y.go
, t.go
可见; 对f.go
, s.go
不可见, 在f.go
, s.go
中import "foo/fooa/internal/baz"
会报错。
go get命令
我们在开发稍微复杂的应用时大概率会使用第三方的开源库/包。代码共享变得越来越流行,也越来越重要。但是最初Go并没有提供相应的能力。一直到现在Go也没有类似npm(Node.js)的中心化的代码共享机制。
一些历史
2009年11月,Go发布时只有一个编译器(compiler),一个链接器(linker)以及一些代码库,开发者必须使用6g
和6l
来构建应用,Go提供了一些样板makefile。这时并没有代码共享的方法,除了手动拷贝代码。
2010年2月,Go提议了一个工具goinstall
,它是一个零配置工具,提供从Github等代码托管服务下载、安装包等功能。
2011年12,Go 1发布,并内置了go
命令,go命令提供了编译、安装、运行等功能,同时go get
替代了goinstall
。go get
可以从代码托管服务下载、安装包。
go get的工作方式
go get
提供了对知名代码托管网站的支持:
Bitbucket (Git, Mercurial)
import "bitbucket.org/user/project"
import "bitbucket.org/user/project/sub/directory"
GitHub (Git)
import "github.com/user/project"
import "github.com/user/project/sub/directory"
Launchpad (Bazaar)
import "launchpad.net/project"
import "launchpad.net/project/series"
import "launchpad.net/project/series/sub/directory"
import "launchpad.net/~user/project/branch"
import "launchpad.net/~user/project/branch/sub/directory"
IBM DevOps Services (Git)
import "hub.jazz.net/git/user/project"
import "hub.jazz.net/git/user/project/sub/directory"
执行go get <import-path>
会将代码下载(克隆,如使用git clone)到GOPATH/src/<import-path>
,go会自动创建相应的目录。如果GOPATH
设置了多个目录,那么代码会下载到第一个目录。
Golang在Github上的代码仓库github.com/golang/example
也提供了stringutil
包,我们可以下载并在helloworld.go
中使用。执行如下命令下载包:
go get github.com/golang/example/stringutil
此时workspace的目录结构:
GOPATH/src/
└── github.com
├── golang
│ └── example
│ └── stringutil
│ ├── reverse.go
│ └── reverse_test.go
└── user
├── hello
│ └── hellworld.go
└── stringutil
└── reverse.go
修改hellworld.go
package main
import (
"fmt"
"github.com/golang/example/stringutil"
)
func main () {
fmt.Println(stringutil.Reverse("hello world golang"))
}
执行go run github.com/user/hello
, 输出gnalog dlrow olleh
。
这也解释了为什么我们创建hello world程序时按照github.com/user/xxx
的形式创建组织目录,这样我们可以在Github发布包,从而方便其他开发者使用。
除了这些知名代码托管网站,还有一些非知名代码托管网站,企业、组织内部一般也都有私有的代码版本控制服务,go get
对这些服务也提供了支持。
支持方式如下。
import path的格式为:
repository.vcs/path
.vcs
是版本控制系统的标识,支持的版本控制系统及其对应的标识如下:
Bazaar .bzr
Fossil .fossil
Git .git
Mercurial .hg
Subversion .svn
例如,
go get git.code.example.com/tarsgo/tars.git
下载后的目录是GOPATH/src/git.code.example.com/tarsgo/tars.git
,所以import语句为import "git.code.example.com/tarsgo/tars.git"
。
.vcs
是可选的,如果没有指定.vcs
,go get
会发起https/http请求,例如
go get git.code.example.com/tarsgo/tars
对应发起的https/http请求是:
https://git.code.example.com/tarsgo/tars?go-get=1 (优先请求)
http://git.code.example.com/tarsgo/tars?go-get=1 (go get需要指定 -insecure)
请求的响应是需要包含go-import
meta的html文档,go通过此meta解析代码的位置等信息。go-import
meta的完整格式是:
<meta name="go-import" content="import-prefix vcs repo-root">
vcs可取的值为bzr
, fossil
, git
, hg
, svn
。
import-prefix是相对于代码仓库根目录的import path。它必须是go get <import-path>
命令中的<import-path>
或其前缀。如果import-prefix是<import-path>
的前缀,go会向https://<import-prefix>?go-get=1
发起请求来验证meta标签。
例如,
import "example.org/pkg/foo"
对应发起的https/http请求是:
https://example.org/pkg/foo?go-get=1 (优先请求)
http://example.org/pkg/foo?go-get=1 (go get需要指定 -insecure)
假设返回的html包含如下meta标签:
<meta name="go-import" content="example.org git https://code.org/r/p/exproj">
由于example.org
是import path example.org/pkg/foo
的前缀, 而不是完整的import path, go会发起验证请求https://example.org/?go-get=1
,如果此页面含有相同的meta标签,则验证通过,go会使用git clone https://code.org/r/p/exproj
将代码clone到GOPATH/src/example.org
中。当然,代码只会下载到GOPATH
环境变量设置的第一个目录中。
基于meta标签的方式有几个优势:
1、可以为package设置canonical import path。import path不依赖代码托管网站地址,方便代码迁移。比如开始代码托管在google code,但是google code要关闭了,可以很方便的将代码迁移到Github,而这个过程对包的使用者是无感知的。
2、可以将分布在不同托管网站的库/包聚合到同一import path下。
3、可以使用使用更简洁,表意更清晰的import path。
gopkg.in
前面提到Go缺乏直接的包版本化能力,我们可以基于go-import meta标签来进行代码的版本化。
2014年三月,Gustavo Niemeyer创建了gopkg.in,宣传口号是"stable APIs for the Go language."。我们可以import gopkg.in上不同版本的包:
import gopkg.in/yaml.v1
import gopkg.in/yaml.v2
其原理就是使用go-import meta标签统一聚合import path,并将clone操作重定向到Github上代码仓库不同的tag或者commit上。可以将gopkg.in看作是到Github的重定向器。
https://gopkg.in/yaml.v1?go-get=1的响应页面:
import comments
如果通过go-import meta标签指定的代码仓库地址和import-prefix不一致,那么使用go get
下载代码时,根据是否指定.vcs
就会产生两种不同的目录结构,即两种不同的import path。
前面例子中example.org/pkg/foo
,如果meta标签指定的代码存放位置是Github上的仓库github.com/example.org/foo
,那我们也可以直接go get github.com/example.org/foo
得到代码,此时的import path是github.com/example.org/foo
。这种情况通常是包作者不想看到的,尤其是如果此代码仓库包含了多个包,而一些包按example.org/xxx
格式import了仓库中的另一些包,此时go get github.com/example.org/foo
的目录结构和import语句中的import path不符,会产生编译错误。
可以使用import comments解决此类问题。import comments是紧跟在package name
之后的注释,和package name
在同一行。
package math // import "path"
package math /* import "path" */
包作者可以通过import comments来指定包被import时希望的import path,go命令工具会拒绝包以其他import path被使用。
import path comments对vender目录下的代码无效。
使用modules方式时import path comments也无效。
vendor目录
go get
提供了从vcs下载、安装包的能力,但是其最大的一个缺点是缺乏包版本化的能力。这会带来很严重的问题,当我们依赖的包有更新时,我们很难确定引入的是非兼容更新、新增API还是仅仅是补丁更新; 同时可重复的构建(reproducible build)变得困难。试想团队中加入了新成员,它clone代码到自己的机器上,执行go get
下载依赖包,然后执行go run
, go build
, go install
等命令运行、构建、安装程序。相对于其他成员机器上的依赖包,它go get
下来的依赖包很可能是作者更新过的,所以可能和其他成员构建出不同的结果,甚至构建错误,如果使用CI/CD工具这个问题会变的更严重。
我们可以把依赖包拷贝到项目的代码仓库里并作为工程源代码的一部分来缓解这个问题。2013年11月,Go 1.2的FAQ里添加了如下类似的建议:
If you're using an externally supplied package and worry that it might change in unexpected ways, the simplest solution is to copy it to your local repository. (This is the approach Google takes internally.) Store the copy under a new import path that identifies it as a local copy. For example, you might copy "original.com/pkg" to "you.com/external/original.com/pkg". Keith Rarick's goven is one tool to help automate this process.
但是这样做也会带来新的问题:依赖包的更新变的困难;同时需要修改原包的import path。
为此,Go 1.5实验性的引入了vendor
目录,并在Go 1.6正式发布。
vendor
目录下package的可见性和internal
相同,即只对vendor目录的父目录以及父目录的下级目录可见。vendor
目录下包的import path需要省略vendor目录以及vendor的上级目录,这也是提供vendor目录功能的一个原因:避免修改依赖包的import path。
前面介绍import path解析时提到过,vendor是除了GOROOT
和GOPATH
外的另一个包查找路径,vendor目录的查找优先级高于GOROOT
和GOPATH
。构建时包查找的完整顺序如下:
- 当前包目录下的
vendor
目录 - 查找上级目录下的
vendor
目录,直到GOPATH/src
下的vendor
目录。 GOROOT/src
GOPATH/src
例如有如下文件目录结构:
GOPATH/src/
├── crash
│ └── bang
│ └── m.go
├── foo
│ ├── f.go
│ ├── fooa
│ │ ├── bar
│ │ │ ├── bara
│ │ │ │ └── a.go
│ │ │ └── x.go
│ │ ├── t.go
│ │ └── vendor
│ │ └── crash
│ │ └── bang
│ │ └── z.go
│ └── foob
│ └── s.go
z.go
所在package的import path是crash/bang
,而不是foo/fooa/vendor/crash/bang
。在a.go
, x.go
, t.go
中import "crash/bang"
解析到的是foo/fooa/vendor/crash/bang
;在f.go
, s.go
中import "crash/bang"
解析到的是GOPATH/src/crash/bang
。
建议一个应用中最多只包含一个vendor目录,并且作为代码仓库的一级子目录。
基于vendor
目录机制,有一些第三方工具被开发出来,用于提供依赖管理功能,如glide
, govendor
等,这里有一个对比列表。
虽然可以使用vendor
目录机制做初步的依赖管理,但是这终究不是一个“合理”的方式。我们在做Node.js开发时很难想象需要将node_modules
目录和项目代码一起提交到vcs做版本控制。
Go并没有关于项目目录结构布局的强性规定,也没有推荐的最佳实践,社区有一些提案,比如Standard Go Project Layout。
以上介绍了基于GOPATH
的一些机制,了解它们会对我们进行Go项目的目录规划起到一些帮助。
基于modules的方式
Go 1.11引入了modules
,提供了初步的版本化和依赖管理能力, modules
的目标是最终要取代GOPATH
。
这部分介绍modules和基于modules的方式要解决的主要问题,以及是如何解决的。
引入modules主要为了解决以下问题:
- 解决Go生态碎片化问题,消除对
bzr
,git
等vcs的依赖。 - 提供同一代码仓库发布不同版本的能力。
- 方便企业、组织搭建私有的包共享仓库。
- 为未来构建社区共享仓库做好准备,类似
npmjs.com
。 - 消除
vendor
目录,使可重复构建更容易。
hello world程序
从简单的hello world程序开始。
在GOPATH/src
之外创建目录helloworld,并使用go mod init
初始化。
mkdir -p $HOME/code/helloworld
cd $HOME/code/helloworld
go mod init example.org/hello
此时,项目目录下多了一个go.mod
文件:
module example.org/hello
go 1.12
创建hello.go
,并编写如下代码
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
构建并运行:
go build
./hello
Hello, world.
go.mod
文件被自动更新为:
module example.org/hello
go 1.12
require rsc.io/quote v1.5.2
可以看到go build
自动下载了hello.go中使用的包rsc.io/quote
,版本是v1.5.2
,并将其记录在go.mod
文件中。go.mod
即是用来记录模块依赖的文件。
同时还多了一个g.sum
文件,它用来记录依赖包的加密校验和,用于模块校验。
module
module是一组相关packages的集合,是版本化的基本单元,即Go按module进行版本化管理。
开启module模式
目前go命令直接支持module模式,但不是默认开启的。Go 1.11引入了环境变量GO111MODULE
,其取值是on
, off
, auto
,默认值是auto
。
off
: 不使用module模式。on
: 开启module模式。auto
: 当在GOPATH/src
之外的目录,并且当前目录或其上级目录(父目录或祖先目录)下有go.mod
文件,那么使用module模式。
Go 1.3开始,module模式将会是默认模式。
新建module
默认情况下创建一个module,只需要在GOPATH/src
之外的目录中加入go.mod
文件即可。go mod init
可以帮助我们创建go.mod
文件。
go.mod
文件所在目录称为module root
,即模块的根。module root
下的每一个后代目录即是一个package,module是这些packages的集合,但是不包括那些自身也含有go.mod
文件的后代目录。
module path
hello world程序中go.mod的第一行定义了module path
:
module example.org/hello
module path
是module下所有packages的import path的共同前缀。
例如,将之前示例中stringutil
包拷贝到当前module root,此时的目录结构:
helloworld
├── go.mod
├── go.sum
├── hello.go
└── stringutil
└── reverse.go
package stringutil
的import path是:example.org/hello/stringutil
,即<module-path>/<dirs>
。
在hello.go使用stringutil。
package main
import (
"fmt"
"rsc.io/quote"
"example.org/hello/stringutil"
)
func main() {
fmt.Println(stringutil.Reverse(quote.Hello()))
}
go build
./hello
.dlrow ,olleH
语义化版本
modules使用语义化版本号Semantic Versioning。版本号形式为v(major).(minor).(patch)
, 如v1.2.3
, v1.5.0-rc.1
。
版本号递增规则如下:
- major: 主版本号, 当API做了不兼容的修改时。
- minor: 次版本号,当做了向下兼容的功能性新增时。
- patch: 修订号,当做了向下兼容的问题修正时。
伪版本(Pseudo-version)
如果没有有效的semantic version时, go会通过被称为Pseudo version(伪版本)的方式记录版本号。
Pseudo version是类似v0.0.0-yyyymmddhhmmss-abcdefabcdef
的形式,时间部分是UTC时间,用来比较两个版本号哪个更早,最后的后缀部分是commit hash。
不同手写Pseudo version,go工具会自动将commit hash转换成Pseudo version形式。
如:
go get github.com/gorilla/mux@c856192
查看go.mod:
module example.org/hello
go 1.12
require (
github.com/gorilla/mux v1.6.3-0.20180517173623-c85619274f5d // indirect
rsc.io/quote v1.5.2
)
go工具会在未被直接使用的包后加上// indirect
标识。
v2+
当API做了不兼容更新时,我们需要增加主版本号,这时包的版本就来到了v2+。此时包的module path需要修改为<module-path>/vN
,如:
module example.org/hello/v2
这样在同一个项目可以使用同一个包的多个不同主版本。
import (
"example.org/hello/stringutil"
stringutilv2 "example.org/hello/v2/stringutil"
)
因为同一主板本的import path相同,Go提出了import compatibility rule,即导入兼容规则:
"If an old package and a new package have the same import path, the new package must be backwards compatible with the old package."
当在未转到module模式时已经有v2+ tag的,go工具会使用+incompatible
标识,如v2.0.1-0.yyyymmddhhmmss-abcdefabcdef+incompatible
。
最小版本选择算法(minimal version selection)
Go module采用最小版本选择算法。
如果应用依赖模块A和B,而A依赖D v1.0.0(require D v1.0.0), B依赖D v1.1.1(require D v1.1.1),D此时的最新版本是v1.2.0,那么编译时最终使用D的版本是v1.1.1,这可以最大程度的保证可重复构建(reproducible build)。
关于最小版本选择算法可以参考Minimal Version Selection。
gopkg.in
在GOPATH
方式部分介绍了gopkg.in
及其在版本化方面做的工作,在module模式下gopkg.in
做为一个特例存在: 所有以gopkg.in/
开始的module path,版本号部分依然用.
分隔,并且.v1
要出现在import path中,如gopkg.in/yaml.v1
、gopkg.in/yaml.v2
, 而不是gopkg.in/yaml
和gopkg.in/yaml/v2
。
go.mod
go.mod
用来记录模块依赖。
module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5
go.mod目前支持5个指令:
module
: 指定module pathgo
: 指定期望的Go版本require
: 依赖某个特定版本的模块exclude
: 忽略某个特定版本的模块replace
: 替换模块
像import语句一样,多条相同指令也可以合并:
require (
new/thing v2.3.4
old/thing v1.2.3
)
比如出于某些原因下载不到golang.org/x
上的模块,这时可以用replace
替换:
replace golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 => github.com/golang/image@v0.0.0-20180708004352-c73c2afc3b81
可以手动编辑go.mod,相关go命令也会自动更新go.mod,go也提供了编辑命令go mod edit
。
下载协议设计
在GOPATH
方式部分介绍了go get
从vcs下载代码。在module版本化的同时,Go也设计了更通用的module共享下载协议:http Get + zip
,从而摆脱对vcs的依赖。
提供module下载的web服务称为module proxy
。
module proxy需要支持如下get请求:
$GOPROXY/<module>/@v/list
: 返回module的所有已知版本号列表$GOPROXY/<module>/@v/<version>.info
: 以JSON格式返回模块的基础信息。$GOPROXY/<module>/@v/<version>.mod
: 返回所请求模块的go.mod文件。$GOPROXY/<module>/@v/<version>.zip
: 以zip压缩包格式返回模块的源码及相关文件。
为了避免在大小写敏感的文件系统下产生问题,<module>
和<version>
会被做case-encoded:大写字母会被编码成!<小写字母>
,如github.com/Azure
会被编码成github.com/!azure
。
这些文件会被缓存在GOPATH/pkg/mod/cache/download
,
➜ ~ ll $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/
total 40
drwxr-xr-x 9 creep staff 288 Aug 8 11:38 ./
drwxr-xr-x 3 creep staff 96 Aug 8 11:14 ../
-rw------- 1 creep staff 7 Aug 8 11:14 list
-rw-r--r-- 1 creep staff 0 Aug 8 11:14 list.lock
-rw------- 1 creep staff 50 Aug 8 11:14 v1.5.2.info
-rw-r--r-- 1 creep staff 0 Aug 8 11:25 v1.5.2.lock
-rw------- 1 creep staff 55 Aug 8 11:14 v1.5.2.mod
-rw------- 1 creep staff 2987 Aug 8 11:38 v1.5.2.zip
-rw------- 1 creep staff 47 Aug 8 11:38 v1.5.2.ziphash
模块信息字段如下:
type Info struct {
Version string // version string
Time time.Time // commit time
}
未来还会被扩展。
如hello world程序中使用的rsc.io/quote
,
cat $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
{"Version":"v1.5.2","Time":"2018-02-14T15:44:20Z"}
zip包里的文件路径要以<module>@<version>/
开始:
➜ ~ unzip -l $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
Archive: /Users/creep/code/go/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
Length Date Time Name
--------- ---------- ----- ----
1479 00-00-1980 00:00 rsc.io/quote@v1.5.2/LICENSE
131 00-00-1980 00:00 rsc.io/quote@v1.5.2/README.md
240 00-00-1980 00:00 rsc.io/quote@v1.5.2/buggy/buggy_test.go
55 00-00-1980 00:00 rsc.io/quote@v1.5.2/go.mod
793 00-00-1980 00:00 rsc.io/quote@v1.5.2/quote.go
917 00-00-1980 00:00 rsc.io/quote@v1.5.2/quote_test.go
--------- -------
3615 6 files
GOPROXY和go get
通过http Get + zip
的方式摆脱了对vcs的依赖,企业、组织和社区可以很方便的搭建模块共享仓库服务。
但是目前Go还没有提供模块的release工具,由作者手动打包不太现实,同时也没有“官方”的模块仓库, 所以go工具目前仍然提供了对vcs的支持。go工具会从vcs中提取信息生成info,mod,zip文件,同样缓存在GOPATH/pkg/mod/cache/download
。
Go引入了GOPROXY
环境变量,当没有设置GOPROXY
,或者设置为direct
时,go工具从vcs下载module;当设置为某个module proxy的url时,则从设置的url下载module;当设置为off
时,go工具不会下载module。
以下介绍GOPROXY
为direct
时go get
的行为,这也是目前go get
的默认行为。
go工具通过vcs的tag识别包版本号, 如git tag v1.2.3
。
使用go get
下载模块时,如果没有指定版本号,go会选择最新的符合semantic version的tag,如v1.2.3
,如果没有,那么选择最新的预发布tag,如v0.0.1-pre1
,如果没有,则选择最近的commit,如e3702be
。
go get
可以指定版本号,也可以指定vcs的某个特分支或commit hash, 如:
go get github.com/gorilla/mux@latest
go get github.com/gorilla/mux@v1.6.2
go get github.com/gorilla/mux@c856192
go get github.com/gorilla/mux@master
go get github.com/gorilla/mux@none
@latest
和不指定版本号的行为一致。@none
指删除此依赖包。
go-import
针对module模式,go-import
meta标签也提供了支持。
<meta name="go-import" content="example.org mod https://code.org/moduleproxy">
vcs
部分的值是mod
。指示可以在module proxy(https://code.org/moduleproxy
)下载模块。
模块更新
使用go get
时可以指定版本号,commit hash, 分支进行模块的更新;也可以使用-u
参数进行模块升级:
-u
: 升级到最新的minor和patch。-u=patch
: 升级到最新的patch。
模块的多主版本管理
现在还有一个问题,module作者如何管理多个主版本。
例如v1.0.0时未转换成module模式,v1.0.1转换为module模式,之后引入非兼容更新,主版本到达v2.0.0,之后v1.x.x和v2.x.x分别继续维护。
目前主要有两种管理方式。
基于主版本分支的方式(Major branch)
v1.0.1时加入了go.mod文件,转换到module模式,module path是my/thing
。之后引入了非兼容更新:去掉了包foo/bar,这时版本号到达v2,创建新分支v2
,module path修改为my/thing/v2
。在分支v2上迭代功能发布v2.0.0, v2.0.1等版本, 在分支v1上继续迭代发布v1.1.0等版本。
同理版本到达v3时,创建主分支v3
。
Go module使用vcs的tag判断版本,所以创建主分支不是必须的,创建主分支是为了代码维护方便。
基于主版本子目录的方式(Major subdirectory)
Go module支持主版本子目录(Major subdirectory)。
v1.0.1时加入了go.mod文件,转换到module模式,module path是my/thing
。之后到达版本v2,这时在module root下新建目录v2
,v2下加入go.mod文件,module path是my/thing/v2
。对v2目录下的修改发布版本v2.x.x,v2之外的修改发布v1.x.x。go命令会根据主版本号使用不同目录下的源代码等相关文件。
同理版本到达v3时,创建目录v3
。
兼容vendor目录
默认情况下,使用modules模式时go命令会忽略vendor目录。
同时,为了兼容vendor机制,go mod vendor
会在模块根目录下创建vendor目录,并拷贝所有依赖到vendor目录。编译时使用-mod=vendor
参数,则使用模块根目录下的vendor,其他目录下的vendor仍然被忽略。
修改stringutil/Reverse.go: 依赖rsc.io/quote
,在翻转后的string前加上quote.Hello()
前缀。
package stringutil
import "rsc.io/quote"
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return quote.Hello() + " " + string(r)
}
去掉hello.go对rsc.io/quote
的依赖:
package main
import (
"fmt"
"example.org/hello/stringutil"
)
func main() {
fmt.Println(stringutil.Reverse("peerc"))
}
执行go mod vendor
,会发现根目下多了vendor目录,vendor目录里是所有依赖模块(包括间接依赖模块)。
go mod vendor
tree $HOME/code/helloworld -L 2
$HOME/code/helloworld
├── go.mod
├── go.sum
├── hello
├── hello.go
├── stringutil
│ └── reverse.go
└── vendor
├── golang.org
├── modules.txt
└── rsc.io
删除$GOPATH/pkg
目录(排除影响),使用-mod=vendor
参数编译,并执行程序。
rm -rf $GOPATH/pkg
go build -v -mod=vendor
./hello
Hello, world. creep
--EOF--