如何构建一个go项目

介绍

在这里我会介绍开发一个简单的Go包以及对go tool的使用。最标准的方法是先拉取,编译再安装Go包然后再到命令行运行。

go tool需要你按特定的方式组织你的代码.接下来请仔细阅读.

代码组织

概览

  • Go程序通常把所有的Go代码放在单一的工作空间
  • 工作空间包含各种版本控制的库(如:git管理的库)
  • 每一个库包含一个或多个包
  • 每一个包由单个目录下面的一个或多个go文件组成
  • 包的路径决定了它导入的路径

注意要和其它Go程序区分开来,每个Go的项目都有自己的工作空间并且工作空间是和版本控制库是紧密联系在一起的。

工作空间

一个工作空间是包含有三子目录的目录:

  • src包信Go的源文析
  • pkg包含仓库生成对应的包
  • bin包含我们生成Go的可执行程序

go tool编译所有的包并安装二进制文件到pkgbin目录
src子目录通常包含多个版本控制的库,这里面包含一个或多个包

一个工作空间看来是是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bin/
hello # command executable
outyet # command executable
pkg/
linux_amd64/
github.com/golang/example/
stringutil.a # package object
src/
github.com/golang/example/
.git/ # Git repository metadata
hello/
hello.go # command source
outyet/
main.go # command source
main_test.go # test source
stringutil/
reverse.go # package source
reverse_test.go # test source
golang.org/x/image/
.git/ # Git repository metadata
bmp/
reader.go # package source
writer.go # package source
... (many more repositories and packages omitted) ...

上面的结构包含两个库(example和image).example库包含两个可执行程序和一个库。image仓库包含bmp包和一些其它的。

一个典型的工作空间是包含多个仓库源,每个仓库包含多个包或可执行程序。大部分Go程序保持所有Go源码和依赖放在同一个工作空间。

可执行程序和程序所需要的库是从不同的包里生成的。我们稍后会讨论他们的区别。

GOPATH环境变量

GOPATH环境变量指定了工作空间的位置.linux默认是~/go,windows下面是C:\Users\YourName\go

如果你喜欢在不同的位置工作,你需要设置下GOPATH指向你想要的那个目录(另一个常见的设置是GOPATH=$HOME),注意GOPATH不要和Go安装的目录相同。

go env GOPATH输出当前生效的GOPATH,如果没有设置会输出默认的值。

为了方便把工作目录的bin子目录设置加入到系统PATH:

1
$export PATH=$PATH:$(go env GOPATH)/bin

Import paths

一个导入路径是标识唯一个包的一串字符。一个包导入路径对应它在工作空间里面的目录或是一个远程仓库。

标准库的包导入路径非常短,比如:”fmt”、”net/http”.对于自己的包来说,你必须选择将来加入一些标准库或其它外部库不太可能会产生冲突的基础路径。

如果你把代码放在某个开源仓库,你应该仓库的根目录做为你的基础路径。比如:你的github帐号是github.com/user,这应该是你的基础路径。

注意在你可以编译之前最好不要发布你的代码到远程仓库。这是一个好的习惯来组织你的代码如果你在将来某一天发布。事实上你可以选择其它任意的目录名称,只要它对标准库和Go生态生成的路径唯一就好。

接下来我们会用github.com/user做为我们的基础路径。在你的工作空间创建一个目录来放你的源码:

1
$mkdir -p $GOPATH/src/github.com/user

第一个Go程序

编译和运行一个程序首先需要选择一个包路径(我们用github.com/user/hello)然后在你的工作空间创建相应的包路径:

1
$ mkdir $GOPATH/src/github.com/user/hello

接下来我们在目录下面创建一个叫hello.go的文件,文件内容如下:

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
fmt.Printf("Hello, world.\n")
}

现在你可以用go tool编译然后安装这个程序:

1
$ go install github.com/user/hello

注意你可以在你电脑的任何地方运行这个命令。go tool会在你的GOPATH查找github.com/user/hello对应的包。

如果你在包的目录下面运行go install话,你可以忽略包的路径:

1
2
$ cd $GOPATH/github.com/user/hello
$ go install

这个命令编译hello生成一个可执行文件。然后安装这个二进制文件到工作空间的bin目录下,生成的名字是hello(windows下是hello.exe)。在我们的例子会在$GOPATH/bin/hello,也就是$HOME/go/bin/hello.

go tool只会在有错误的情况下输出,如果没有任何输出说明是执行成功。

现在你可以在命令行输入绝对路径来运行你的程序:

1
$ $GOPATH/bin/hello

