go moudle使用实践及问题解决

一个现代的语言如果没有包管理机制,确实说不过去。谢天谢地,golang自1.11版本正式引入包管理机制,虽然社区产生了一些争吵,这些暂且不提,还是让我们拥抱官方的标准方案,社区的方案暂且随他去吧。关于go module官方说明可以参看https://github.com/golang/go/wiki/Modules。

一、GO111MODULE环境变量

可以用环境变量 GO111MODULE 开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是 auto。
* GO111MODULE=off 强制关闭mod机制,golang退化到1.11前的方式
* GO111MODULE=on 强制开启mod机制,go 会忽略 GOPATH只根据 go.mod 下载依赖。
* GO111MODULE=auto 默认方式,在非 $GOPATH/src 目录自动开启mod机制

在使用模块的时候,GOPATH 是无意义的,不过它还是会把下载的依赖储存在 $GOPATH/pkg/mod中,如果没有配置GOPATH环境变量,测试发现也不会有问题,golang会把缓存下周的包依赖放到 /tmp 目录。强烈推荐配置这个GOPATH环境变量,放到tmp目录的数据被清理时候,我们build工程还会重新下载项目依赖,这个是非常耗时的过程。

建议所有使用golang开发的项目尽快完成go moudle的切换,官方明确说明1.12版本开始语言会强制开启mod机制,届时再被动迁移比较麻烦。目前比较活跃的三方包均开始逐步支持mod了。

二、go get 命令

mod开启后,go get命令的使用方式也发生了变更为获取依赖的特定版本,用来升级和降级依赖。可以自动修改 go.mod 文件,而且依赖的依赖版本号也可能会变。新版 go get 可以在末尾加 @ 符号,用来指定版本。版本号必须符合https://semver.org/lang/zh-CN/ 的规范,版本 号前面需要带”v”

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
1. 主版本号:当你做了不兼容的 API 修改,
2. 次版本号:当你做了向下兼容的功能性新增,
3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
举例:

go get github.com/shawnfeng/sutil # 匹配最新的一个 tag
go get github.com/shawnfeng/sutil@latest # 和上面一样
go get github.com/shawnfeng/sutil@v1.0.5 # 匹配 v1.0.5
go get github.com/shawnfeng/sutil@5346574fa3b3 # 匹配 5346574fa3b3 版本
go get github.com/shawnfeng/sutil@master # 匹配 master 分支

三、仓库搭建

既然引入了包管理机制,我们就要统一规范module命名,在没有module机制之前,很多项目都是使用的一个大仓库管理了一堆项目,即使对项目有一定的拆分,但是因为没有包管理机制问题,想通过简单的go get方式实现不同库不同版本的动态发布非常困难,go get本身就是在命令除非时机拉取了仓库的master代码,之后就是本地,非0即1的模式让人用起来非常不爽。

伴鱼对项目的拆分按照两个规则
1. 项目之间按照进行大的分拆,每个项目进行单独的权限管理
2. 项目内按照公共和私有仓库进行分别的版本管理
– 公共部分公开可见,例如,服务接口client端代码,以及idl
– 私有部分仅对项目内人员可见,例如,例如服务的实现逻辑部门

包命名规则,路径url的路径规划一定要参考golang的规则看看这个文档 https://golang.org/cmd/go/ ,因为我们使用的都是git仓库,所以所有项目都携带了.git后缀

另外,不要尝试使用http方式的的代码路径,你会遇到各种问题,一定要通过https实现。至于遇到哪些问题,说多了都是泪,不信你自己可以玩玩看。

四、几个疑问

1、如果依赖的库没有go.mod文件有什么问题?

对于此类库是没有问题的,如果依赖的库使用module机制即增加了go.mod文件,那么项目如果依赖了该库,在自己的go.mod文件中是没有被依赖项目中依赖的仓库版本的,如果被依赖的库没有go.mod,那么在主项目中会把被依赖库中依赖的所有库在主项目go.mod中体现,并后缀indirect字段说明
require (
……
github.com/frankban/quicktest v1.1.0 // indirect
)

2、如果主项目中和被依赖项目中,或者主项目依赖的多个项目中共同依赖了同一个三方库,并且各个库的版本不一样,以哪一个版本为准?

