理解Go 1.5 vendor

Go 1.5中(目前最新版本go1.5beta3)加入了一个experimental feature: vendor/。这个feature不是Go 1.5的正式功能,但却是Go Authors们在解决Go被外界诟病的包依赖管理的道路上的一次重要尝试。目前关于Go vendor机制的资料有限,主要的包括如下几个:

1、Russ Cox在Golang-dev group上的一个名 为"proposal: external packages" topic上的reply。
2、Go 1.5beta版发布后Russ Cox根据上面topic整理的一个doc
3、medium.com上一篇名为“Go 1.5 vendor/ experiment"的文章。

但由于Go 1.5稳定版还未发布(最新消息是2015.8月中旬发布),因此估计真正采用vendor的repo尚没有。但既然是Go官方解决方案,后续从 expreimental变成official的可能性就很大(Russ的初步计划:如果试验顺利,1.6版本默认 GO15VENDOREXPERIMENT="1";1.7中将去掉GO15VENDOREXPERIMENT环境变量)。因此对于Gophers们,搞 清楚vendor还是很必要的。本文就和大家一起来理解下vendor这个新feature。

一、vendor由来

Go第三方包依赖和管理的问题由来已久,民间知名的解决方案就有godepgb等。这次Go team在推出vendor前已经在Golang-dev group上做了长时间的调研,最终Russ Cox在Keith Rarick的proposal的基础上做了改良,形成了Go 1.5中的vendor。

Russ Cox基于前期调研的结果,给出了vendor机制的群众意见基础:
    – 不rewrite gopath
    – go tool来解决
    – go get兼容
    – 可reproduce building process

并给出了vendor机制的"4行"诠释:

If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import "p" is interpreted as import "d/vendor/p" if that exists.

When there are multiple possible resolutions,the most specific (longest) path wins.

The short form must always be used: no import path can  contain “/vendor/” explicitly.

Import comments are ignored in vendored packages.

这四行诠释在group中引起了强烈的讨论,短小精悍的背后是理解上的不小差异。我们下面逐一举例理解。

二、vendor基本样例

Russ Cox诠释中的第一条是vendor机制的基础。粗犷的理解就是如果有如下这样的目录结构:

d/
   vendor/
          p/
           p.go
   mypkg/
          main.go

如果mypkg/main.go中有"import p",那么这个p就会被go工具解析为"d/vendor/p",而不是$GOPATH/src/p。

现在我们就来复现这个例子,我们在go15-vendor-examples/src/basic下建立如上目录结构(其中go15-vendor-examples为GOPATH路径):

$ls -R
d/

./d:
mypkg/    vendor/

./d/mypkg:
main.go

./d/vendor:
p/

./d/vendor/p:
p.go

其中main.go代码如下:

//main.go
package main

import "p"

func main() {
    p.P()
}

p.go代码如下:

//p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/p")
}

在未开启vendor时,我们编译d/mypkg/main.go会得到如下错误结果:

$ go build main.go
main.go:3:8: cannot find package "p" in any of:
    /Users/tony/.bin/go15beta3/src/p (from $GOROOT)
    /Users/tony/OpenSource/github.com/experiments/go15-vendor-examples/src/p (from $GOPATH)

错误原因很显然:go编译器无法找到package p,d/vendor下的p此时无效。

这时开启vendor:export GO15VENDOREXPERIMENT=1,我们再来编译执行一次:
$go run main.go
P in d/vendor/p

开启了vendor机制的go tool在d/vendor下找到了package p。

也就是说拥有了vendor后,你的project依赖的第三方包统统放在vendor/下就好了。这样go get时会将第三方包同时download下来,使得你的project无论被下载到那里都可以无需依赖目标环境而编译通过(reproduce the building process)。

三、嵌套vendor

那么问题来了!如果vendor中的第三方包中也包含了vendor目录,go tool是如何choose第三方包的呢?我们来看看下面目录结构(go15-vendor-examples/src/embeded):

d/
   vendor/
          p/
            p.go
          q/
            q.go
            vendor/
               p/
                 p.go
   mypkg/
          main.go

embeded目录下出现了嵌套vendor结构:main.go依赖的q包本身还有一个vendor目录,该vendor目录下有一个p包,这样我们就有了两个p包。到底go工具会选择哪个p包呢?显然为了验证一些结论,我们源文件也要变化一下:

d/vendor/p/p.go的代码不变。

//d/vendor/q/q.go
package q

import (
    "fmt"
    "p"
)

func Q() {
    fmt.Println("Q in d/vendor/q")
    p.P()
}

//d/vendor/q/vendor/p/p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/q/vendor/p")
}

//mypkg/main.go
package main

import (
    "p"
    "q"
)

func main() {
    p.P()
    fmt.Println("")
    q.Q()
}

