go命令行工具是我们在写代码中常用的,我们这边做一个简单的整理。

go build

go build 命令主要是用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。

  1. 如果是普通包,当你执行go build命令后,不会产生任何文件。

  2. 如果是main包,当只执行go build命令后,会在当前目录下生成一个可执行文件。如果需要在$GOPATH/bin木下生成相应的可执行文件,需要执行go install 或者使用 go build -o 路径/可执行文件。

  3. 如果某个文件夹下有多个文件,而你只想编译其中某一个文件,可以在 go build 之后加上文件名,例如 go build a.go;go build 命令默认会编译当前目录下的所有go文件。

  4. 你也可以指定编译输出的文件名。比如,我们可以指定go build -o myapp,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。

  5. go build 会忽略目录下以”_”或者”.”开头的go文件。

  6. go build的时候会选择性地编译以系统名结尾的文件(Linux、Darwin、Windows、Freebsd)。例如linux系统下面编译只会选择array_linux.go文件,其它系统命名后缀文件全部忽略。

  7. go build -x -v 打印编译过程

golang中包的理解

定义:关键字 package XXXX

我们知道一个非main包在编译后会生成一个.a文件(在临时目录下生成,除非使用go install安装到$GOROOT或$GOPATH下,否则你看不到.a),用于后续可执行程序链接使用。

Go标准库中的包对应的源码部分路径在:$GOROOT/src,而标准库中包编译后的.a文件路径在$GOROOT/pkg/darwin_amd64下。

执行go install libproj1/foo,Go编译器编译foo包,并将foo.a安装到$GOPATH/pkg/darwin_amd64/libproj1下—不用先go build然后在go install

因此我们要依赖第三方包,就必须搞到第三方包的源码,这也是Golang包管理的一个特点。

编译main包时,编译器到底用的是.a还是源码?—-在使用第三方包的时候,当源码和.a均已安装的情况下,编译器链接的是源码。

最根本的是链接的是链接了以该最新源码编译的临时目录下的.a文件,而不是pkg下面的.a文件。—如果想依赖pkg下面的.a文件,那只能分布编译了,把6l链接时的-L $WORK 去掉,才会找到pkg下面(具体参考-X -V参数的编译原理)

标志库也是依赖源码编译产生的临时目录下的.a文件,但是当标准库的源码发生变化时,编译器不会尝试重新编译–但是第三方库发生变化时,会重新编译生成临时文件,然后连接

临时文件不是一直存在的,只是在编译的时候产生

import后面的是路劲名还是包名?

import后面的最后一个元素应该是路径,就是目录,并非包名。而调用的函数的那个是包名,所以源码路劲一定要存在,不然就can not find

引用包 import 文件所在的目录路劲(除去$GOPATH/src) 同一个目录下不能定义不同的package

import m "lib/math"

import语句用m替代lib/math m指代的是lib/math路径下唯一的那个包–一定是唯一,不然报错

交叉编译

mac和linux环境编译不兼容,不能互相运行。可以跨平台编译。

Golang 支持交叉编译,在一个平台上生成另一个平台的可执行程序,最近使用了一下,非常好用,这里备忘一下。

Mac 下编译 Linux 和 Windows 64位可执行程序

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go

Linux 下编译 Mac 和 Windows 64位可执行程序

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go

Windows 下编译 Mac 和 Linux 64位可执行程序

SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build main.go

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go
  • GOOS:目标平台的操作系统(darwin、freebsd、linux、windows)
  • GOARCH:目标平台的体系架构(386、amd64、arm)

交叉编译不支持 CGO 所以要禁用它

上面的命令编译 64 位可执行程序,你当然应该也会使用 386 编译 32 位可执行程序 很多博客都提到要先增加对其它平台的支持,但是我跳过那一步,上面所列的命令也都能成功,且得到我想要的结果,可见那一步应该是非必须的,或是我所使用的 Go 版本已默认支持所有平台。

go install

go install 命令在内部实际上分成了两步操作:

第一步是生成结果文件(可执行文件或者.a包)

第二步会把编译好的结果移到 $GOPATH/pkg 或者 $GOPATH/bin。

  • 可执行文件: 一般是 go install 带main函数的go文件产生的,有函数入口,所有可以直接运行。
  • .a应用包: 一般是 go install 不包含main函数的go文件产生的,没有函数入口,只能被调用。

静态库的使用

编译静态库demo.a

go install demo

在命令行运行go install demo命令,会在%GOPATH%目录下生相应的静态库文件demo.a(windows平台一般在%GOPATH%\src\pkg\windows_amd64目录)。

编译main.go

进入main.go所在目录,编译main.go:

go tool compile -I E:\share\git\go_practice\pkg\windows_amd64 main.go

-I选项指定了demo包的安装路径,供main.go导入使用,即E:\share\git\go_practice\pkg\windows_amd64目录,编译成功后会生成相应的目标文件main.o。

链接main.o

go tool link -o main.exe -L E:\share\git\go_practice\pkg\windows_amd64 main.o

-L选项指定了静态库demo.a的路径,即E:\share\git\go_practice\pkg\windows_amd64目录,链接成功后会生成相应的可执行文件main.exe。

运行main.exe

main.exe
call demo ...

现在,就算把demo目录删除,再次编译链接main.go,也能正确生成main.exe:

go tool compile -I E:\share\git\go_practice\pkg\windows_amd64 main.go
go tool link -o main.exe -L E:\share\git\go_practice\pkg\windows_amd64 main.o
main.exe
call demo ...

但是,如果删除了静态库demo.a,就不能编译main.go,如下:

go tool compile -I E:\share\git\go_practice\pkg\windows_amd64 main.go
main.go:3: can't find import: "demo"

以上就是go语言静态库的编译和使用方法.

动态库

将go语言标准库编译成动态库

$ go install -buildmode=shared -linkshared  std

在命令行运行go install -buildmode=shared -linkshared std命令,-buildmode指定编译模式为共享模式,-linkshared表示链接动态库,成功编译后会在$GOROOT目录下生标准库的动态库文件libstd.so,一般位于$GOROOT/pkg/linux_amd64_dynlink目录:

$ cd $GOROOT/pkg/linux_amd64_dynlink
$ ls libstd.so
libstd.so

将demo.go编译成动态库

$ go install  -buildmode=shared -linkshared demo
$ cd $GOPATH/pkg
$ ls linux_amd64_dynlink/
demo.a  demo.shlibname  libdemo.so

成功编译后会在$GOPATH/pkg目录生成相应的动态库libdemo.so。

以动态库方式编译main.go

$ go build -linkshared main.go
$ ll -h
total 25K
drwxrwx---. 1 root vboxsf 4.0K Apr 28 17:30 ./
drwxrwx---. 1 root vboxsf 4.0K Apr 28 17:22 ../
drwxrwx---. 1 root vboxsf    0 Apr 28 08:37 demo/
-rwxrwx---. 1 root vboxsf  16K Apr 28 17:30 main*
-rwxrwx---. 1 root vboxsf   58 Apr 28 08:37 main.go*
$ ./main
call demo ...

从示例中可以看到,以动态库方式编译生成的可执行文件main大小才16K。如果以静态库方式编译,可执行文件main大小为1.5M,如下所示:

$ go build main.go
$ ll -h
total 1.5M
drwxrwx---. 1 root vboxsf 4.0K Apr 28 17:32 ./
drwxrwx---. 1 root vboxsf 4.0K Apr 28 17:22 ../
drwxrwx---. 1 root vboxsf    0 Apr 28 08:37 demo/
-rwxrwx---. 1 root vboxsf 1.5M Apr 28 17:32 main*
-rwxrwx---. 1 root vboxsf   58 Apr 28 08:37 main.go*
$ ./main
call demo ...

以动态库方式编译时,如果删除动态库libdemo.so或者动态库libstd.so,运行main都会由于找不到动态库导致出错,例如删除动态库libdemo.so:

$ rm ../pkg/linux_amd64_dynlink/libdemo.so
$ ./main
./main: error while loading shared libraries: libdemo.so: cannot open shared object file: No such file or directory

以上就是go语言动态库的编译和使用方法,需要注意的是,其他go程序在使用go动态库时,必须提供动态库的源码,否则会编译失败。例如,这里将demo.go代码删除,再以动态库方式编译main.go时,会编译失败:

$ go install  -buildmode=shared -linkshared demo
$ rm demo/demo.go
$ go build -linkshared main.go
main.go:3:8: no buildable Go source files in /media/sf_share/git/go_practice/src/demo

动态库编译方式和静态库不一样,静态库可以不提供源码,直接使用静态库编译,而动态库不行。

go get

go get 命令主要是用来动态获取远程代码包的,目前支持的有BitBucket、GitHub、Google Code和Launchpad。这个命令在内部实际上分成了两步操作:

  • 第一步是下载源码包
  • 第二步是执行go install。

下载源码包的go工具会自动根据不同的域名调用不同的源码工具.

