依赖包

差点烂尾。但多亏齐老师提醒,我才意识到已经超过一周没有写文章了。最近手边事情有些多,所以没有精力做到日更。但我会努力更新下去,事乃成功归于足下,就这么一篇一篇的更下去,总有会收获的那天。

这篇文章聊聊golang依赖包的事情。从前四式可以看出,golang程序中很多依赖包都需要从github下载,还有一部分依赖包需要从golang.org中下载。github还好办些,至少GFW还会分时分段放行。但golang.org就完全扯淡了,GFW不给你丁点放行的机会。当你通过go get golang.org/xxx时,死的心都有。当你改不了世界,就想法去苟且吧。所以这篇文章就来给你个苟且方案。

苟且方案一:

所有的golang.org代码都同步在github.com/golang中,所以当你需要golang.org/某些数据时,就先go get github.com/golang/xxx。然后自行rename。虽然多了一些手动的步骤,但至少能让你code下去,所以算作方案一。如果不满足,就继续看苟且方案二。

苟且方案二:

在终端环境设置代理,推荐的代理就是大名鼎鼎的shadowsocks。让go get的请求都通过代理去翻墙。如果想看具体怎么设置,首先要有一个shadowsocks环境,然后建议看看我的视频https://youtu.be/YDSQrjsOV7I ,这个视频介绍了如何通过provixy和shadowsocks来为终端翻墙。这个方案优点在于完全自动化,几乎可以忽略GFW的存在,同时也可以代理其它终端工具。但也有缺点,就是依赖网速,网速慢了也是扯淡。在这个方案基础之上,也有苟且方案三。

苟且方案三:

如果你的服务器需要翻墙,我把苟且方案二最核心的provixy和shadowsocks cli封装成了docker镜像,直接启动就能用。镜像名称是vikings/shadowsocks, 如果有不明白的地方,可以发邮件或者在https://github.com/andy-zhangtao/AwesomeDockerfile 提issue。好了,下面是用golang来解决golang的苟且方案四。

苟且方案四:

方案四本质也是代理,但不走shadowsocks,只是用golang来写一个small proxy,然后只提供依赖包的下载功能。首先,我们来看看这个方案是怎么实现的。下面是大致的实现流程

Leadfoot Server是一个运行在golang开发环境中的应用程序,当用户需要执行go get时,就把请求发给Leadfoot,当Leadfoot接受到请求后,就会代替用户执行go get。然后将所有接受到的代码打包成zip文件。最后将这个打包文件返回给用户端,用户端执行unzip就有所有需要的依赖代码了。

综上所述,Leadfoot肯定会有配合使用的Server和Client。所以先来看Server端的实现。

在开始讲解之前,先看一个结构体.

  1. type Down struct {
  2. Path string `json:"path"`
  3. Md5 string
  4. }

这个结构体用来标示唯一的用户请求。因为每个请求都是无状态并且异步完成的,为了不重复下载数据,需要标示每个请求。如何使用,后面会提到。

当用户需要下载依赖包时,会调用server的下载API,下面是此API的具体实现.

  1. func Download(w http.ResponseWriter, r *http.Request) {
  2. data, err := ioutil.ReadAll(r.Body)
  3. if err != nil {
  4. Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
  5. return
  6. }
  7. d := &Down{}
  8. err = json.Unmarshal(data, d)
  9. if err != nil {
  10. Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
  11. return
  12. }
  13. md := md5.Sum([]byte(d.Path))
  14. d.Md5 = hex.EncodeToString(md[:len(md)])
  15. STATUS[d.Md5] = DOWNING
  16. err = GoGet(d)
  17. if err != nil {
  18. STATUS[d.Md5] = ERROR
  19. // ioutil.WriteFile(TEMP+"/"+d.Md5+".err", []byte(err.Error()), 777)
  20. Sandstorm.HTTPError(w, err.Error(), http.StatusInternalServerError)
  21. return
  22. }
  23. STATUS[d.Md5] = DOWNDONE
  24. }

客户端通过POST方法,在body中写入需要下载的数据。目前需要的数据就是path一个字段。

当Leadfoot接收到数据之后,计算出path相对应的md5值,然后使用md5作为key来存储状态(后面所有针对这个path的查询都已md5为准),并且标示为正在下载。

GoGet就用来下载path,下面是其实现

  1. func GoGet(d *Down) error {
  2. os.MkdirAll(TEMP+d.Md5+"/src", 0777)
  3. err := os.Setenv("GOPATH", TEMP+d.Md5)
  4. if err != nil {
  5. return err
  6. }
  7. cmd := exec.Command("go", "get", d.Path)
  8. // cmd.Env = append(cmd.Env, "$GOPATH=")
  9. var out bytes.Buffer
  10. cmd.Stdout = &out
  11. cmd.Stderr = &out
  12. err = cmd.Run()
  13. if err != nil {
  14. file, _ := os.Create(TEMP + "/" + d.Md5 + ".err")
  15. out.WriteTo(file)
  16. return err
  17. }
  18. // pack := strings.Split(d.Path, "/")[0]
  19. Zip(d.Md5, os.Getenv("GOPATH")+"/src/")
  20. return nil
  21. }