目录和代码编排完毕,我们就来到了见证奇迹的时刻了!我们执行一下main.go:

$go run main.go
P in d/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

可以看出main.go中最终引用的是d/vendor/p,而q.Q()中调用的p.P()则是d/vendor/q/vendor/p包的实现。go tool到底是如何在嵌套vendor情况下选择包的呢?我们回到Russ Cox关于vendor诠释内容的第二条:

   When there are multiple possible resolutions,the most specific (longest) path wins.

这句话很简略,但却引来的巨大争论。"longest path wins"让人迷惑不解。如果仅仅从字面含义来看,上面main.go的执行结果更应该是:

P in d/vendor/q/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

d/vendor/q/vendor/p可比d/vendor/p路径更long,但go tool显然并未这么做。它到底是怎么做的呢?talk is cheap, show you the code。我们粗略翻看一下go tool的实现代码:

在$GOROOT/src/cmd/go/pkg.go中有一个方法vendoredImportPath,这个方法在go tool中广泛被使用

// vendoredImportPath returns the expansion of path when it appears in parent.
// If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path,
// x/vendor/path, vendor/path, or else stay x/y/z if none of those exist.
// vendoredImportPath returns the expanded path or, if no expansion is found, the original.
// If no expansion is found, vendoredImportPath also returns a list of vendor directories
// it searched along the way, to help prepare a useful error message should path turn
// out not to exist.
func vendoredImportPath(parent *Package, path string) (found string, searched []string)

这个方法的doc讲述的很清楚,这个方法返回所有可能的vendor path,以parentpath为x/y/z为例:

x/y/z作为parentpath输入后,返回的vendorpath包括:
   
x/y/z/vendor/path
x/y/vendor/path
x/vendor/path
vendor/path

这么说还不是很直观,我们结合我们的embeded vendor的例子来说明一下,为什么结果是像上面那样!go tool是如何resolve p包的!我们模仿go tool对main.go代码进行编译(此时vendor已经开启)。

根据go程序的package init顺序,go tool首先编译p包。如何找到p包呢?此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在p包,于是go tool将p包resolve为d/vendor/p,于是下面的p.P()就会输出:
P in d/vendor/p

接下来初始化q包。与p类似,go tool对main.go代码进行编译,此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在q包,于是乎go tool将q包resolve为d/vendor/q,由于q包自身还依赖p包,于是go tool继续对q中依赖的p包进行选择,此时go tool的编译对象变为了d/vendor/q/q.go,parent = d/vendor/q,于是经过vendordImportPath处理,可能的vendor路径为:

d/vendor/q/vendor
d/vendor/vendor
d/vendor

存在p包的路径包括:

d/vendor/q/vendor/p
d/vendor/p

此时按照Russ Cox的诠释2:choose longest,于是go tool选择了d/vendor/q/vendor/p,于是q.Q()中的p.P()输出的内容就是:
"
P in d/vendor/q/vendor/p"

如果目录结构足够复杂,这个resolve过程也是蛮繁琐的,但按照这个思路依然是可以分析出正确的包的。

另外vendoredImportPath传入的parent x/y/z并不是一个绝对路径,而是一个相对于$GOPATH/src的路径。

BTW,上述测试样例代码在这里可以下载到。

四、第三和第四条

最难理解的第二条已经pass了,剩下两条就比较好理解了。

The short form must always be used: no import path can  contain “/vendor/” explicitly.

这条就是说,你在源码中不用理会vendor这个路径的存在,该怎么import包就怎么import,不要出现import "d/vendor/p"的情况。vendor是由go tool隐式处理的。

Import comments are ignored in vendored packages.

go 1.4引入了canonical imports机制,如:

package pdf // import "rsc.io/pdf"

如果你引用的pdf不是来自rsc.io/pdf,那么编译器会报错。但由于vendor机制的存在,go tool不会校验vendor中package的import path是否与canonical import路径是否一致了。

五、问题

根据小节三中的分析,对于vendor中包的resolving过程类似是一个recursive(递归)过程。

main.go中的p使用d/vendor/p;而q.go中的p使用的是d/vendor/q/vendor/p,这样就会存在一个问题:一个工程中存 在着两个版本的p包,这也许不会带来问题,也许也会是问题的根源,但目前来看从go tool的视角来看似乎没有更好的办法。Russ Cox期望大家良好设计工程布局,作为lib的包不携带vendor更佳。

这样一个project内的所有vendor都集中在顶层vendor里面。就像下面这样:

d/
    vendor/   
            q/
            p/
            … …
    mypkg1
            main.go
    mypkg2
            main.go
    … …

另外Go vendor不支持第三方包的版本管理,没有类似godep的Godeps.json这样的存储包元信息的文件。不过目前已经有第三方的vendor specs放在了github上,之前Go team的Brad Fizpatrick也在Golang-dev上征集过类似的方案,不知未来vendor是否会支持。