所以为了go get 能正常工作,你必须确保安装了合适的源码管理工具,并同时把这些命令加入你的PATH中。其实go get支持自定义域名的功能,具体参见go help remote。

go get 命令本质:首先通过源码工具clone代码到src目录,然后执行go install。

参数:

go get命令可以接受所有可用于go build命令和go install命令的标记。这是因为go get命令的内部步骤中完全包含了编译和安装这两个动作。另外,go get命令还有一些特有的标记,如下表所示:

标记名称    标记描述
-d  让命令程序只执行下载动作,而不执行安装动作。
-f  仅在使用-u标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里Fork过来的,那么这样做就尤为重要了。
-fix    让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。
-insecure   允许命令程序使用非安全的scheme(如HTTP)去下载指定的代码包。如果你用的代码仓库(如公司内部的Gitlab)没有HTTPS支持,可以添加此标记。请在确定安全的情况下使用它。
-t  让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。
-u  让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。

go test

go test 命令,会自动读取源码目录下面名为*_test.go的文件,生成并运行测试用的可执行文件。输出的信息类似

ok   archive/tar   0.011s
FAIL archive/zip   0.022s
ok   compress/gzip 0.033s
...

默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考go help testflag

单元测试

go语言的单元测试采用内置的测试框架,通过引入testing包以及go test来提供测试功能。

在源代码包目录内,所有以_test.go为后缀名的源文件被go test认定为测试文件,这些文件不包含在go build的代码构建中,而是单独通过 go test来编译,执行。 前置条件:

  • 文件名须以”_test.go”结尾
  • 每个测试函数必须导入testing包,方法名须以”Test”打头,并且形参为 (t *testing.T)

当运行go test命令时,go test会遍历所有的*_test.go中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

情景

测试单个文件,一定要带上被测试的原文件

go test -v  wechat_test.go wechat.go 

-v是显示出详细的测试结果, -cover 显示出执行的测试用例的测试覆盖率。
go test -v -cover=true ./src/utils/slice_test.go ./src/utils/slice.go

测试单个方法

go test -v wechat_test.go -test.run TestRefreshAccessToken

当测试整个utils包时,使用命令:

go test -v -cover=true ./src/utils/...

当测试单个测试用例时,使用命令:

#./src/utils为包utils的路径
go test -v -cover=true ./src/utils -run TestSuccessStringInSlice

工具

自动生成测试用例的工具gotests,以后有需要使用可以了解一下

基准测试

基准测试,是一种测试代码性能的方法,比如你有多种不同的方案,都可以解决问题,那么到底是那种方案性能更好呢?这时候基准测试就派上用场了。

基准测试主要是通过测试CPU和内存的效率问题,来评估被测试代码的性能,进而找到更好的解决方案。比如链接池的数量不是越多越好,那么哪个值才是最优值呢,这就需要配合基准测试不断调优了。

如何编写基准测试

基准测试代码的编写和单元测试非常相似,它也有一定的规则,我们先看一个示例。

itoa_test.go

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

这是一个基准测试的例子,从中我们可以看出以下规则:

  • 基准测试的代码文件必须以_test.go结尾
  • 基准测试的函数必须以Benchmark开头,必须是可导出的
  • 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  • 基准测试函数不能有返回值
  • b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
  • 最后的for循环很重要,被测试的代码要放到循环里
  • b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能

下面我们运行下基准测试,看看效果。

go test -bench=. -run=none
BenchmarkSprintf-8      20000000               117 ns/op
PASS
ok      flysnow.org/hello       2.474s

运行基准测试也要使用go test命令,不过我们要加上-bench=标记,它接受一个表达式作为参数,匹配基准测试的函数,.表示运行所有基准测试。

因为默认情况下 go test 会运行单元测试,为了防止单元测试的输出影响我们查看基准测试的结果,可以使用-run=匹配一个从来没有的单元测试方法,过滤掉单元测试的输出,我们这里使用none,因为我们基本上不会创建这个名字的单元测试方法。

下面着重解释下说出的结果,

  • 函数后面的-8了吗?这个表示运行时对应的GOMAXPROCS的值。接着的20000000表示运行for循环的次数,也就是调用被测试代码的次数,最后的117 ns/op表示每次需要话费117纳秒。
  • Benchmarkxxx-4 格式为基准测试函数名-GOMAXPROCS,后面的-4代表测试函数运行时对应的CPU核数
  • 1 表示执行的次数
  • xx ns/op 表示每次的执行时间
  • xx B/op 表示每次执行分配的总字节数(内存消耗)
  • xx allocs/op 表示每次执行发生了多少次内存分配

