Go开发关键技术指南:Modules

Modules

先把最重要的说了,关于modules的最新详细信息可以执行命令go help modules或者查这个长长的手册Go Modules,另外modules弄清楚后很好用迁移成本低。

Go Module的好处,可以参考Demo:

  1. 代码不用必须放GOPATH,可以放在任何目录,终于不用做软链了。
  2. Module依然可以用vendor,如果不需要更新依赖,可以不必从远程下载依赖代码,同样不必放GOPATH。
  3. 如果在一个仓库可以直接引用,会自动识别模块内部的package,同样不用链接到GOPATH。

Go最初是使用GOPATH存放依赖的包(项目和代码),这个GOPATH是公共的目录,如果依赖的库的版本不同就杯具了。2016年也就是7年后才支持vendor规范,就是将依赖本地化了,每个项目都使用自己的vendor文件夹,但这样也解决不了冲突的问题(具体看下面的分析),相反导致各种包管理项目天下混战,参考pkg management tools。2017年也就是8年后,官方的vendor包管理器dep才确定方案,看起来命中注定的TheOne终于尘埃落定。不料2018年也就是9年后,又提出比较完整的方案versioning和vgo,这年Go1.11支持了Modules,2019年Go1.12和Go1.13改进了不少Modules内容,Go官方文档推出一系列的Part 1 — Using Go Modules、Part 2 — Migrating To Go Modules和Part 3 — Publishing Go Modules,终于应该大概齐能明白,这次真的确定和肯定了,Go Modules是最终方案。

为什么要搞出GOPATH、Vendor和GoModules这么多技术方案?本质上是为了创造就业岗位,一次创造了index、proxy和sum三个官网,哈哈哈。当然技术上也是必须要这么做的,简单来说是为了解决古老的DLL Hell问题,也就是依赖管理和版本管理的问题。版本说起来就是几个数字,比如1.2.3,实际上是非常复杂的问题,推荐阅读Semantic Versioning,假设定义了良好和清晰的API,我们用版本号来管理API的兼容性;版本号一般定义为MAJOR.MINOR.PATCH,Major变更时意味着不兼容的API变更,Minor是功能变更但是是兼容的,Patch是BugFix也是兼容的,Major为0时表示API还不稳定。由于Go的包是URL的,没有版本号信息,最初对于包的版本管理原则是必须一直保持接口兼容:

If an old package and a new package have the same import path, the new package must be backwards compatible with the old package.

试想下如果所有我们依赖的包,一直都是接口兼容的,那就没有啥问题,也没有DLL Hell。可惜现实却不是这样,如果我们提供过包就知道,对于持续维护和更新的包,在最初不可能提供一个永远不变的接口,变化的接口就是不兼容的了。就算某个接口可以不变,还有依赖的包,还有依赖的依赖的包,还有依赖的依赖的依赖的包,以此往复,要求世界上所有接口都不变,才不会有版本问题,这么说起来,包管理是个极其难以解决的问题,Go花了10年才确定最终方案就是这个原因了,下面举例子详细分析这个问题。