六、vendor vs. internal

在golang-dev有人提到:有了vendor,internal似乎没用了。这显然是混淆了internal和vendor所要解决的问题。

internal故名思议:内部包,不是对所有源文件都可见的。vendor是存储和管理外部依赖包,更类似于external,里面的包都是copy自 外部的,工程内所有源文件均可import vendor中的包。另外internal在1.4版本中已经加入到go核心,是不可能轻易去除的,虽然到目前为止我们还没能亲自体会到internal 包的作用。

在《Go 1.5中值得关注的几个变化》一文中我提到过go 1.5 beta1似乎“不支持”internal,beta3发布后,我又试了试看beta3是否支持internal包。

结果是beta3中,build依旧不报错。但go list -json会提示错误:
"DepsErrors": [
        {
            "ImportStack": [
                "otherpkg",
                "mypkg/internal/foo"
            ],
            "Pos": "",
            "Err": "use of internal package not allowed"
        }
    ]

难道真的要到最终go 1.5版本才会让internal包发挥作用?

制作go-talks.appspot.com应用镜像

Go语言号称面向工程:对工程目录组织、代码风格(gofmt)、文档(生成)都制定的相应的“标准”,并提供了相应的工具帮助开发者满足这些工程specs。

gofmt用于格式化代码,形成统一代码风格。
godoc.org用于查看标准库或repo的doc。
go-talks.appspot.com则是用来查看go slide。

godocgo-talks这种以服务形式提供文档查看的形式不得不说是golang的又一创新。

这几年Golang的开发者们是非常勤奋的,为了推广Golang,他们撰写博客,编写文档,并四处布道,积累下许多有价值的文档,这些文档多以 Gopher所特有的present格式存在着,这些 present格式的文档以.slide、.article或.ext为后缀,通过go-talks.appspot.com提供的present渲染服 务浏览,并且支持github.com repo中的slide文件。Go开发者们只需要将自己写好的slide文件存放在自己github.com上的repo中,就可以随时随地在世界各地打 开这类present文件为大家布道了。

不过来到中国大陆后,事情就没那么顺利了,因为appspot.com在大陆是无法直接访问的,你懂得哦。为了观看这些大牛的slide,内地的Go程序员只能四处寻找出(fan)国(qiang)工具,但这毕竟不是十分方便。

上周末@开发者头条分享了“why Go is fast? [Slide] High performance servers without the event loop (Golang)”这个Dave Cheney在O'Reilly OSCON上分享的Go slide,但因为链接被qiang,无法直接观看。于是就想到能不能制作一个go-talks.appspot.com的镜像站点,让国内Go程序员也 能享受些福利呢?于是乎我就开始了镜像制作的探索过程。

一、在本地搭建go-talks.appspot.com镜像

present格式类似于markup,是一种标记语言,只是present格式更多用来制作slide。

golang.org/x/tools/present提供了present文件格式的解析库,最初本以为需要从头开始写server,并利用 present库解析,写模板和javascript实现类似翻页等功能呢。但后来居然在gddo repo,也就是godoc.org的源码工程中找到了go-talks.appsport.com站点的源码: talksapp。

不过talksapp是运行在google app engine上的应用,要将其直接运行在standalone server上是否可行呢?是否需要改造?这些都是未知数,不过有了源码自然是很好的。我们先来试试这个程序是否能在本地运行起来。

首先下载gddo repo:

$go get github.com/golang/gddo/
$cd $GOPATH/src/github.com/golang/gddo/talksapp

talksapp的主页文档似乎有些out-dated,我并没有找到config.go.template。   

但按照文档要求,需要下载Go App Engine SDK,这个需要搭梯子。在https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go页面根据您的平台版本下载最新Go SDK版本。解压后,先放在那里不动。

根据talksapp文档,第三步就应该是sh setup.sh。setup.sh中get两个repo均在qiang外,需要梯子才能下载。

setup.sh正确执行之后,我们用go_appengine下dev_appserver.py来运行talksapp:

$dev_appserver.py ~/Test/GoToolsProjects/src/github.com/golang/gddo/talksapp
INFO     2015-07-27 08:25:09,076 api_server.py:205] Starting API server at: http://localhost:51801
INFO     2015-07-27 08:25:09,080 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO     2015-07-27 08:25:09,083 admin_server.py:118] Starting admin server at: http://localhost:8000
/Users/tony/Test/GoToolsProjects/src/appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:115: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.
  'There are too many files in your application for '
ERROR    2015-07-27 08:25:11,941 http_runtime.py:380] bad runtime process port ['']
2015/07/27 08:25:11 secret.json needs to define ClientID and ClientSecret