golang会自动检查各个库中共同依赖仓库的版本,并使用最高的版本。伴鱼再迁移项目中遇到了这个问题,我在主项目中修改mgo的版本总是在tidy或者build后被强制替换,就是因为伴鱼主项目依赖的三方库中依赖了mgo更高的版本造成的。
为什么伴鱼要使用低版本mgo?是因为当时迁移go module之前,伴鱼使用的go版本是1.10,为了保证迁移安全,项目向1.11.2迁移时候保留了原来依赖库的版本。

3、接问题2,如果我们必须把依赖版本降低下来怎么办?如何强制降低版本的依赖

使用replace机制
replace (
gopkg.in/mgo.v2 => gopkg.in/mgo.v2 v2.0.0-20141107142503-e2e914857713
)

4、如何依赖未提交的库最新代码进行开发?

可以使用replace配置,替换成本地的路径
module example.com/go_service.git
replace (
example.com/server/common/go/pub.git => /localpath
)

require (
example.com/server/common/go/pub.git v0.0.0-20181226054539-bec28798b114
)

5、git 私有仓库如何使用?

因为golang拉取依赖都按照预定义策略,例如https,如果依赖仓库是私有仓库怎么完成自动构建?例如,我们有多个私有项目,项目之间也存在包依赖关系。可以通过修改.gitconfig配置完成,例如如果你使用的是gerrit做为代码审核工具的话,可以通过命令
git config –global url.”ssh://你的用户名@example.com:29418/”.insteadOf “https://example.com/”
在.gitconfig 增加如下的配置
[url “ssh://你的用户名@example:29418/”]
insteadOf = https://example.com/

6、go.mod go.sum 冲突解决方式?

如果在mod之前代码冲突会发生在多人修改一块代码,这是因为代码修改的问题,不是机制问题,开发者必须自行解决冲突。但是引入mod机制后,因为多人多go.mod的调整,已经go.sum的自动调整逻辑,在多个开发者调整go.mod时候冲突变成了必然发生的状态,这是机制问题,我们必须解决,伴鱼遇到了这个问题,尝试了用以下方式进行了解决。

利用将go.mod的公共依赖直接指定为master方式,并忽略对go.sum的版本管理

这种狗屎的解决办法,会强制要求开发者不要修改公共依赖的版本号,始终使用master,在build后,master被替换也不要提交其更改。
这种方式造成的问题,一个是没办法控制版本依赖关系,这让mod机制几乎失效,另外还存在一个不可调和的问题,测试和线上可能不能同时被编译通过,这个描述起来可能有点费劲,大体上是这样
例如,我们主项目依赖了一个 client的库,client库中包含了一些接口代码或者idl自动生成的代码,依赖的client项目也是由该主项目的开发人员负责,如果我们在版本迭代中开发了新功能,例如增加了idl的rpc接口,并且放入了测试环境,这时候主项目编译通过的条件是必须实现rpc的主体实现才能通过。
这时候还没有问题,突然,我们线上出了个bug,需要紧急修复,开发人员从主干拉取了master代码,进行紧急修复,并执行发布,这时候就出了问题,因为依赖的client已经被调整到了master,这时候要强制你hotfix中去合并开发中的代码,你说这个问题狗屎不?
所以这种方法我们使用1天就决定废弃

自动冲突解决

期间其实想了各种方案,但是最终选择了这种方案,让发布工具自动完成冲突解决,即发现go.mod冲突时候自动选择最新依赖,自动解决冲突的代码,这里贴一下

#! /usr/bin/env python
# -*- coding: utf-8 -*-


import sys


def isconfilt(ln):
    ln = ln.strip()
    if len(ln) < 3:
        return False
    ck = ln[0:3]
    return ck == "<<<" or ck == "===" or ck == ">>>"

def formatversion(se):
    vc = se.split()
    if len(vc) < 2:
        return None

    repo = vc[0]
    version = vc[1]

    vtime = version.split("-")
    if len(vtime) < 3:
        return {
            "repo": repo,
            "version": "0",
            "tp": "check",
        }

    return {
        "repo": repo,
        "version": vtime[1],
        "tp": "check",
    }


def doit():

    infile = sys.argv[1]

    fp = open(infile)
    data = fp.read()
    fp.close()

    lines = data.splitlines()

    ifconflictfile = False
    for e in lines:
        ifconflictfile = isconfilt(e)
        if ifconflictfile:
            break

    if not ifconflictfile:
        # 不是冲突的文件不要处理
        return


    vercheck = {}
    # 输出的行
    outlines = []

    is_in_require_scope = False


    for idx, e in enumerate(lines):
        se = e.strip()
        if len(se) > len("require"):
            if se[0:len("require")] == "require" and se[len(se)-1:len(se)] == "(":
                is_in_require_scope = True
                outlines.append({"ln": e, "tp": "output"})
                continue

        if is_in_require_scope and len(se) >= len(")") and se[0:1] == ")":
            is_in_require_scope = False
            outlines.append({"ln": e, "tp": "output"})
            continue

        isc = isconfilt(e)
        if not is_in_require_scope and isc:
            # 如果不是在require作用域发生冲突,直接发布失败,返回错误码1
            print "非require域发生冲突", idx, e
            return 1

        if isc:
            continue

        if not is_in_require_scope:
            outlines.append({"ln": e, "tp": "output"})
            continue

        if len(se) == 0:
            outlines.append({"ln": e, "tp": "output"})
            continue

        if len(se) >= 2 and se[0:2] == "//":
            outlines.append({"ln": e, "tp": "output"})
            continue


        rvt = formatversion(se)
        if rvt == None:
            print "require 依赖格式错误", idx, e
            return 1


        repo = rvt.get("repo", None)
        version = rvt.get("version", None)
        tp = rvt.get("tp", None)

        if repo not in vercheck:
            vercheck[repo] = version
        else:

            if vercheck[repo] < version:
                vercheck[repo] = version

        if tp == "output":
            outlines.append({"ln": e, "tp": "output"})
            continue

        outlines.append({"ln": e, "tp": "check", "version": version, "repo": repo})


    outputs = []
    for e in outlines:
        ln = e["ln"]
        tp = e["tp"]
        if tp == "output":
            outputs.append(ln)
            continue


        if tp == "check" and e["version"] == vercheck[e["repo"]]:
            #print e
            outputs.append(ln)

    fp = open(infile, "w")

    for e in outputs:
        fp.write(e+"\n")
    fp.close()

    return 0

if __name__ == '__main__':
    sys.exit(doit())

这种自动解决方式,一定能避免上面方案提到的问题么?这个其实不是必然的,虽然很少会发生,但是极端情况如果我们开发人员A提交的代码版本,不希望被依赖到主项目,但是开发人员B也在此版本基础上进行开发,并希望依赖最新版本代码,这时候还可能会出现上面的问题,解决的办法
– 不要弄一个太过庞大的项目仓库,随着业务的膨胀保持足够小的分拆力度,这样能很大程度减少问题发生
– 通过子go.mod来解决,即在发生冲突的项目内目录,独立建立go.mod文件,做特殊的依赖,golang 使用go.mod的方式是,层级式的一层层向上查找,所以子目录的go.mod会被优先使用

五、填坑

1、cannot find module providing package

这个问题困扰我很久,后来才才发现是git版本问题。git升级版本后解决问题。一定要先确认git版本,当前伴鱼使用的是2.18.0版本是正常的

2、带大写字母的库 unexpected module path

这个问题非常坑,我觉得这个是个bug,例如 github.com/Jeffail/tunny 这个库,只要你依赖了,你是一定tidy或这个build不过去的,一直会报
$ go mod tidy -v
go: finding github.com/jeffail/tunny v0.0.0-20181108205650-4921fff29480
go: github.com/jeffail/tunny@v0.0.0-20181108205650-4921fff29480: parsing go.mod: unexpected module path “github.com/Jeffail/tunny”
go: error loading module requirements
这个应该是路径中携带了大写字母造成的bug,怎么解决

replace (
github.com/jeffail/tunny => github.com/Jeffail/tunny v0.0.0-20181108205650-4921fff29480
)

require (
github.com/jeffail/tunny v0.0.0-20181108205650-4921fff29480

3、包拉取超时timeout错误

请使用代理,你依赖的包被墙了