或者你已经把$GOPATH/bin加入到系统PATH了,你只需要输入:

1
2
$ hello
Hello, world.

如果你正在用一个版本控制系统,那么现在是初始化一个仓库的最佳时机,新增这些文件,然后提交你第一次变更。再强调一次,这一步是可选的:你不需要用源代码控制系统来写Go代码。

1
2
3
4
5
6
7
8
$ cd $GOPATH/src/github.com/user/hello
$ git init
Initialized empty Git repository in /home/user/work/src/github.com/user/hello/.git/
$ git add hello.go
$ git commit -m "initial commit"
[master (root-commit) 0b4507d] initial commit
1 file changed, 1 insertion(+)
create mode 100644 hello.go

第一个库

让我们写一个库并在hello程序里面使用这个库

再说下,第一步创建包路径:

1
$ mkdir $GOPATH/src/github.com/user/stringutil

接下来在目录下创建一个reverse.go的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
// Package stringutil contains utility functions for working with strings.
package stringutil
// Reverse returns its argument string reversed rune-wise left to right.
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}

现在我们用go build来编译这个包:

1
$ go build github.com/user/stringutil

或进入目录:

1
$ go build

这个不会产生文件。为了这样做,你必须用go install把这个包放到你工作空间的pkg目录。

在你确实stringutil包编译成功后,修改你原先的hello.go代码:

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"fmt"
"github.com/user/stringutil"
)
func main() {
fmt.Printf(stringutil.Reverse("!oG ,olleH"))
}

运行我们的程序,结果如下:

1
2
$ hello
Hello, Go!

经过上面的步骤后,我们的工作空间如下:

1
2
3
4
5
6
7
8
9
10
11
12
bin/
hello # command executable
pkg/
linux_amd64/ # this will reflect your OS and architecture
github.com/user/
stringutil.a # package object
src/
github.com/user/
hello/
hello.go # command source
stringutil/
reverse.go # package source

包名

在Go的源码里面第一条语句必需是:

1
package name

name是包默认的名字用来导入。(一个包里所有的文件必须用同一个name)

Go的转换规则是把导入路径最后一个元素做为包名的:如果”crypto/rot13”导入的话name应该命名为rot13.

可执行的命令必须用package main.

在链接到一个二进制文件的时候不要求所有包里的名称都唯一,但是导入的路径必须唯一。

测试

Go有一个轻微测试框,由go test命令和testing包组成。

新建一个以_test.go结尾的文件名来做为测试,文件里面包含类TestXXX(t *testing.T)的函数。测试框架会运行每一个这样的函数;如果函数用T.Error或t.Fail调用失败,这个测试是被认为失败了。

到stringutil包里新增一个测试文件($GOPATH/src/github.com/user/stringutil/reverse_test.go),内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package stringutil
import "testing"
func TestReverse(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{"Hello, 世界", "界世 ,olleH"},
{"", ""},
}
for _, c := range cases {
got := Reverse(c.in)
if got != c.want {
t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
}
}
}

运行这个测试:

1
2
$ go test github.com/user/stringutil
ok github.com/user/stringutil 0.165s

同样的,可以在包里面运行这个测试:

1
2
$ go test
ok github.com/user/stringutil 0.165s

远程包

包的导入描述了是怎样通过版本控制系统获取包的源码的(如:git或Mercurial).go tool利用这个特性从远程仓库获取这个包。例如,在这个文章里面example是放在Github上面的(github.com/golang/example)。如果你在包的导入路径添加了这个地址,go get将会自动拉取、编译和安装:

1
2
3
$ go get github.com/golang/example/hello
$ $GOPATH/bin/hello
Hello, Go examples!

如果指定的包不存在当前的工作空间,go get将会把包放到由GOPATH指定的工作空间里面。(如果这个包已经在工作空间中存在,go get跳过从远程的拉取和安装)

执行上面go get命令后,现在的工作空间目录结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bin/
hello # command executable
pkg/
linux_amd64/
github.com/golang/example/
stringutil.a # package object
github.com/user/
stringutil.a # package object
src/
github.com/golang/example/
.git/ # Git repository metadata
hello/
hello.go # command source
stringutil/
reverse.go # package source
reverse_test.go # test source
github.com/user/
hello/
hello.go # command source
stringutil/
reverse.go # package source
reverse_test.go # test source

托管在Gihub上的hello程序和它依赖stringutil包在同一个仓库。在hello.go文件里用相同的导入路径转换,所以go get命令找到和安装依赖包到对应的目录。

1
import "github.com/golang/example/stringutil"

英语很渣的一个翻译
参考