使用浏览器访问localhost:8080,得到的页面中也只是有些错误日志,日志与上面最后两行相同。从错误日志来看,似乎需要配置一下secret.json这个文件,至少ClientID和ClientSecret不能为空。

我就随意配置两个值(这两个值似乎应该是github.com的账号和密码,用于OAuth2,如果随意配置无法成功,那建议配置上真实的账号和密码),看看是否可以访问:

{
    "ClientID": "xx",
    "ClientSecret": "yy"
}

这回再执行talksapp就不再报错了。用浏览器访问localhost:8080, go-talks的页面顺利正常显示出来!看来在本地是可以运行的哦!

我们再来测试一下访问github.com上的一个slide,地址如下:

http://localhost:8080/github.com/gophercon/2015-talks/Dmitry_Vyukov_-_Go_Dynamic_Tools/tools.slide

加载有些慢,有些时候提示:
  
   canceled: Deadline exceeded (timeout)

试了几次后,居然加载成功了!又试了几个slide,除了有些慢,都是成功的。看来talksapp是可以在standalone主机上运行的。

二、在vps上部署go-talks镜像

虽然在本机上可以正常浏览Golang大牛们的slide的了,但毕竟放在local上不是很方便,离开这台机器又无法访问了。广大内地go程序员们依旧 生活在“水深火热”中,在“分享经济”兴起的今天,我想也力所能及的做些贡献吧。于是想到了将这个镜像部署到我的blog vps上,这样大家就可以自由浏览golang slide了。

我的vps放在了DigitalOcean上(Ubuntu 14.04 server amd64),配置较低,平时仅仅作为blog托管主机。不过放一个go-talks镜像应该还是可以满足的,也可以更充分“压榨”一下DO的资源。

于是乎,我就按照上面的步骤将talksapp安装在了vps上。考虑到talksapp作为一个守护进程,又安装了supervisor对其进行管理:

/etc/supervisor/conf.d/go-talks.conf
[program:go-talks]
environment=GOROOT=/root/.bin/go142
environment=GOPATH=/root/go-talks
directory=/root/go-talks/src/github.com/golang/gddo/talksapp
command=/root/go-talks/go_appengine/goapp serve
autostart=true
autorestart=true
startsecs=3

这里没有使用dev_appserver.py,而是用了两位一个程序goapp,通过在talksapp目录下执行goapp serve来启动这个"GAE"服务。现在vps上启动了localhost:8080服务,但外面的人还是无法访问到这个服务。

如果要对外发布这个服务,我需要一个域名,考虑到自己已有的blog域名,为了快速开通服务,我添加了一个二级域名:go-talks.tonybai.com,模仿go-talks.appspot.com。

我们还需要调整一下apache2 server。原先的apache2 server只是为blog(wordpress)提供服务,现在我们需要将go-talks.tonybai.com映射到主机内部的8080端口服务 上,这就需要开启apache2的反向代理功能,对apache2也不是很熟悉,于是在网上找到了一段配置,补充到/etc/apache2 /apache2.conf中:

<VirtualHost *:80>
    ServerName go-talks.tonybai.com
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://localhost:8080/
    ProxyPassReverse / http://localhost:8080/
</VirtualHost>

Include /etc/phpmyadmin/apache.conf

重启apache2,出现下面错误:

root@tonybai:/etc/apache2# sudo service apache2 restart
 * Restarting web server apache2          [fail]
 * The apache2 configtest failed.
Output of config test was:
AH00526: Syntax error on line 85 of /etc/apache2/apache2.conf:
Invalid command 'ProxyPreserveHost', perhaps misspelled or defined by a module not included in the server configuration
Action 'configtest' failed.
The Apache error log may have more information.

似乎是反向代理需要更多apache2 module才能运行,于是:

sudo a2enmod proxy
sudo a2enmod proxy_http

再重启apache2,这回ok了。

在DNS服务商内已经添加了go-talks.tonybai.com这个域名,但由于国内DNS生效时间较慢,为了测试服务是否ok,我修改了 hosts文件,手动将go-talks.tonybai.com指向vps的公网地址。接下来访问go-talks.tonybai.com这个地址, 镜像制作成功了! 又测试了几个slide,均正确生成!速度稍慢,那是因为vps的一般延迟都在2600ms左右。

我的VPS性能不高,大家访问时也许会感觉较慢,但有胜于无!

最后再重申一下go-talks.tonybai.com的使用方法:

如果某个分享链接为:go-talks.appspot.com/xxx/yy/zz/foo.slide,那么将该地址替换为:go- talks.tonybai.com/xxx/yy/zz/foo.slide即可。也就是将appspot换成tonybai,其他不变。

该服务已经利用监控宝监控起来了,如果出现问题(比如网络或资源不足的问题),我会及时处理。但这里不保证100%可用哦!希望大家友好使用,不要拍砖!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats