记一次go get安装hugo的坑、go module、goinstall、gobuild。

该文章的golang版本为 go version go1.14.5 darwin/amd64

心路历程

1. 报错

晚上原本想装个hugo玩玩,hugo提供了挺多安装方法好比最直接的二进制包安装、homebrew等包管理安装、docker,正好电脑里也有golang的环境,平时装go写的软件也很方便go get就解决了,天然而然想到go get -v github.com/gohugoio/hugo,看着控制台Download了半天最后居然给我报了个错:html

package github.com/jdkato/prose/transform: cannot find package "github.com/jdkato/prose/transform" in any of:
        /usr/local/Cellar/go/1.14.5/libexec/src/github.com/jdkato/prose/transform (from $GOROOT)
        /Users/jabin/go/src/github.com/jdkato/prose/transform (from $GOPATH)

大概意思就是找不到github.com/jdkato/prose/transform这个包,我下意识点开https://github.com/jdkato/prose/transform一探究竟,发现github确实报了404错误,说明这个包确实有点问题。git

2. 尝试

而后我寻思着不对劲怎么装都装不上,打开hugo的github仓库看到了这样的向导:github

mkdir $HOME/src
cd $HOME/src
git clone https://github.com/gohugoio/hugo.git
cd hugo
go install

我不信邪,就试了一下先clonego install会怎么样,而后发现居然成功了,在golang的bin目录(在个人电脑下是~/go/bin)下生成了一个名为hugo的可执行文件。golang

3. 排错

我寻思着go get是把项目先拉下来而后编译放在~/go/bin下,go install是本身手动进入了某个项目目录下执行编译输出到~/go/bin,两者应该只有本身把项目拉下来而后进入该项目的区别,怎么会出现这样奇怪的错误。docker

3.1 灵异事件

一遍思考一遍手上又执行了一遍go get -v github.com/gohugoio/hugo,这一遍居然什么报错也没有,我赶忙删掉~/go/bin下的编译产物hugo,检验是否真的go get可以正常编译出可执行文件了,结果是确实没有报错且正常编译出结果了缓存

我百思不得其解,可是回想一开始获得的错误,彷佛错误中提到了两个路径:闭包

  • /usr/local/Cellar/go/1.14.5/libexec/src/github.com/jdkato/prose/transform (from $GOROOT)
  • /Users/jabin/go/src/github.com/jdkato/prose/transform

我逐一检查后发现确实两个地址都没法找到须要的这个包,因而我开始想这个项目是否是有什么问题,熟悉go包和github地址的人应该清楚,这个地址说明这个包的做者是jdkato,项目名是prosetransform应该是项目下的一个文件夹。app

3.2 计算机不会耍赖皮

写文章时 https://github.com/jdkato/prose的最新commit是 c2b2f78b870e41bec89843648b04b1716a0fb9c6
hugo的github仓库的最新commit是 c84ad8db821c10225c0e603c6ec920c67b6ce36f

因而我开始在github搜jdkato这个名字,发现了这个做者确实有prose这个项目https://github.com/jdkato/prose,点进去一看发现这个项目也的的确确没有transform这个文件夹。函数

再翻看hugo的github仓库发现最新的release是三天前,说明hugo的代码自己没有什么问题。ui

这时候我得出一种推论,hugo可能用了prose这个项目的老版本,老版本里存在transform这个文件夹,hugo多是本身的golang环境里缓存了prose这个项目的老版本之类的。可是很快这种想法就被我本身推翻了,由于个人的确确在本身的电脑上go install成功了hugo的最新源码,而且在go install成功以前我还处于package github.com/jdkato/prose/transform: cannot find 的错误中,根本不存在缓存了老版本项目代码的可能。

3.3 真相只有一个

推测到这里,我想起来一个重要的东西,go.mod正是那个能够缓存旧版本的关键人物啊!我赶忙再翻看hugo的github仓库,果真发现了go.mod这个文件,里面也有这样一段记录:

module github.com/gohugoio/hugo

require (
    ...省略
    github.com/jdkato/prose v1.2.0
    ...省略
)

replace github.com/markbates/inflect => github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6

go 1.12

能够看到require中指出了对项目github.com/jdkato/prose的版本要求为v1.2.0,我啪的一下就打开了https://github.com/jdkato/prose/tree/v1.2.0,果真在v1.2.0这个旧版本的tag下有咱们想要的transform文件夹。

那么go get报错的真相就只有一个,go get获取了最新版本的github.com/jdkato/prose,正好最新版本的github.com/jdkato/prose重构了,项目结构发生了改变。而go install成功后go get也能成功的灵异事件也能说的过去了,就是由于go install获取到了对的版本的项目,致使go get也可以找到须要的文件了。

3.4 案件远没有结束

尽管得出了一个看起来讲的过去的解释,可是问题又来了:为何go install就能获取到对应的版本的项目?