备注:标准库也有遇到接口变更的风险,比如Context是Go1.7才引入标准库的,控制程序生命周期,后续有很多接口的第一个参数都是ctx context.Context,比如net.DialContext就是后面加的一个函数,而net.Dial也是调用它。再比如http.Request.WithContext则提供了一个函数,将context放在结构体中传递,这是因为要再为每个Request的函数新增一个参数不太合适。从context对于标准库的接口的变更,可以看得到这里有些不一致性,有很多批评的声音比如Context should go away for Go 2,就是觉得在标准库中加context作为第一个参数不能理解,比如Read(ctx context.Context等。

GOPATH & Vendor

咱们先看GOPATH的方式。Go引入外部的包,是URL方式的,先在环境变量$GOROOT中搜索,然后在$GOPATH中搜索,比如我们使用Errors,依赖包github.com/ossrs/go-oryx-lib/errors,代码如下所示:

package main

import (
  "fmt"
  "github.com/ossrs/go-oryx-lib/errors"
)

func main() {
  fmt.Println(errors.New("Hello, playground"))
}

如果我们直接运行会报错,错误信息如下:

prog.go:5:2: cannot find package "github.com/ossrs/go-oryx-lib/errors" in any of:
    /usr/local/go/src/github.com/ossrs/go-oryx-lib/errors (from $GOROOT)
    /go/src/github.com/ossrs/go-oryx-lib/errors (from $GOPATH)

需要先下载这个依赖包go get -d github.com/ossrs/go-oryx-lib/errors,然后运行就可以了。下载后放在GOPATH中:

Mac $ ls -lh $GOPATH/src/github.com/ossrs/go-oryx-lib/errors
total 72
-rw-r--r--  1 chengli.ycl  staff   1.3K Sep  8 15:35 LICENSE
-rw-r--r--  1 chengli.ycl  staff   2.2K Sep  8 15:35 README.md
-rw-r--r--  1 chengli.ycl  staff   1.0K Sep  8 15:35 bench_test.go
-rw-r--r--  1 chengli.ycl  staff   6.7K Sep  8 15:35 errors.go
-rw-r--r--  1 chengli.ycl  staff   5.4K Sep  8 15:35 example_test.go
-rw-r--r--  1 chengli.ycl  staff   4.7K Sep  8 15:35 stack.go

如果我们依赖的包还依赖于其他的包,那么go get会下载所有依赖的包到GOPATH。这样是下载到公共的GOPATH的,可以想到,这会造成几个问题:

  1. 每次都要从网络下载依赖,可能对于美国这个问题不存在,但是对于中国,要从GITHUB上下载很大的项目,是个很麻烦的问题,还没有断点续传。
  2. 如果两个项目,依赖了GOPATH了项目,如果一个更新会导致另外一个项目出现问题。比如新的项目下载了最新的依赖库,可能会导致其他项目出问题。
  3. 无法独立管理版本号和升级,独立依赖不同的包的版本。比如A项目依赖1.0的库,而B项目依赖2.0的库。注意:如果A和B都是库的话,这个问题还是无解的,它们可能会同时被一个项目引用,如果A和B是最终的应用是没有问题,应用可以用不同的版本,它们在自己的目录。

为了解决这些问题,引入了vendor,在src下面有个vendor目录,将依赖的库都下载到这个目录,同时会有描述文件说明依赖的版本,这样可以实现升级不同库的升级。参考vendor,以及官方的包管理器dep。但是vendor并没有解决所有的问题,特别是包的不兼容版本的问题,只解决了项目或应用,也就是会编译出二进制的项目所依赖库的问题。

咱们把上面的例子用vendor实现,先要把项目软链或者挪到GOPATH里面去,若没有dep工具可以参考Installation安装,然后执行下面的命令来将依赖导入到vendor目录:

dep init && dep ensure

这样依赖的文件就会放在vendor下面,编译时也不再需要从远程下载了:

├── Gopkg.lock
├── Gopkg.toml
├── t.go
└── vendor
    └── github.com
        └── ossrs
            └── go-oryx-lib
                └── errors
                    ├── errors.go
                    └── stack.go

Remark: Vendor也会选择版本,也有版本管理,但每个包它只会选择一个版本,也就是本质上是本地化的GOPATH,如果出现钻石依赖和冲突还是无解,下面会详细说明。

何为版本冲突?

我们来看GOPATH和Vencor无法解决的一个问题,版本依赖问题的一个例子Semantic Import Versioning,考虑钻石依赖的情况,用户依赖于两个云服务商的SDK,而他们可能都依赖于公共的库,形成一个钻石形状的依赖,用户依赖AWS和Azure而它们都依赖OAuth:

image.png

如果公共库package(这里是OAuth)的导入路径一样(比如是github.com/google/oauth),但是做了非兼容性变更,发布了OAuth-r1和OAuth-r2,其中一个云服务商更新了自己的依赖,另外一个没有更新,就会造成冲突,他们依赖的版本不同:

image.png

在Go中无论怎么修改都无法支持这种情况,除非在package的路径中加入版本语义进去,也就是在路径上带上版本信息(这就是Go Modules了),这和优雅没有关系,这实际上是最好的使用体验:

image.png

另外做法就是改变包路径,这要求包提供者要每个版本都要使用一个特殊的名字,但使用者也不能分辨这些名字代表的含义,自然也不知道如何选择哪个版本。

先看看Go Modules创造的三大就业岗位,index负责索引、proxy负责代理缓存和sum负责签名校验,它们之间的关系在Big Picture中有描述。可见go-get会先从index获取指定package的索引,然后从proxy下载数据,最后从sum来获取校验信息:

image.png

vgo全面实践

还是先跟着官网的三部曲,先了解下modules的基本用法,后面补充下特别要注意的问题就差不多齐了。首先是Using Go Modules,如何使用modules,还是用上面的例子,代码不用改变,只需要执行命令:

go mod init private.me/app && go run t.go

Remark:和vendor并不相同,modules并不需要在GOPATH下面才能创建,所以这是非常好的。

执行的结果如下,可以看到vgo查询依赖的库,下载后解压到了cache,并生成了go.mod和go.sum,缓存的文件在$GOPATH/pkg下面:

Mac:gogogo chengli.ycl$ go mod init private.me/app && go run t.go
go: creating new go.mod: module private.me/app
go: finding github.com/ossrs/go-oryx-lib v0.0.7
go: downloading github.com/ossrs/go-oryx-lib v0.0.7
go: extracting github.com/ossrs/go-oryx-lib v0.0.7
Hello, playground

Mac:gogogo chengli.ycl$ cat go.mod
module private.me/app
go 1.13
require github.com/ossrs/go-oryx-lib v0.0.7 // indirect

Mac:gogogo chengli.ycl$ cat go.sum
github.com/ossrs/go-oryx-lib v0.0.7 h1:k8ml3ZLsjIMoQEdZdWuy8zkU0w/fbJSyHvT/s9NyeCc=
github.com/ossrs/go-oryx-lib v0.0.7/go.mod h1:i2tH4TZBzAw5h+HwGrNOKvP/nmZgSQz0OEnLLdzcT/8=

Mac:gogogo chengli.ycl$ tree $GOPATH/pkg
/Users/winlin/go/pkg
├── mod
│   ├── cache
│   │   ├── download
│   │   │   ├── github.com
│   │   │   │   └── ossrs
│   │   │   │       └── go-oryx-lib
│   │   │   │           └── @v
│   │   │   │               ├── list
│   │   │   │               ├── v0.0.7.info
│   │   │   │               ├── v0.0.7.zip
│   │   │   └── sumdb
│   │   │       └── sum.golang.org
│   │   │           ├── lookup
│   │   │           │   └── github.com
│   │   │           │       └── ossrs
│   │   │           │           └── [email protected]
│   └── github.com
│       └── ossrs
│           └── [email protected]
│               ├── errors
│               │   ├── errors.go
│               │   └── stack.go
└── sumdb
└── sum.golang.org
└── latest

可以手动升级某个库,即go get这个库:

Mac:gogogo chengli.ycl$ go get github.com/ossrs/go-oryx-lib
go: finding github.com/ossrs/go-oryx-lib v0.0.8
go: downloading github.com/ossrs/go-oryx-lib v0.0.8
go: extracting github.com/ossrs/go-oryx-lib v0.0.8

Mac:gogogo chengli.ycl$ cat go.mod
module private.me/app
go 1.13
require github.com/ossrs/go-oryx-lib v0.0.8

升级某个包到指定版本,可以带上版本号,例如go get github.com/ossrs/[email protected]。当然也可以降级,比如现在是v0.0.8,可以go get github.com/ossrs/[email protected]降到v0.0.7版本。也可以升级所有依赖的包,执行go get -u命令就可以。查看依赖的包和版本,以及依赖的依赖的包和版本,可以执行go list -m all命令。查看指定的包有哪些版本,可以用go list -m -versions github.com/ossrs/go-oryx-lib命令。

Note: 关于vgo如何选择版本,可以参考Minimal Version Selection。

如果依赖了某个包大版本的多个版本,那么会选择这个大版本最高的那个,比如:

  • 若a依赖v1.0.1,b依赖v1.2.3,程序依赖a和b时,最终使用v1.2.3。
  • 若a依赖v1.0.1,d依赖v0.0.7,程序依赖a和d时,最终使用v1.0.1,也就是认为v1是兼容v0的。

比如下面代码,依赖了四个包,而这四个包依赖了某个包的不同版本,分别选择不同的包,执行rm -f go.mod && go mod init private.me/app && go run t.go,可以看到选择了不同的版本,始终选择的是大版本最高的那个(也就是满足要求的最小版本):

package main

import (
    "fmt"
    "github.com/winlinvip/mod_ref_a" // 1.0.1
    "github.com/winlinvip/mod_ref_b" // 1.2.3
    "github.com/winlinvip/mod_ref_c" // 1.0.3
    "github.com/winlinvip/mod_ref_d" // 0.0.7
)

func main() {
    fmt.Println("Hello",
        mod_ref_a.Version(),
        mod_ref_b.Version(),
        mod_ref_c.Version(),
        mod_ref_d.Version(),
    )
}

若包需要升级大版本,则需要在路径上加上版本,包括本身的go.mod中的路径,依赖这个包的go.mod,依赖它的代码,比如下面的例子,同时使用了v1和v2两个版本(只用一个也可以):

package main

import (
    "fmt"
    "github.com/winlinvip/mod_major_releases"
    v2 "github.com/winlinvip/mod_major_releases/v2"
)

func main() {
    fmt.Println("Hello",
        mod_major_releases.Version(),
        v2.Version2(),
    )
}

运行这个程序后,可以看到go.mod中导入了两个包:

module private.me/app
go 1.13
require (
        github.com/winlinvip/mod_major_releases v1.0.1
        github.com/winlinvip/mod_major_releases/v2 v2.0.3
)

Remark: 如果需要更新v2的指定版本,那么路径中也必须带v2,也就是所有v2的路径必须带v2,比如go get github.com/winlinvip/mod_major_releases/[email protected]

而库提供大版本也是一样的,参考mod_major_releases/v2,主要做的事情:

  1. 新建v2的分支,git checkout -b v2,比如https://github.com/winlinvip/mod_major_releases/tree/v2。
  2. 修改go.mod的描述,路径必须带v2,比如module github.com/winlinvip/mod_major_releases/v2
  3. 提交后打v2的tag,比如git tag v2.0.0,分支和tag都要提交到git。

其中go.mod更新如下:

module github.com/winlinvip/mod_major_releases/v2
go 1.13

代码更新如下,由于是大版本,所以就变更了函数名称:

package mod_major_releases

func Version2() string {
    return "mmv/2.0.3"
}

Note: 更多信息可以参考Modules: v2,还有Russ Cox: From Repository to Modules介绍了两种方式,常见的就是上面的分支方式的例子,还有一种文件夹方式。

Go Modules特别需要注意的问题:

  • 对于公开的package,如果go.mod中描述的package,和公开的路径不相同,比如go.mod是private.me/app,而发布到github.com/winlinvip/app,当然其他项目import这个包时会出现错误。对于库,也就是希望别人依赖的包,go.mod描述的和发布的路径,以及package名字都应该保持一致。
  • 如果一个包没有发布任何版本,则会取最新的commit和日期,格式为v0.0.0-日期-commit号,比如v0.0.0-20191028070444-45532e158b41,参考Pseudo Versions。版本号可以从v0.0.x开始,比如v0.0.1或者v0.0.3或者v0.1.0或者v1.0.1之类,没有强制要求必须要是1.0开始的发布版本。
  • mod replace在子module无效,只在编译的那个top level有效,也就是在最终生成binary的go.mod中定义才有效,官方的说明是为了让最终生成时控制依赖。例如想要把github.com/pkg/errors重写为github.com/winlinvip/errors这个包,正确做法参考分支replace_errors;若不在主模块(top level)中replace参考replace_in_submodule,只在子模块中定义了replace但会被忽略;如果在主模块replace会生效replace_errors,而且在主模块依赖掉子模快依赖的模块也生效replace_deps_of_submodule。不过在子模快中也能replace,这个预感到会是个混淆的地方。有一个例子就是fork仓库后修改后自己使用,这时候go.mod的package当然也变了,参考Migrating Go1.13 Errors,Go1.13的errors支持了Unwrap接口,这样可以拿到root error,而pkg/errors使用的则是Cause(err)函数来获取root error,而提的PR没有支持,pkg/errors不打算支持Go1.13的方式,作者建议fork来解决,所以就可以使用go mod replace来将fork的url替换pkg/errors。
  • go get并非将每个库都更新后取最新的版本,比如库github.com/winlinvip/mod_minor_versions有v1.0.1、v1.1.2两个版本,目前依赖的是v1.1.2版本,如果库更新到了v1.2.3版本,立刻使用go get -u并不会更新到v1.2.3,执行go get -u github.com/winlinvip/mod_minor_versions也一样不会更新,除非显式更新go get github.com/winlinvip/[email protected]才会使用这个版本,需要等一定时间后才会更新。
  • 对于大版本比如v2,必须用go.mod描述,直接引用也可以比如go get github.com/winlinvip/[email protected],会提示v2.0.0+incompatible,意思就是默认都是v0和v1,而直接打了v2.0.0的tag,虽然版本上匹配到了,但实际上是把v2当做v1在用,有可能会有不兼容的问题。或者说,一般来说v2.0.0的这个tag,一定会有接口的变更(否则就不能叫v2了),如果没有用go.mod会把这个认为是v1,自然可能会有兼容问题了。
  • 更新大版本时必须带版本号比如go get github.com/winlinvip/mod_major_releases/[email protected],如果路径中没有这个v2则会报错无法更新,比如go get github.com/winlinvip/[email protected],错误消息是invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1,这个就是说mod_major_releases这个下面有go.mod描述的版本是v0或v1,但后面指定的版本是@v2所以不匹配无法更新。
  • 和上面的问题一样,如果在go.mod中,大版本路径中没有带版本,比如require github.com/winlinvip/mod_major_releases v2.0.3,一样会报错module contains a go.mod file, so major version must be compatible: should be v0 or v1,这个有点含糊因为包定义的go.mod是v2的,这个错误的意思是,require的那个地方,要求的是v0或v1,而实际上版本是v2.0.3,这个和手动要求更新go get github.com/winlinvip/[email protected]是一回事。
  • 注意三大岗位有cache,比如[email protected]的go.mod描述有错误,应该是v5,而不是v3。如果在打完tag后,获取了这个版本go get github.com/winlinvip/mod_major_error/v5,会提示错误but does not contain package github.com/winlinvip/mod_major_error/v5等错误,如果删除这个tag后再推v5.0.0,还是一样的错误,因为index和goproxy有缓存这个版本的信息。解决版本就是升一个版本v5.0.1,直接获取这个版本就可以,比如go get github.com/winlinvip/mod_major_error/[email protected],这样才没有问题。详细参考Semantic versions and modules。
  • 和上面一样的问题,如果在版本没有发布时,就有go get的请求,会造成版本发布后也无法获取这个版本。比如github.com/winlinvip/mod_major_error没有打版本v3.0.1,就请求go get github.com/winlinvip/mod_major_error/[email protected],会提示没有这个版本。如果后面再打这个tag,就算有这个tag后,也会提示401找不到reading https://sum.golang.org/lookup/github.com/winlinvip/mod_major_error/[email protected]: 410 Gone。只能再升级个版本,打个新的tag比如v3.0.2才能获取到。

总结起来说:

  • GOPATH,自从默认为$HOME/go后,很好用,依赖的包都缓存在这个公共的地方,只要项目不大,完全是很直接很好用的方案。一般情况下也够用了,估计GOPATH可能会被长期使用,毕竟习惯才是最可怕的,习惯是活的最久的,习惯就成为了一种生活方式,用余老师的话说“文化是一种精神价值和生活方式,最终体现了集体人格”。
  • vendor,vendor缓存依赖在项目本地,能解决很多问题了,比GOPATH更好的是对于依赖可以定期更新,一般的项目中,对于依赖都是有需要了去更新,而不是每次编译都去取最新的代码。所以vendor还是非常实用的,如果能保持比较克制,不要因为要用一个函数就要依赖一个包,结果这个包依赖了十个,这十个又依赖了百个。
  • vgo/modules,代码使用上没有差异;在版本更新时比如明确需要导入v2的包,才会在导入url上有差异;代码缓存上使用proxy来下载,缓存在GOPATH的pkg中,由于有版本信息所以不会有冲突;会更安全,因为有sum在;会更灵活,因为有index和proxy在。

如何无缝迁移?

现有GOPATH和vendor的项目,如何迁移到modules呢?官方的迁移指南Migrating to Go Modules,说明了项目会有三种状态:

  • 完全新的还没开始的项目。那么就按照上面的方式,用modules就好了。
  • 现有的项目,使用了其他依赖管理,也就是vendor,比如dep或glide等。go mod会将现有的格式转换成modules,支持的格式参考这里。其实modules还是会继续支持vendor,参考下面的详细描述。
  • 现有的项目,没有使用任何依赖管理,也就是GOPATH。注意go mod init的包路径,需要和之前导出的一样,特别是Go1.4支持的import comment,可能和仓库的路径并不相同,比如仓库在https://go.googlesource.com/lint,而包路径是golang.org/x/lint。

Note: 特别注意如果是库支持了v2及以上的版本,那么路径中一定需要包含v2,比如github.com/russross/blackfriday/v2。而且需要更新引用了这个包的v2的库,比较蛋疼,不过这种情况还好是不多的。

咱们先看一个使用GOPATH的例子,我们新建一个测试包,先以GOPATH方式提供,参考github.com/winlinvip/mod_gopath,依赖于github.com/pkg/errors,rsc.io/quote和github.com/gorilla/websocket。

再看一个vendor的例子,将这个GOPATH的项目,转成vendor项目,参考github.com/winlinvip/mod_vendor,安装完dep后执行dep init就可以了,可以查看依赖:

chengli.ycl$ dep status
PROJECT                       CONSTRAINT  VERSION   REVISION  LATEST    PKGS USED
github.com/gorilla/websocket  ^1.4.1      v1.4.1    c3e18be   v1.4.1    1
github.com/pkg/errors         ^0.8.1      v0.8.1    ba968bf   v0.8.1    1
golang.org/x/text             v0.3.2      v0.3.2    342b2e1   v0.3.2    6
rsc.io/quote                  ^3.1.0      v3.1.0    0406d72   v3.1.0    1
rsc.io/sampler                v1.99.99    v1.99.99  732a3c4   v1.99.99  1

接下来转成modules包,先拷贝一份github.com/winlinvip/mod_gopath代码(这里为了演示差别所以拷贝了一份,直接转换也是可以的),变成github.com/winlinvip/mod_gopath_vgo,然后执行命令go mod init github.com/winlinvip/mod_gopath_vgo && go test ./... && go mod tidy,接着发布版本比如git add . && git commit -am "Migrate to vgo" && git tag v1.0.1 && git push origin v1.0.1:

Mac:mod_gopath_vgo chengli.ycl$ cat go.mod
module github.com/winlinvip/mod_gopath_vgo
go 1.13
require (
    github.com/gorilla/websocket v1.4.1
    github.com/pkg/errors v0.8.1
    rsc.io/quote v1.5.2
)

depd的vendor的项目也是一样的,先拷贝一份github.com/winlinvip/mod_vendor成github.com/winlinvip/mod_vendor_vgo,执行命令go mod init github.com/winlinvip/mod_vendor_vgo && go test ./... && go mod tidy,接着发布版本比如git add . && git commit -am "Migrate to vgo" && git tag v1.0.3 && git push origin v1.0.3

module github.com/winlinvip/mod_vendor_vgo
go 1.13
require (
    github.com/gorilla/websocket v1.4.1
    github.com/pkg/errors v0.8.1
    golang.org/x/text v0.3.2 // indirect
    rsc.io/quote v1.5.2
    rsc.io/sampler v1.99.99 // indirect
)

这样就可以在其他项目中引用它了:

package main

import (
    "fmt"
    "github.com/winlinvip/mod_gopath"
    "github.com/winlinvip/mod_gopath/core"
    "github.com/winlinvip/mod_vendor"
    vcore "github.com/winlinvip/mod_vendor/core"
    "github.com/winlinvip/mod_gopath_vgo"
    core_vgo "github.com/winlinvip/mod_gopath_vgo/core"
    "github.com/winlinvip/mod_vendor_vgo"
    vcore_vgo "github.com/winlinvip/mod_vendor_vgo/core"
)

func main() {
    fmt.Println("mod_gopath is", mod_gopath.Version(), core.Hello(), core.New("gopath"))
    fmt.Println("mod_vendor is", mod_vendor.Version(), vcore.Hello(), vcore.New("vendor"))
    fmt.Println("mod_gopath_vgo is", mod_gopath_vgo.Version(), core_vgo.Hello(), core_vgo.New("vgo(gopath)"))
    fmt.Println("mod_vendor_vgo is", mod_vendor_vgo.Version(), vcore_vgo.Hello(), vcore_vgo.New("vgo(vendor)"))
}

Note: 对于私有项目,可能无法使用三大件来索引校验,那么可以设置GOPRIVATE来禁用校验,参考Module configuration for non public modules。

vgo with vendor

Vendor并非不能用,可以用modules同时用vendor,参考How do I use vendoring with modules? Is vendoring going away?,其实vendor并不会消亡,Go社区有过详细的讨论vgo & vendoring决定在modules中支持vendor,有人觉得,把vendor作为modules的存储目录挺好的啊。在modules中开启vendor有几个步骤:

  1. 先转成modules,参考前面的步骤,也可以新建一个modules例如go mod init xxx,然后把代码写好,就是一个标准的module,不过文件是存在$GOPATH/pkg的,参考github.com/winlinvip/[email protected]
  2. go mod vendor,这一步做的事情,就是将modules中的文件都放到vendor中来。当然由于go.mod也存在,当然也知道这些文件的版本信息,也不会造成什么问题,只是新建了一个vendor目录而已。在别人看起来这就是这正常的modules,和vendor一点影响都没有。参考github.com/winlinvip/[email protected]
  3. go build -mod=vendor,修改mod这个参数,默认是会忽略这个vendor目录了,加上这个参数后就会从vendor目录加载代码(可以把$GOPATH/pkg删掉发现也不会下载代码)。当然其他也可以加这个flag,比如go test -mod=vendor ./...或者go run -mod=vendor .

调用这个包时,先使用modules把依赖下载下来,比如go mod init private.me/app && go run t.go

package main

import (
    "fmt"
    "github.com/winlinvip/mod_vendor_vgo"
    vcore_vgo "github.com/winlinvip/mod_vendor_vgo/core"
    "github.com/winlinvip/mod_vgo_with_vendor"
    vvgo_core "github.com/winlinvip/mod_vgo_with_vendor/core"
)

func main() {
    fmt.Println("mod_vendor_vgo is", mod_vendor_vgo.Version(), vcore_vgo.Hello(), vcore_vgo.New("vgo(vendor)"))
    fmt.Println("mod_vgo_with_vendor is", mod_vgo_with_vendor.Version(), vvgo_core.Hello(), vvgo_core.New("vgo with vendor"))
}

然后一样的也要转成vendor,执行命令go mod vendor && go run -mod=vendor t.go。如果有新的依赖的包需要导入,则需要先使用modules方式导入一次,然后go mod vendor拷贝到vendor。其实一句话来说,modules with vendor就是最后提交代码时,把依赖全部放到vendor下面的一种方式。

Note: IDE比如goland的设置里面,有个Preferences /Go /Go Modules(vgo) /Vendoring mode,这样会从项目的vendor目录解析,而不是从全局的cache。如果不需要导入新的包,可以默认开启vendor方式,执行命令go env -w GOFLAGS='-mod=vendor'

Concurrency&Control

并发是服务器的基本问题,并发控制当然也是基本问题,Go并不能避免这个问题,只是将这个问题更简化。

Links

由于限制了文章字数,只好分成不同章节:

  • Overview 为何Go有时候也叫Golang?为何要选择Go作为服务器开发的语言?是冲动?还是骚动?Go的重要里程碑和事件,当年吹的那些牛逼,都实现了哪些?
  • Could Not Recover 君可知,有什么panic是无法recover的?包括超过系统线程限制,以及map的竞争写。当然一般都能recover,比如Slice越界、nil指针、除零、写关闭的chan等。
  • Errors 为什么Go2的草稿3个有2个是关于错误处理的?好的错误处理应该怎么做?错误和异常机制的差别是什么?错误处理和日志如何配合?
  • Logger 为什么标准库的Logger是完全不够用的?怎么做日志切割和轮转?怎么在混成一坨的服务器日志中找到某个连接的日志?甚至连接中的流的日志?怎么做到简洁又够用?
  • Interfaces 什么是面向对象的SOLID原则?为何Go更符合SOLID?为何接口组合比继承多态更具有正交性?Go类型系统如何做到looser, organic, decoupled, independent, and therefore scalable?一般软件中如果出现数学,要么真的牛逼要么装逼。正交性这个数学概念在Go中频繁出现,是神仙还是妖怪?为何接口设计要考虑正交性?
  • Modules 如何避免依赖地狱(Dependency Hell)?小小的版本号为何会带来大灾难?Go为什么推出了GOPATH、Vendor还要搞module和vgo?新建了16个仓库做测试,碰到了9个坑,搞清楚了gopath和vendor如何迁移,以及vgo with vendor如何使用(毕竟生产环境不能每次都去外网下载)。
  • Concurrency & Control 服务器中的并发处理难在哪里?为什么说Go并发处理优势占领了云计算开发语言市场?什么是C10K、C10M问题?如何管理goroutine的取消、超时和关联取消?为何Go1.7专门将context放到了标准库?context如何使用,以及问题在哪里?
  • Engineering Go在工程化上的优势是什么?为什么说Go是一门面向工程的语言?覆盖率要到多少比较合适?什么叫代码可测性?为什么良好的库必须先写Example?
  • Go2 Transition Go2会像Python3不兼容Python2那样作吗?C和C++的语言演进可以有什么不同的收获?Go2怎么思考语言升级的问题?
  • SRS & Others Go在流媒体服务器中的使用。Go的GC靠谱吗?Twitter说相当的靠谱,有图有真相。为何Go的声明语法是那样?C的又是怎样?是拍的大腿,还是拍的脑袋?

你可能感兴趣的:(Go开发关键技术指南:Modules)