如何编写 Go 代码 - 代码组织与测试「译」
译者注:GOPATH 和目录组织现在有了更多选择,请查看: Go Modules 。感觉这篇官方文档成文比较早了,有些地方可能已经过时没更新,但是博主认为此文依旧值得一看。原文地址: How to Write Go Code
内容简介
本文将演示开发一个简单的 Go 包,并介绍 go 命令行工具,描述 fetch 、 build 和 install 包的标准方式。
go 命令行工具需要你按照特定方式组织代码。请仔细阅读本文档,本文描述了安装、配置并运行你的 Go 项目的最简单方式。
有类似的视频讲解: 视频讲解。
代码组织
总览
- Go 开发者通常将他们所有 Go 代码存放于一个工作区( workspace )中
- 一个工作区包含多个版本控制仓库 (例如由 Git 管理)
- 每个仓库包含一个或多个包
- 每个包由一个或多个在单个目录的 Go 源码文件组成
- 到达包的文件目录路径决定了其导入路径 ( import path )
注意与其他编程环境不同,其他编程环境往往每个项目都是单独的工作区并且与版本控制仓库联系紧密。
工作区( workspace )
工作区是目录层次结构,根目录下有两个子目录:
- src 目录,包含 Go 源码文件
- bin 目录,包含可执行文件
go 命令行工具构建和安装可执行文件到 bin 目录。
src 目录一般包含多个版本控制仓库( 例如 Git 或者 Mercurial ),用来追踪一个或者多个源码包的开发。
为了让你对实践中的工作区有一个直观印象,这里是个例子:
bin/
hello # command executable
outyet # command executable
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 仓库包含两个命令 ( hello 和 outyet )和一个库 ( stringutil )。而 image 仓库包含 bmp 包和其他一些省略了的东西。
一般工作区包含了许多源代码仓库,包含着许多包和命令。大部分 Go 开发者把他们所有的 Go 源代码都保存在一个工作区域中。
GOPATH 环境变量
GOPATH 环境变量指明了你的工作区域的路径。默认情况下,其为用户 home 目录下的 go 文件夹,所以在 Unix 类系统中为 $home/go ,在 Plan 9 中为 $home/go ,而在 Windows 中则通常为 C:\Users\YourName\go 。
如果你希望在不同的路径中工作,需要设置 GOPATH 到你想要的目录。(另外一个流行的做法是设置 GOPATH=$HOME 。)注意, GOPATH 一定不能设置为 Go 的安装目录。
go env 命令会打印当前有效的 GOPATH ,如果没有设置环境变量将会打印默认路径。
方便起见,可以添加工作区的 bin 子目录到 PATH 中:
$ export PATH=$PATH:$(go env GOPATH)/bin
简洁起见,本文后面所涉及的代码均使用 $GOPATH 而不是 $(go env GOPATH) 。为了使后续代码如期运行,如果你没有设置 GOPATH ,你可以使用 $HOME/go 来替换,或者执行下面的命令:
$ export GOPATH=$(go env GOPATH)
更多关于 GOPATH 环境变量的信息,请阅读: 'go help gopath'。
导入路径
一个导入路径就是一段确定了(唯一)一个包的字符串。一个包的导入路径等于其在工作区域的路径或者远程仓库路径(下面会展开说)。
标准库中的包都有对应的短导入路径,例如 "fmt" 和 "net/http" 。而你自己的包必须选择一个不与将来标准库会添加的新特性会冲突的基础路径,或者可能与外部库冲突的基本路径。
如果你在某个源码仓库中保存你的代码,那你应当使用仓库的根目录作为你的基本路径。例如 GitHub ,你应当使用 github.com/user 作为你的基本路径。
注意,如果你在构建之前,不需要发布你的代码到远程仓库,那么按照将来发布时候应该有的格式去组织你的代码是个好习惯。实践中,你可以选择任意的路径名,当然,其需要足以区分于标准库和巨大的 Go 生态系统。
这里我们以 github.com/user 作为我们的基础路径,在你的工作区域中建立对应的目录来保存源代码:
$ mkdir -p $GOPATH/src/github.com/user
你的第一个程序
为了编译和运行一个简单的程序,首先需要选择一个包路径(这里使用 github.com/user/hello ),然后在工作区域创建对应的包目录:
$ mkdir $GOPATH/src/github.com/user/hello
下一步,在该目录创建文件 hello.go ,包含如下 Go 代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
然后就可以通过 go 命令行工具构建和安装程序了:
$ go install github.com/user/hello
注意,你可以在你系统的任意路径运行这个命令。 go 命令行工具会在 GOPATH 指明的工作区域中寻找 github.com/user/hello 包对应的源代码。
如果你就在包目录执行 go install 命令,则可以忽略包路径:
$ cd $GOPATH/src/github.com/user/hello
$ go install
这个命令会构建出 hello 命令并生成可执行文件。随后会将该二进制文件安装到工作区域的 bin 目录下并命名为 hello 文件(如果是 Windows 就是 hello.exe )。
go 命令行工具仅在错误发生时才有输出,如果没有任何输出表明他们执行成功。
现在,你就可以通过绝对路径来运行该程序了:
$ $GOPATH/bin/hello
Hello, world.
如果你添加了 $GOPATH/bin 到 PATH 中,则可以直接通过可执行的二进制文件名来运行:
$ hello
Hello, world.
如果你使用源码控制系统,现在就是初始化仓库的好时候,添加这些文件,并且提交你的第一次改动。当然,这个步骤是可选的,写 Go 代码并不一定需要源代码控制。
$ cd $GOPATH/src/github.com/user/hello
$ git init
Initialized empty Git repository in /home/user/go/src/github.com/user/hello/.git/
$ git add hello.go
$ git commit -m "initial commit"
[master (root-commit) 0b4507d] initial commit
1 file changed, 7 insertion(+)
create mode 100644 hello.go
推送代码到远程仓库留作读者练习。
你的第一个库
让我们写一个库并且在 hello 程序中用到它。
同样的,第一步是选择一个包路径 (这里使用 github.com/user/stringutil )并创建对应的目录:
$ mkdir $GOPATH/src/github.com/user/stringutil
下一步,在该目录中创建 reverse.go 文件,并写入如下代码:
// 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 来编译这个包:
$ go build
这不会产生输出文件,而是在本地缓存中保存编译好的包。
确认 stringutil 包构建完毕之后,修改 hello.go 文件(在 $GOPATH/src/github.com/user/hello 中)的代码来使用它:
package main
import (
"fmt"
"github.com/user/stringutil"
)
func main() {
fmt.Println(stringutil.Reverse("!oG ,olleH"))
}
安装 hello 程序:
$ go install github.com/user/hello
运行新版本的程序,你应当看到一个新的,反转了的信息:
$ hello
Hello, Go!
经过这些步骤,你的工作区域应当看起来是这个样子:
bin/
hello # command executable
src/
github.com/user/
hello/
hello.go # command source
stringutil/
reverse.go # package source
包名 ( package name )
Go 源码文件中的第一个表达式必须是:
package name
这里的 name 也是该包被导入时默认的名字(包内所有文件都必须使用同样的 name )。
Go 语言的惯例是,包名就是导入路径的最后一个元素名字,例如通过 "crypto/rot13" 导入的包,就应当把这个 name 设置为 rot13 。
可执行命令的包名必须总是声明为 package main 。
并不要求某个程序所引用的包,其包名必须全都是独一无二不重复的,只是要求他们的导入路径(完整路径名)不重复。
可以看高效的 Go 语言学习更多 Go 语言的命名约定。
测试
Go 语言通过 go test 命令和 testing 包提供了一个轻量级的测试框架。
你可以通过创建 _test.go 结尾的文件并在其中加入以 TestXXX 命名的 func (t *testing.T) 类型的函数来进行测试。测试框架将会执行每一个这种函数,如果函数调用失败函数,例如 t.Error 或者 t.Fail ,则测试就会被认为是失败了。
现在通过创建文件 $GOPATH/src/github.com/user/stringutil/reverse_test.go ,并加入如下代码来测试 stringutil 包:
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)
}
}
}
然后通过 go test 命令来运行测试:
$ go test github.com/user/stringutil
ok github.com/user/stringutil 0.165s
同样的,如果你就在包目录下执行 go 命令行工具,则可以省略该包路径:
$ go test
ok github.com/user/stringutil 0.165s
更多细节可以通过运行 go help test 来查看 testing 包的文档。
远程包
一个导入路径可以描述如何通过版本管理工具如 Git 或者 Mercurial 来获取包的源代码。 go 命令行工具通过这个属性自动获取远程仓库。例如,本文的例子也通过 Git 仓库的方式保存在 Github ,路径为: github.com/golang/example 。如果你在包的导入路径中包含了仓库的 URL , go get 命令将会自动自动获取、构建和安装:
$ go get github.com/golang/example/hello
$ $GOPATH/bin/hello
Hello, Go examples!
如果指定的包当前不在工作区域中, go get 会将其放在 GOPATH 指定的第一个工作区域中。(如果已经存在, go get 会跳过远程获取步骤,剩余步骤与 go install 一样。)
上述操作,现在工作区域目录树应当如下:
bin/
hello # command executable
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
Github 上托管的 hello 命令依赖于同一仓库中的 stringutil 包。 hello.go 文件中的导入路径也采用同样的约定,所以 go get 命令也能够定位并安装依赖包。
import "github.com/golang/example/stringutil"
这个约定是让你的包可以为其他人所用的最简单的方式。 Go Wiki 和 godoc.org 提供了外部 Go 项目列表。
更多关于使用远程仓库的信息可以通过 go help importpath 查看。
下一步该干什么?
订阅 golang-发布 邮件列表,接收 Go 语言新版本发布通知。
查看 高效的 Go 语言 查看更多关于写出清晰、符合约定的 Go 代码的小知识。
查看 Go 新手教程 学习写对 Go 语言语法。
访问 文档页面 ,查看一系列关于 Go 语言、库和工具的深度解读文章。
帮助
实时帮助可以去 Freenode 的#go-nuts IRC 频道,询问其他有所帮助的 Go 仔们。
用来讨论 Go 语言的官方邮件列表: Go Nuts 。
bug 报告: Go 问题追踪 。
感觉go语言挺好的