目前来说,GoGet没有使用特殊的解决方案,就是简单的调用系统命令来执行go get。其中有一个小技巧,就是每次下载时都重新设定GOPATH,这是因为每个请求所下载的数据都不同。如果不区分出这些请求,那么后面打包时,所有数据就都混为一谈了。因此有必要设置出隔离的小环境,而设置隔离就是通过GOPATH。如果忘记了GOPATH的用法,返回到第一式去看看吧。

当数据下载完成之后,就自动进行打包,也就是Zip函数.

  1. func Zip(md5, dir string) {
  2. tmpDir, err := ioutil.TempDir("/tmp", md5+"_zip_")
  3. if err != nil {
  4. panic(err)
  5. }
  6. defer func() {
  7. _ = os.RemoveAll("/tmp/" + md5)
  8. }()
  9. outFilePath := filepath.Join(tmpDir, md5+".zip")
  10. fmt.Println(outFilePath)
  11. progress := func(archivePath string) {
  12. fmt.Println(archivePath)
  13. }
  14. err = zip.ArchiveFile(dir, outFilePath, progress)
  15. if err != nil {
  16. panic(err)
  17. }
  18. DONE[md5] = outFilePath
  19. }

zip文件也是以md5来标示,这是因为如果下次有请求时,会尝试看看有没有同名的zip文件,方便节省资源。好了,下载阶段就完成了。

客户端只有当server端下载完成之后,才能开始下载zip文件。因此server端就会提供一个查询接口。也就是下面的函数:

  1. func Query(w http.ResponseWriter, r *http.Request) {
  2. pack := r.Header.Get("package")
  3. if pack == "" {
  4. Sandstorm.HTTPError(w, "I need a header named `package`", http.StatusInternalServerError)
  5. return
  6. }
  7. md := md5.Sum([]byte(pack))
  8. m := hex.EncodeToString(md[:len(md)])
  9. switch STATUS[m] {
  10. case ERROR:
  11. content, _ := ioutil.ReadFile(TEMP + "/" + m + ".err")
  12. Sandstorm.HTTPSuccess(w, "GO GET ERROR["+string(content)+"]")
  13. case DOWNING:
  14. Sandstorm.HTTPSuccess(w, "Package downloading... ")
  15. case DOWNDONE:
  16. Sandstorm.HTTPSuccess(w, CANDOWN)
  17. }
  18. return
  19. }

这里面可以做个优化,客户端可以使用md5作为查询请求,但目前使用的是path。server会根据path再计算一次md5,所有有些浪费资源。

当客户端接收到DOWNDONE之后,就可以拉取zip文件了。下面是拉取的API:

  1. func Pull(w http.ResponseWriter, r *http.Request) {
  2. //First of check if Get is set in the URL
  3. // Filename := request.URL.Query().Get("file")
  4. pack := r.Header.Get("package")
  5. if pack == "" {
  6. Sandstorm.HTTPError(w, "I need a header named `package`", http.StatusInternalServerError)
  7. return
  8. }
  9. md := md5.Sum([]byte(pack))
  10. Filename := DONE[hex.EncodeToString(md[:len(md)])]
  11. fmt.Println("Client requests: " + Filename)
  12. //Check if file exists and open
  13. Openfile, err := os.Open(Filename)
  14. defer Openfile.Close() //Close after function return
  15. if err != nil {
  16. //File not found, send 404
  17. http.Error(w, "File not found.", 404)
  18. return
  19. }
  20. //File is found, create and send the correct headers
  21. //Get the Content-Type of the file
  22. //Create a buffer to store the header of the file in
  23. FileHeader := make([]byte, 512)
  24. //Copy the headers into the FileHeader buffer
  25. Openfile.Read(FileHeader)
  26. //Get content type of file
  27. FileContentType := http.DetectContentType(FileHeader)
  28. //Get the file size
  29. FileStat, _ := Openfile.Stat() //Get info from file
  30. FileSize := strconv.FormatInt(FileStat.Size(), 10) //Get file size as a string
  31. //Send the headers
  32. w.Header().Set("Content-Disposition", "attachment; filename="+Filename)
  33. w.Header().Set("Content-Type", FileContentType)
  34. w.Header().Set("Content-Length", FileSize)
  35. //Send the file
  36. //We read 512 bytes from the file already so we reset the offset back to 0
  37. Openfile.Seek(0, 0)
  38. io.Copy(w, Openfile) //'Copy' the file to the client
  39. return
  40. }

这里使用了Http的GET方法。而因为依赖包的名称和url很相似,所以就放在了Header当中。因此第一步就是从Header中取出package名称。后面就是设置Header值,然后读取本地文件数据再写入ResponseWriter中。

这些就是Server的处理逻辑,相对于Server而言,Client就显得很简单了。首先调用DownloadAPI,然后定时查询QueryAPI,最后调用Pull API,当接受完数据之后,执行Unzip就可以了。因为Server打包数据时,是按照golang规范目录结构打包的,所以直接将文件unzip到GOPATH当中,并且选择覆盖旧数据,就完成更新了。

所有的源代码都保存在https://github.com/andy-zhangtao/Leadfoot 可以去上面看看所有的源码。当前我提供了一个Leadfoot Server,如果你想试用一下,可以执行下面的命令来测试:

  1. Leadfoot client -s http://leadfoot.openss.cc -p github.com/knq/chromedp

好了,Leadfoot就介绍到这里了。