一. 理解 Go 的环境变量
GOROOT
该环境变量的值为 Go 语言的当前安装目录。
GOPATH
该环境变量的值为 Go 语言的工作区的集合(意味着可以有很多个)。工作区类似于工作目录。每个不同的目录之间用:分隔。
工作区是放置 Go 源码文件的目录。一般情况下,Go 源码文件都需要存放到工作区中。
工作区一般会包含3个子文件夹,自己手动新建以下三个目录:src 目录,pkg 目录,bin 目录。
Go
/home/halfrost/gorepo
├── bin
├── pkg
└── src
这里需要额外说的一点:关于 IDE 新建 Go 项目。IDE 在新建完 Go 的项目以后,会自动的执行 go get 命令去把相应的基础包拉过来,
在这个过程中会新建 bin、pkg、src 三个目录。不用 IDE 的同学,需要自己手动创建这三个目录。
上图是 Atom 的 go-plus 插件在一个新的项目打开的时候,自动 go get 的一些基础包。
bin 目录里面存放的都是通过 go install 命令安装后,由 Go 命令源码文件生成的可执行文件( 在 Mac 平台下是 Unix executable 文件,在 Windows 平台下是 exe 文件)。
注意:有两种情况下,bin 目录会变得没有意义。
当设置了有效的 GOBIN 环境变量以后,bin 目录就变得没有意义。
如果 GOPATH 里面包含多个工作区路径的时候,必须设置 GOBIN 环境变量,否则就无法安装 Go 程序的可执行文件。
pkg 目录是用来存放通过 go install 命令安装后的代码包的归档文件(.a 文件)。归档文件的名字就是代码包的名字。所有归档文件都会被存放到该目录下的平台相关目录中,即在 $GOPATH/pkg/$GOOS_$GOARCH 中,同样以代码包为组织形式。
这里有两个隐藏的环境变量,GOOS 和 GOARCH。这两个环境变量是不用我们设置的,系统就默认的。GOOS 是 Go 所在的操作系统类型,GOARCH 是 Go 所在的计算架构。平台相关目录是以
$GOOS_$GOARCH 命名的,Mac 平台上这个目录名就是 darwin_amd64。
src 目录是以代码包的形式组织并保存 Go 源码文件的。每个代码包都和 src 目录下的文件夹一一对应。每个子目录都是一个代码包。
这里有一个特例,命令源码文件并不一定必须放在 src 文件夹中的。
这里需要纠正一个错误的观点:“所有的 Go 的代码都要放在 GOPATH 目录下”(这个观点是错误的)
目前 Go 最新版1.8.3里面基本命令只有以下的16个。
Go
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
bug start a bug report
fix run go tool fix on packages
fmt run gofmt on package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet run go tool vet on packages
其中和编译相关的有 build、get、install、run 这4个。接下来就依次看看这四个的作用。
在详细分析这4个命令之前,先罗列一下通用的命令标记,以下这些命令都可适用的:
go run 命令只能接受一个命令源码文件以及若干个库源码文件(必须同属于 main 包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么 go run 命令就只会打印错误提示信息并退出,而不会继续执行。
这个命令具体干了些什么事情呢?来分析分析:
vim
YDZ ~/LeetCode_Go/helloworld/src/me $ go run -n helloworld.go
#
#
mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/helloworld -L $WORK -w -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
$WORK/command-line-arguments/_obj/exe/helloworld
这里可以看到创建了两个临时文件夹 _obj 和 exe,先执行了 compile 命令,然后 link,生成了归档文件.a 和 最终可执行文件,最终的可执行文件放在 exe 文件夹里面。命令的最后一步就是执行了可执行文件。
举个例子,生成的临时文件可以用 go run -work 看到,比如当前生成的临时文件夹是如下的路径:
vim
/var/folders/66/dcf61ty92rgd_xftrsxgx5yr0000gn/T/go-build876472071
打印目录结构:
vim
├── command-line-arguments
│ └── _obj
│ └── exe
│ └── helloworld
└── command-line-arguments.a
可以看到,最终go run命令是生成了2个文件,一个是归档文件,一个是可执行文件。command-line-arguments 这个归档文件是 Go 语言为命令源码文件临时指定的一个代码包。在接下来的几个命令中,生成的临时代码包都叫这个名字。
go run 命令在第二次执行的时候,如果发现导入的代码包没有发生变化,那么 go run 不会再次编译这个导入的代码包。直接静态链接进来。
vim
go run -a
加上-a的标记可以强制编译所有的代码,即使归档文件.a存在,也会重新编译。
如果嫌弃编译速度慢,可以加上-p n,这个是并行编译,n是并行的数量。n一般为逻辑 CPU 的个数。
vim
// 假设当前文件夹名叫 myGoRepo
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go
YDZ:~/helloworld/src/myGoRepo $ go build
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go myGoRepo
于是在当前目录直接生成了以当前文件夹为名的可执行文件( 在 Mac 平台下是 Unix executable 文件,在 Windows 平台下是 exe 文件)
我们先记录一下这个可执行文件的 md5 值
vim
YDZ ~/helloworld/src/myGoRepo $ md5 /Users/YDZ/helloworld/src/myGoRepo/myGoRepo
MD5 (/Users/YDZ/helloworld/src/myGoRepo/myGoRepo) = 1f23f6efec752ed34b9bd22b5fa1ddce
但是这种情况下,如果使用 go install 命令,如果 GOPATH 里面只有一个工作区,就会在当前工作区的 bin 目录下生成相应的可执行文件。如果 GOPATH 下有多个工作区,则是在 GOBIN 下生成对应的可执行文件。
咱们先接着刚刚 go build 继续操作。
vim
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go myGoRepo
YDZ:~/helloworld/src/myGoRepo $ go install
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go
执行完 go install 会发现可执行文件不见了!去哪里了呢?其实是被移动到了 bin 目录下了(如果 GOPATH 下有多个工作区,就会放在
GOBIN 目录下)。
vim
YDZ:~/helloworld/bin $ ls
myGoRepo
再来比对一下这个文件的 md5 值:
vim
YDZ ~/helloworld/bin $ md5 /Users/YDZ/helloworld/bin/myGoRepo
MD5 (/Users/YDZ/helloworld/bin/myGoRepo) = 1f23f6efec752ed34b9bd22b5fa1ddce
和 go build 命令执行出来的可执行文件完全一致。我们可以大胆猜想,是把刚刚 go build 命令执行出来的可执行文件移动到了 bin 目录下(如果 GOPATH 下有多个工作区,就会放在 GOBIN 目录下)。
那 go build 和 go install 究竟干了些什么呢?
这个问题一会再来解释,先来说说 go build。
go build 用于编译我们指定的源码文件或代码包以及它们的依赖包。,但是注意如果用来编译非命令源码文件,即库源码文件,go build 执行完是不会产生任何结果的。这种情况下,go build 命令只是检查库源码文件的有效性,只会做检查性的编译,而不会输出任何结果文件。
go build 编译命令源码文件,则会在该命令的执行目录中生成一个可执行文件,上面的例子也印证了这个过程。
go build 后面不追加目录路径的话,它就把当前目录作为代码包并进行编译。go build 命令后面如果跟了代码包导入路径作为参数,那么该代码包及其依赖都会被编译。
go run 的-a标记在 go build 这里同样奏效,go build 加了-a强制编译所有涉及到的代码包,不加-a只会编译归档文件不是最新的代码包。
go build 使用-o标记可以指定输出文件(在这个示例中指的是可执行文件)的名称。它是最常用的一个 go build 命令标记。但需要注意的是,当使用标记-o的时候,不能同时对多个代码包进行编译。
标记-i会使 go build 命令安装那些编译目标依赖的且还未被安装的代码包。这里的安装意味着产生与代码包对应的归档文件,并将其放置到当前工作区目录的 pkg 子目录的相应子目录中。在默认情况下,这些代码包是不会被安装的。
go build 常用的一些标记如下:
go build 命令究竟做了些什么呢?我们来打印一下每一步的执行过程。先看看命令源码文件执行了 go build 干了什么事情。
vim
#
#
mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
mv $WORK/command-line-arguments/_obj/exe/a.out helloworld
可以看到,执行过程和 go run 大体相同,唯一不同的就是在最后一步,go run 是执行了可执行文件,但是 go build 命令是把可执行文件移动到了当前目录的文件夹中。
打印看看生成的临时文件夹的树形结构
vim
.
├── command-line-arguments
│ └── _obj
│ └── exe
└── command-line-arguments.a
和 go run 命令的结构基本一致,唯一的不同可执行文件不在 exe 文件夹中了,被移动到了当前执行 go build 的文件夹中了。
在来看看库源码文件执行了 go build 以后干了什么事情:
vim
#
#
mkdir -p $WORK//Users/YDZ/Downloads/goc2p-master/src/pkgtool/_obj/
mkdir -p $WORK//Users/YDZ/Downloads/goc2p-master/src/
cd /Users/YDZ/Downloads/goc2p-master/src/pkgtool
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/_/Users/YDZ/Downloads/goc2p-master/src/pkgtool.a -trimpath $WORK -p _/Users/YDZ/Downloads/goc2p-master/src/pkgtool -complete -buildid cef542c3da6d3126cdae561b5f6e1470aff363ba -D _/Users/YDZ/Downloads/goc2p-master/src/pkgtool -I $WORK -pack ./envir.go ./fpath.go ./ipath.go ./pnode.go ./util.go
这里可以看到 go build 命令只是把库源码文件编译了一遍,其他什么事情都没有干。
再看看生成的临时文件夹的树形结构
vim
.
└── _
└── Users
└── YDZ
└── Downloads
└── goc2p-master
└── src
├── pkgtool
│ └── _obj
└── pkgtool.a
可以看到它的目录结构层级前段部分是该代码包所在本机的路径的相对路径。然后生成了归档文件 .a 文件。
go install 用于编译并安装指定的代码包及它们的依赖包。当指定的代码包的依赖包还没有被编译和安装时,该命令会先去处理依赖包。与 go build 命令一样,传给 go install 命令的代码包参数应该以导入路径的形式提供。并且,go build 命令的绝大多数标记也都可以用于
go install 命令。实际上,go install 命令只比 go build 命令多做了一件事,即:安装编译后的结果文件到指定目录。
安装代码包会在当前工作区的 pkg 的平台相关目录下生成归档文件(即 .a 文件)。
安装命令源码文件会在当前工作区的 bin 目录(如果 GOPATH 下有多个工作区,就会放在 GOBIN 目录下)生成可执行文件。
同样,go install 命令如果后面不追加任何参数,它会把当前目录作为代码包并安装。这和 go build 命令是完全一样的。
go install 命令后面如果跟了代码包导入路径作为参数,那么该代码包及其依赖都会被安装。
go install 命令后面如果跟了命令源码文件以及相关库源码文件作为参数的话,只有这些文件会被编译并安装。
go install 命令究竟做了些什么呢?我们来打印一下每一步的执行过程。
vim
#
#
mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
mkdir -p /Users/YDZ/Ele_Project/clairstormeye/bin/
mv $WORK/command-line-arguments/_obj/exe/a.out /Users/YDZ/Ele_Project/clairstormeye/bin/helloworld
前面几步依旧和 go run 、go build 完全一致,只是最后一步的差别,go install 会把命令源码文件安装到当前工作区的 bin 目录(如果 GOPATH 下有多个工作区,就会放在 GOBIN 目录下)。如果是库源码文件,就会被安装到当前工作区的 pkg 的平台相关目录下。
还是来看看 go install 生成的临时文件夹的结构:
vim
.
├── command-line-arguments
│ └── _obj
│ └── exe
└── command-line-arguments.a
结构和运行了 go build 命令一样,最终生成的文件也都被移动到了相对应的目标目录中。
在安装多个库源码文件时有可能遇到如下的问题:
Go
hc@ubt:~/golang/goc2p/src/pkgtool$ go install envir.go fpath.go ipath.go pnode.go util.go
go install: no install location for .go files listed on command line (GOBIN not set)
而且,在我们为环境变量 GOBIN 设置了正确的值之后,这个错误提示信息仍然会出现。这是因为,只有在安装命令源码文件的时候,命令程序才会将环境变量 GOBIN 的值作为结果文件的存放目录。而在安装库源码文件时,在命令程序内部的代表结果文件存放目录路径的那个变量不会被赋值。最后,命令程序会发现它依然是个无效的空值。所以,命令程序会同样返回一个关于“无安装位置”的错误。这就引出一个结论,我们只能使用安装代码包的方式来安装库源码文件,而不能在 go install 命令罗列并安装它们。另外,go install 命令目前无法接受标记-o以自定义结果文件的存放位置。这也从侧面说明了
go install 命令不支持针对库源码文件的安装操作。
如果在 go get 下载过程中加入-d 标记,那么下载操作只会执行下载动作,而不执行安装动作。比如有些非常特殊的代码包在安装过程中需要有特殊的处理,所以我们需要先下载下来,所以就会用到-d 标记。
还有一个很有用的标记是-u标记,加上它可以利用网络来更新已有的代码包及其依赖包。如果已经下载过一个代码包,但是这个代码包又有更新了,那么这时候可以直接用-u标记来更新本地的对应的代码包。如果不加这个-u标记,执行 go get 一个已有的代码包,会发现命令什么都不执行。只有加了-u标记,命令会去执行 git pull 命令拉取最新的代码包的最新版本,下载并安装。
命令 go get 还有一个很值得称道的功能——智能下载。在使用它检出或更新代码包之后,它会寻找与本地已安装 Go 语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装 Go 语言的版本是1.x,那么 go get 命令会在该代码包的远程仓库中寻找名为 “go1” 的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。
go get 常用的一些标记如下:
go get 命令究竟做了些什么呢?我们还是来打印一下每一步的执行过程。
vim
cd .
git clone https://github.com/go-errors/errors /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git submodule update –init –recursive
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git show-ref
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git submodule update –init –recursive
WORK=/var/folders/66/dcf61ty92rgd_xftrsxgx5yr0000gn/T/go-build124856678
mkdir -p $WORK/github.com/go-errors/errors/_obj/
mkdir -p $WORK/github.com/go-errors/
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/github.com/go-errors/errors.a -trimpath $WORK -p github.com/go-errors/errors -complete -buildid bb3526a8c1c21853f852838637d531b9fcd57d30 -D _/Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors -I $WORK -pack ./error.go ./parse_panic.go ./stackframe.go
mkdir -p /Users/YDZ/Ele_Project/clairstormeye/pkg/darwin_amd64/github.com/go-errors/
mv $WORK/github.com/go-errors/errors.a /Users/YDZ/Ele_Project/clairstormeye/pkg/darwin_amd64/github.com/go-errors/errors.a
这里可以很明显的看到,执行完 go get 命令以后,会调用 git clone 方法下载源码,并编译,最终会把库源码文件编译成归档文件安装到 pkg 对应的相关平台目录下。
一般情况下,为了分离自己与第三方的代码,我们会设置两个或更多的工作区。我们现在有一个目录路径为 /home/hc/golang/lib 的工作区,并且它是环境变量 GOPATH 值中的第一个目录路径。注意,环境变量 GOPATH 中包含的路径不能与环境变量GOROOT的值重复。好了,如果我们使用 go get 命令下载和安装代码包,那么这些代码包都会被安装在上面这个工作区中。我们暂且把这个工作区叫做
Lib 工作区。
如果使用 vendor 管理依赖的话,常用命令是:
go get -u -x -a github.com/golang/geo/s2
rm -rf Godeps vendor && make dep
三. 静态链接 or 动态链接 ?
Go 在最初刚刚发布的时候,静态链接被当做优点宣传,只须编译后的一个可执行文件,无须附加任何东西就能部署。将运行时、依赖库直接打包到可执行文件内部,简化了部署和发布的操作,无须事先安装运行环境和下载诸多第三方库。不过最新版本却又加入了动态链接的内容了。
普通的 go build 、go install 用的都是静态链接
目前最新版的 Go 是如何支持动态链接的呢?
在 go build 、go install 的时候加上 -buildmode 参数。
这些是以下 buildmode 的选项:
archive: 将非 main 包构建为 .a 文件 . main 包将被忽略。
c-archive: 将 main 软件包及其导入的所有软件包构建到 C 归档文件中
c-shared: 将列出的主要软件包,以及它们导入的所有软件包构建到
C 动态库中。
shared: 将所有列出的非 main 软件包合并到一个动态库中。
exe: 构建列出的 main 包及其导入到可执行文件中的一切。 将忽略未命名为 main 的包。
默认情况下,列出的 main 软件包内置到可执行文件中,列出的非
main 软件包内置到 .a 文件中。