zoukankan      html  css  js  c++  java
  • Golang高阶:Golang1.5到Golang1.12包管理

    Golang1.5到Golang1.12包管理

    1. 前言

    Golang 是一门到如今有十年的静态高级语言了,2009年的时候算是正式推出了,然后到最近的一两年,2017-2018年的时候,突然直线上升,爆火了,得益于容器化运维/直播/短视频/区块链...

    Golang 语法简单,简单即是复杂,软件构建的核心在于将复杂的东西简单化,处理好复杂度。

    作为一个 gopher,我们要知道他的包管理,这样才能合理化代码结构,做好工程管理。(gopher:地鼠)

    2. GOPATH/ Golang 1.5之前

    Golang 的包管理一直让人口病,一开始它用 GOPATH 来进行依赖库管理,特别简单粗暴。

    如果环境变量:

    export GOROOT=/home/love/go
    export GOPATH=/home/love/code
    export GOBIN=$GOROOT/bin
    

    上面 GOROOT 是指 Golang编译器以及其工具链,基础源码库所在的目录, GOPATH 是用户自定义的代码所在位置。

    以下 GOPATH 的结构如下:

    ├── src
        └── github.com
            └── hunterhug
                └── rabbit
                    └── a
                        └── a.go
                    └── main.go
    ├── bin
    ├── pkg 
    

    我们写的开发包有简单易懂的路径之分,比如我的包叫 github/hunterhug/rabbit,那么结构如上面一样。

    我们进入到 rabbit 目录,main.go 代码:

    package main
    import "github/hunterhug/rabbit/a"
    
    func main(){
      ...
    }
    

    然后 go build 的话,找包时,就会从 GOPATH src 下面开始找,比如 rabbit 包下的 main.go 依赖了 github/hunterhug/rabbit/a,那么它首先从 src 下面按路径往下拼接查找,然后就找到了,最后生成和包名 github/hunterhug/rabbit 一样的一个叫 rabit 的二进制。

    如果我们 go install的话,这个二进制就会保存在 GOBIN 下(如果不存在 GOBIN,会保存在 GOPATH bin 下)。如果我们要编译时,缺少包,那么 go get -v 将会下载依赖包源码到 GOPATH src 下,然后在 GOPATH pkg 目录下生成该包的静态库(下次用就不用再从源码编译了,算缓存)。

    但是我们包找不到时:

    love@love:~/code/src/github.com/hunterhug/fafacms$ go build
    core/server/server.go:4:2: cannot find package "github.com/gin-contrib/cors" in any of:
            /home/love/go/src/github.com/gin-contrib/cors (from $GOROOT)
            /home/love/code/src/github.com/gin-contrib/cors (from $GOPATH)
    

    我们发现,原来,其实是先去 GOROOT 下找包,找不到包,再去 GOPATH 找,流下了感动的泪水!比如我们的 GOPATH 下建了一个 fmt 包:

    package fmt
    
    func PPrintln() {
    	print("i am diy fmt")
    }
    
    

    但是我们想引用这个库,main.go 使用:

    package main
    import fmt
    func main(){
      fmt.PPrintln()
    }
    

    发现引用不了,2333! 所以,GOPATH 下的包最好不要和 GOROOT 下的标准库重名!

    你再看下 GOROOT 的结构:

    ├── src
        └── time
        └── fmt
    ├── bin
    ├── pkg 
    

    这不和我们的 GOPATH 很像吗,对,现在的 Golang编译器 是自编译的,就是用 Golang 来写 Golang编译器,它的编译器及中间产物,基础库等,保持和 GOPATH 一毛一样,无缝衔接。

    但是不同依赖包是有版本的,版本变了怎么办?这就需要人工管理了。

    3. Golang vendor/Golang1.5以后

    自己管理库版本,想想都不太可能,毕竟 Javamaven, Pythonpip, PHPcomposeNodeJsnpm

    于是从 Golang1.5 开始推出 vendor 文件夹机制( vendor:供应商/小贩)。

    Golang1.6 正式开启这个功能。

    比如我们的包叫 awesomeProject,在 GOPATH 下结构:

    ├── src
        └── awesomeProject
            └── vendor
                └── fmt
                    └── fmt.go
            └── main.go
    ├── pkg 
    

    其中 main.go

    package main
    
    import "fmt"
    
    func main() {
    	fmt.PPrintln()
    }
    

    我们进入 awesomeProject 目录,并且 go build, 偶也成功。

    这下子不会像上面没 vendor 时直接引用 GOROOT 的标准包了,我们终于可以用和标准包重名的包了,那就是放在和 main.go 同目录的 vendor 下面!

    这下子,我们 import 的包会先在同级 vendor 下找,找不到再按照以前的方式。

    如果我们将 main 改成引用一个不存在的包 b:

    package main
    
    import (
    	"b"
    )
    
    func main() {
    	b.P()
    }
    

    然后 go build 提示:

    main.go:4:2: cannot find package "b" in any of:
            /home/love/code/src/awesomeProject/vendor/b (vendor tree)
            /home/love/go/src/b (from $GOROOT)
            /home/love/code/src/b (from $GOPATH)
    

    如果此时我们再任性一点,在 GOPATH src 下建立一个空的 vendor 文件夹,则会提示:

    main.go:4:2: cannot find package "b" in any of:
            /home/love/code/src/awesomeProject/vendor/b (vendor tree)
            /home/love/code/src/vendor/b
            /home/love/go/src/b (from $GOROOT)
            /home/love/code/src/b (from $GOPATH)
    

    好了,我们发现现在的加载方式是:

    包同目录下的vendor
    GOPATH src 下的vendor
    GOROOT src
    GOPATH src
    

    如果在 GOROOTGOPATH 下建 vendor 会怎么样?我们就不止疼了,233。。

    好了,现在问题就是 vendor 是怎么冒泡的,如果我 main.go 引用了 vendor/b,而 b 包里面引用了一个 c 包。此时 vendor/b 会怎么找库?

    ├── src
        └── awesomeProject
            └── vendor
                └── b
                    └── b.go
            └── main.go
    ├── pkg 
    

    现在 vendor/b/b.go 的内容:

    package b
    
    import "c"
    
    func P() {
    	print(" i am vendor b
    ")
    	c.P()
    }
    

    我们进入 awesomeProject 项目 go build,出现:

    vendor/b/b.go:3:8: cannot find package "c" in any of:
    	/home/love/code/src/awesomeProject/vendor/c (vendor tree)
    	/home/love/code/src/vendor/c
    	/home/love/go/src/c (from $GOROOT)
    	/home/love/code/src/c (from $GOPATH)
    

    现在加载流程是:

    包同目录的包(即b包同目录看看有没有c包)
    GOPATH src 下的vendor
    GOROOT src
    GOPATH src
    

    此时我们在 vendor/b 下建一个空 vendor

    ├── src
        └── awesomeProject
            └── vendor
                └── b
                    └── vendor
                    └── b.go
            └── main.go
    ├── pkg 
    

    进入 awesomeProject 项目再 go build 会出现:

    vendor/b/b.go:3:8: cannot find package "c" in any of:
    	/home/love/code/src/awesomeProject/vendor/b/vendor/c (vendor tree)
    	/home/love/code/src/awesomeProject/vendor/c
    	/home/love/code/src/vendor/c
    	/home/love/go/src/c (from $GOROOT)
    	/home/love/code/src/c (from $GOPATH)
    

    如果我们再满足上面的 c 包,同理在 c 包建一个空 vendor

    ├── src
        └── awesomeProject
            └── vendor
                └── b
                    └── vendor
                        └── c
                            └── vendor
                            └── c.go
                    └── b.go
            └── main.go
    ├── pkg 
    

    cc.go 引用了不存在的 d 包:

    package c
    
    import "d"
    
    func P() {
    	d.P()
    }
    
    

    进入 awesomeProject 项目再 go build 会出现:

    vendor/b/vendor/c/c.go:3:8: cannot find package "d" in any of:
    	/home/love/code/src/awesomeProject/vendor/b/vendor/c/vendor/d (vendor tree)
    	/home/love/code/src/awesomeProject/vendor/b/vendor/d
    	/home/love/code/src/awesomeProject/vendor/d
    	/home/love/code/src/vendor/d
    	/home/love/go/src/d (from $GOROOT)
    	/home/love/code/src/d (from $GOPATH)
    

    发现, 查找包 vendor 是往上冒泡的, 一个包引用另一个包,先看看 同目录 vendor 下有没有这个包, 没有的话一直追溯到上一层 vendor 看有没有,没有的话再上一层
    vendor,直到 GOPATH src/vendor

    所以现在的加载流程是:

    包同目录下的vendor
    包目录向上的最近的一个vendor
    ...
    GOPATH src 下的vendor
    GOROOT src
    GOPATH src
    

    总结: vendor 向上冒泡!!!!

    这样的话, 我们可以把包的依赖都放在 vendor 下,然后提交到仓库,这样可以省却拉取包的时间,并且相对自由,你想怎么改都可以,你可以放一个已经被人删掉的 github 包在 vendor 下。这样,依然手动,没法管理依赖版本。

    所以很多第三方,比如 glide , godep, govendor 工具出现了, 使用这些工具, 依赖包必须有完整的 git 版本, 然后会将所有依赖的版本写在一个配置文件中。

    比如 godep

    go get -v github.com/tools/godep
    

    在包下执行

    godep save 
    

    会生成 Godeps/Godep.json记录依赖版本,并且将包收集于 当前vendor下。

    3. go mod/Go1.11以后

    Golang 1.11 开始, 实验性出现了可以不用定义 GOPATH 的功能,且官方有 go mod 支持。Golang 1.12 更是将此特征正式化。

    现在用 Golang1.12 进行:

    go mod init
    go: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'
    

    其中 GO111MODULE=auto 是一个开关,开启或关闭模块支持,它有三个可选值: off/on/auto,默认值是 auto

    1. GO111MODULE=off,无模块支持,和之前一样。
    2. GO111MODULE=on,模块支持,忽略 GOPATHvendor 文件夹,只根据 go.mod下载依赖。
    3. GO111MODULE=auto,该项目在 GOPATH src 外面且根目录有 go.mod 文件时,开启模块支持。

    在使用模块的时候, GOPATH 是无意义的,不过它还是会把下载的依赖储存在 GOPATH/src/mod 中,也会把 go install 的结果放在 GOPATH/bin(如果 GOBIN 不存在的话)

    我们将项目移出 GOPATH,然后:

    go mod init
    

    出现:

    go: cannot determine module path for source directory /home/love/awesomeProject (outside GOPATH, no import comments)
    

    现在 main.go 改为:

    package main // import "github.com/hunterhug/hello"
    
    import (
    	"b"
    )
    
    func main() {
    	b.P()
    }
    

    将会生成 go.mod:

    module github.com/hunterhug/hello
    
    go 1.12
    

    此时我们:

    go build
    build github.com/hunterhug/hello: cannot load b: cannot find module providing package b
    

    这下没法查找 vendor 了,我们加上参数再来:

    go build -mod=vendor
    build github.com/hunterhug/hello: cannot load c: open /home/love/awesomeProject/vendor/c: no such file or directory
    

    流下了感动的泪水, vendor 冒泡呢?原来启用了 go.modvendor 下的包 b 无法找到b/vendor 下的包 c,只能找到一级,2333333,这是好还是坏?

    一般情况下, vendor 下面有 vendor 是不科学的, godep 等工具会将依赖理顺,确保只有一个 vendor

    那么 go.mod 导致 vendor 无法冒泡产生的影响,一点都不大,流下感动的泪水。

    现在我们来正确使用 go mod, 一般情况下:

    省略N步
    

    到了这里,我们很遗憾的说再见了,现在 go mod 刚出来, 可能还会再更新,您可以谷歌或者其他方式搜索这方面的文章,或者:

    go help modules
    

    这一部分可能隔一段时间再细写。

    目前生产环境用 go mod 还不太现实, 我还是先推荐定义 GOPATHvendor 用法。

    4. 使用 Docker 来多阶段编译 Golang

    装环境太难, 我的天啊, 我每次都要装环境, 我们可以用下面的方法 So easy 随时切换 Golang 版本。

    如果你的 Golang 项目依赖存于 vendor 下,那么我们可以使用多阶段构建并打包成容器镜像,Dockefile 如下:

    FROM golang:1.12-alpine AS go-build
    
    WORKDIR /go/src/github.com/hunterhug/fafacms
    
    COPY core /go/src/github.com/hunterhug/fafacms/core
    COPY vendor /go/src/github.com/hunterhug/fafacms/vendor
    COPY main.go /go/src/github.com/hunterhug/fafacms/main.go
    
    RUN go build -ldflags "-s -w" -o fafacms main.go
    
    FROM alpine:3.9 AS prod
    
    WORKDIR /root/
    
    COPY --from=go-build /go/src/github.com/hunterhug/fafacms/fafacms /bin/fafacms
    RUN chmod 777 /bin/fafacms
    CMD /bin/fafacms $RUN_OPTS
    

    其中 github.com/hunterhug/fafacms 是你的项目。使用 golang:1.12-alpine 来编译二进制,然后将二进制打入基础镜像:alpine:3.9,这个镜像特别小。

    编译:

    sudo docker build -t hunterhug/fafacms:latest .
    

    我们多了一个镜像 hunterhug/fafacms:latest , 而且特别小, 才几M 。

    运行:

    sudo docker run -d  --net=host  --env RUN_OPTS="-config=/root/fafacms/config.json" hunterhug/fafacms
    

    可是,如果我们用了 cgo, 那么请将 Dockerfile 改为:

    FROM golang:1.12 AS go-build
    
    WORKDIR /go/src/github.com/hunterhug/fafacms
    
    COPY core /go/src/github.com/hunterhug/fafacms/core
    COPY vendor /go/src/github.com/hunterhug/fafacms/vendor
    COPY main.go /go/src/github.com/hunterhug/fafacms/main.go
    
    RUN go build -ldflags "-s -w" -o fafacms main.go
    
    FROM bitnami/minideb-extras-base:stretch-r165 AS prod
    
    WORKDIR /root/
    
    COPY --from=go-build /go/src/github.com/hunterhug/fafacms/fafacms /bin/fafacms
    RUN chmod 777 /bin/fafacms
    CMD /bin/fafacms $RUN_OPTS
    

    5. 总结

    管理依赖,到如何将代码编译成二进制,是一个过程,还有许多细节。 上面是我的经验,感谢阅读。

  • 相关阅读:
    Java类与对象
    读《大道至简——团队缺乏的不只是管理》有感
    java课后作业
    c++ 创建单项链表
    c++ 结构指针和双向链表
    c++ 自定义数据结构运用
    c++ 时间函数和结构化数据
    c++ 结束程序的几种方式
    c++ main函数的参数
    c++ 参数个数可变的函数
  • 原文地址:https://www.cnblogs.com/nima/p/11751388.html
Copyright © 2011-2022 走看看