以上是测试时间默认是1秒,也就是1秒的时间,调用两千万次,每次调用花费117纳秒。如果想让测试运行的时间更长,可以通过-benchtime指定,比如3秒。

go test -bench=. -benchtime=3s -run=none
BenchmarkSprintf-8      50000000               109 ns/op
PASS
ok      flysnow.org/hello       5.628s

可以发现,我们加长了测试时间,测试的次数变多了,但是最终的性能结果:每次执行的时间,并没有太大变化。一般来说这个值最好不要超过3秒,意义不大。

性能对比

上面那个基准测试的例子,其实是一个int类型转为string类型的例子,标准库里还有几种方法,我们看下哪种性能更加。

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

func BenchmarkFormat(b *testing.B){
    num:=int64(10)
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.FormatInt(num,10)
    }
}

func BenchmarkItoa(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.Itoa(num)
    }
}

运行基准测试,看看结果

go test -bench=. -run=none
BenchmarkSprintf-8      20000000               117 ns/op
BenchmarkFormat-8       50000000                33.3 ns/op
BenchmarkItoa-8         50000000                34.9 ns/op
PASS
ok      flysnow.org/hello       5.951s

从结果上看strconv.FormatInt函数是最快的,其次是strconv.Itoa,然后是fmt.Sprintf最慢,前两个函数性能达到了最后一个的3倍多。那么最后一个为什么这么慢的,我们再通过-benchmem找到根本原因。

go test -bench=. -benchmem -run=none
BenchmarkSprintf-8      20000000               110 ns/op              16 B/op          2 allocs/op
BenchmarkFormat-8       50000000                31.0 ns/op             2 B/op          1 allocs/op
BenchmarkItoa-8         50000000                33.1 ns/op             2 B/op          1 allocs/op
PASS
ok      flysnow.org/hello       5.610s

-benchmem可以提供每次操作分配内存的次数,以及每次操作分配的字节数。从结果我们可以看到,性能高的两个函数,每次操作都是进行1次内存分配,而最慢的那个要分配2次;性能高的每次操作分配2个字节内存,而慢的那个函数每次需要分配16字节的内存。从这个数据我们就知道它为什么这么慢了,内存分配都占用都太高。

在代码开发中,对于我们要求性能的地方,编写基准测试非常重要,这有助于我们开发出性能更好的代码。不过性能、可用性、复用性等也要有一个相对的取舍,不能为了追求性能而过度优化。

go clean

go clean 命令是用来移除当前源码包里面编译生成的文件,这些文件包括

_obj/ 旧的object目录,由Makefiles遗留
_test/ 旧的test目录,由Makefiles遗留
_testmain.go 旧的gotest文件,由Makefiles遗留
test.out 旧的test记录,由Makefiles遗留
build.out 旧的test记录,由Makefiles遗留
*.[568ao] object文件,由Makefiles遗留
DIR(.exe) 由 go build 产生
DIR.test(.exe) 由 go test -c 产生
MAINFILE(.exe) 由 go build MAINFILE.go产生

go fmt

go fmt 命令主要是用来帮你格式化所写好的代码文件。

比如我们写了一个格式很糟糕的test.go文件,我们只需要使用fmt命令,就可以让go帮我们格式化我们的代码文件。但是我们一般很少使用这个命令,因为我们的开发工具一般都带有保存时自动格式化功能,这个功能底层其实就是调用了 go fmt 命令而已。

使用go fmt命令,更多时候是用gofmt,而且需要参数-w,否则格式化结果不会写入文件。gofmt -w src,可以格式化整个项目。

go doc

go doc 命令其实就是一个很强大的文档工具。

如何查看相应package的文档呢? 例如builtin包,那么执行go doc builtin;如果是http包,那么执行go doc net/http;查看某一个包里面的函数,那么执行godoc fmt Printf;也可以查看相应的代码,执行godoc -src fmt Printf;

通过命令在命令行执行 godoc -http=:端口号 比如godoc -http=:8080。然后在浏览器中打开127.0.0.1:8080,你将会看到一个golang.org的本地copy版本,通过它你可以查询pkg文档等其它内容。如果你设置了GOPATH,在pkg分类下,不但会列出标准包的文档,还会列出你本地GOPATH中所有项目的相关文档,这对于经常被限制访问的用户来说是一个不错的选择。

其他命令

Go语言还提供了其它有用的工具,例如下面的这些工具

  • go fix 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1
  • go version 查看go当前的版本
  • go env 查看当前go的环境变量
  • go list 列出当前全部安装的package
  • go run 编译并运行Go程序