go-mod-issue.md 5.9 KB


title: "由 go module 的一个问题说起" date: 2021-12-12T11:57:51+07:00

draft: false

问题描述

在描述问题之前,简单说一点背景

go mod import 机制的简单描述

这里只说 go mod 机制中和本问题相关的部分。

go mod init github.com/myname/projectname 初始化一个 go mod,这里的 github.com/myname/projectname 就是 mod 的名称,没有什么特别的规定,一般来说,使用 github 的话,就是这个工程在 github.com 中的 repo 地址,当这个 module 是作为 library 开发的,有别人使用这个库的时候就可以以 go get 的方式加入到自己的 go mod import 列表中来。然后可以在源代码中 import github.com/myname/projectname 来使用,其实在 go get github.com/myname/projectname 之后,这个库就在本地的 $GOPATH/pkg/mod/github.com/myname/projectname@vx.x.x 路径中。

gin为示例,以图来描述大致如下:

远程仓库 github.com/gin-gonic/gin(module github.com/gin-gonic/gin)

            |
            |

go get github.com/gin-gonic/gin 到本地保存在 $GOPATH/pkg/mod/github.com/gin-gonic/gin

            |
            |

工程中引入 import "github.com/gin-gonic/gin

遇到的问题

从上面描述中可以看到,go mod namegithub.com 中地址、go get 到本地的路径、import mod name 之间存在对应关系。

对于 gin 这个库,如果我 fork 一下,则有了我自己的一个版本,假设我的 github 账户名为 john 则我的 fork 版本位于 github.com/john/gin,当我本地 go get 我的 fork 版本后,问题出来了。很简单,因为 fork,上面那个“对应关系”被打破了,这时候的状态图示如下

远程仓库 github.com/john/gin(module github.com/gin-gonic/gin)

            |
            |

go get github.com/john/gin 到本地保存在 $GOPATH/pkg/mod/github.com/john/gin

            |
            |

工程中引入 import "github.com/gin-gonic/gin(出错,其实 go get 时就出错了 因为 url 路径和 module name 不一致)

解决方案

这个问题肯定第一时间就发现了,因为那么多贡献者在 github 上提交 pr 。

replace github.com/gin-gonic/gin => $GOPATH/pkg/mod/github.com/john/gin@vx.y.z/

在 go.mod 中加入上面一行,执行 go mod tidy 即可

由此以往

go mod 从某种程度上可以看作 Golang 的包管理子系统,凡是使用过别的语言的包管理系统的都会发现,此次遇到的问题有点匪夷所思,就这么一个简单的场景,就需要特别处理,对于一个包管理工具来说,显得有点“不够聪明” 或者 “包袱过重”。

简述 golang 的包管理历史

  1. $GOPATH 时代。 $GOPATH 时代就是初代了,要求设置一个 $GOPATH 环境变量,其值为一个本地路径。之后 go get 的包都将放置在 $GOPATH 下某个约定的位置。

    $GOPATH 有两个问题:1. 当时包还没使用版本的概念,当使用指定版本时需要 hack 2. 全局安装,当多个功能需要依赖同一个包的不同版本时需要 hack

  2. 群雄时代 因为 $GOPATH 的问题,就出现了好几个三方包管理方案,最著名的是 dep,在当前工程中使用 vendor 目录保存依赖。百花齐放很好,dep 的方式深得我心。 这时候的问题是:一个工程需要依赖多个包时,每个包可能使用的包管理方式不一样,就需要处理很多不必要的问题。

  3. go mod 时代 就目前。

其他语言的包管理

  1. Python pip 使用可指定版本的全局安装的方式。但对于特别需求的工程,可以使用 venv 搭建单独的 python 环境,python 版本、包版本完全独立于系统,相当与增强型的 vendor。

  2. PHP composer 可指定版本安装到当前 vendor 子目录

  3. Node npm 可指定版本,可选择全局安装或当前目录安装,相当于同时支持全局和 vendor

基本不外乎这几种方式,当然 Golang 和这些语言的包管理有一个很大的不同,那就是 Golang 没有自己专属的包服务器,比如 Python 的 pypi.org;而是借助与 github.com。

这有几点问题,因为 github.com 不是为 Golang 包管理设计的,但因为 github.com 的灵活性和广泛的支持度,这不是大问题,只是需要 Golang mod 的作者按照约定的规则对提交有针对性的处理一下。

另外,Golang 代码的 push 同时也意味着包发布,这可能并不是某些作者希望的。

使用 github 作为包存放服务器不是问题,因为没几个人喜欢千篇一律的世界,这种想法以及想法的实施非常重要。 出现 fork 问题的重要原因还是因为对子路径的依赖,对子路径的依赖可能从某些角度出发是必要的,当最终它绝对不是必要的,所有的理由都是借口。

就比如 Golang1.18 将要出现的泛型,语法大致如 func [T]foo(a, b int) T {} 没错,泛型变量使用 [],当然开发团队还是有理由,非常合理的理由,总之会造成歧义。C++,Java,C# 哪个语法复杂度不必 Golang 高?但用<>就不会解析出歧义。于是,几乎是历史首次:泛型不以 <> 来标识,用云风的话来说:就用你们的眼球,来 parse 这些代码吧!同时我不得不认为:Golang 语法简洁的重要原因之一是他们工程师团队没有能力或懒得解析更复杂的语法。

最后

Golang 是一个很优秀的语言,他的优秀的重要原因是内置 goroutine 和 channel 以及支持多核的 goroutine 调度器,强类型的编译型语言,简洁的语法设计。这几点使 Golang 具有无限的潜力。

同时,Golang 也有一些不足(每个人都可以说一罗筐),在包管理和泛型标识这些理应很基本的问题上出现意外,对于 Golang 团队来说,显得业余了。