我天然而然想到了go.mod这个关键人物,我一样得出一个大胆的推论:go get不会理会go.mod中的限制,而go install则会在乎。

3.5 口说无凭,怎么证实

粗略了翻阅了有关go getgo install的一些说明,发现没什么和我这个问题相关的,只好来点硬核的了,正好go也是自举的,go语言的go语言源码仍是能够看看的。

3.6 源码中的线索

git clone https://github.com/golang/go.git
cd go
code .

上来直接一个闪电三连码,用命令行打开我习惯的vscode开始看。

经过搜索找到了go getgo install的源码,分别位于

命令 源码位置
go get ./go/internal/get/get.go
go install ./go/internal/work/build.go

为何go install不是在install.go里?根据个人观察,由于go install和go build的功能和实现都很接近,因此这两个命令的源码都在/go/internal/work/build.go。

主要看看get.go的源码,里面有一个runGet方法,这是执行go get的入口方法,该方法中存在这样的调用链:
load.PackagesForBuild->PackagesAndErrors->ImportPaths

方法名 源码位置
load.PackagesForBuild .go/internal/load/pkg.go
PackagesAndErrors .go/internal/load/pkg.go
ImportPaths .go/internal/load/pkg.go

这个ImportPaths方法完整以下:

func ImportPaths(args []string) []*search.Match {
    if ModInit(); cfg.ModulesEnabled {
        return ModImportPaths(args)
    }
    return search.ImportPaths(args)
}

可一看到源码中关于ModInit(); cfg.ModulesEnabled是否成立有两种处理模式。

3.7 关键信息

由于源码里对于ModInit()只是简单的var ModInit func(),没有方法体能够看到,也许是再别的地方进行了实现,我也没有深究。主要是看到了cfg.ModulesEnabled这个变量,直接想到了go module是否生效的问题,因而百度了go module而且看到了这篇文章[go module 基本使用](https://www.cnblogs.com/chnmig/p/11806609.html),里面有这样一段话:

go在1.13版本默认是auto, 表明 当项目在 GOPATH/src 外且项目根目录有 go.mod 文件时,开启 go module.也就是说,若是你不把代码放置在 GOPATH/src 下则默认使用 MODULE 管理.
很差意思看错了,1.13+的版本判断开不开启MODULE的依据是根目录下有没有go.mod文件
咱们也可手动更改成 on(所有开启)/off(所有不开启)

我恍然大悟,由于我go get的地方正好就是随便一个地方->没有在项目里->天然没有go.mod->也就没有开启module->ModInit(); cfg.ModulesEnabled不成立->go get源码进入了另外一种处理方式->hugo源码中的go.mod没有生效,一切都解释的通了。

3.8 验证

验证的方法也很简单,根据前文的判断关键,新建一个有go.mod的环境再执行一次go get就能够了。

mkdir testgo
cd testgo 
go mod init xxx
go: creating new go.mod: module xxx
go get -v github.com/gohugoio/hugo

在有go.mod存在的环境下是能够安装成功的,也证实了前面的说法,至此一切都解决了。

4. 结尾

此次的问题也花了一整个下午的时间去排查,先是花了一些时间思考可能的缘由,又花了很长时间去看源码,读源码真的花了很长很长的时间,一连看了几个小时直到头都有些晕了在起身去吃饭,吃完饭回来有精神了继续看了一会就发现问题了,可能在文章中就是简单的调用链路和几行关键代码,可是怎么在上千行的源码中找到这些关键信息远没有文章中的那么简单,首先是要看懂,而后是顺着调用链往下继续看懂,有时候还会误解而后找错方向找了好久……

但结果仍是很是好的,解决了本身的疑惑,更难能的是在源码里学到了几个颇有意思很妙的写法,好比这两个:

// 这个else if的顺序和做用域利用的很充分
func CleanPatterns(patterns []string) []string {
    // 省略
    var out []string
    for _, a := range patterns {
        var p, v string
        if build.IsLocalImport(a) || filepath.IsAbs(a) {
            p = a
        } else if i := strings.IndexByte(a, '@'); i < 0 {
            p = a
        } else {
            p = a[:i]
            v = a[i:]
        }
        // 省略
     }
     // 省略
}
// 匿名函数+函数变量+递归+闭包的妙用
func PackageList(roots []*Package) []*Package {
    seen := map[*Package]bool{}
    all := []*Package{}
    var walk func(*Package)
    walk = func(p *Package) {
        if seen[p] {
            return
        }
        seen[p] = true
        for _, p1 := range p.Internal.Imports {
            walk(p1)
        }
        all = append(all, p)
    }
    for _, root := range roots {
        walk(root)
    }
    return all
}

写的有点长了,但仍是想完整的记录一下心路历程,折腾的过程仍是颇有意思的。

相关文章
相关标签/搜索