如何写Go代码
一、介绍
这篇文章演示在一个模块里面开发一个简单的Go包,并且介绍Go工具,标准的方法用于获取、构建、和下载Go模块,包和命令。
注意:这篇文章假设你正在使用Go1.13或更新的版本,并且GO111MODULE
环境变量没有设置。
二、代码的组织
Go程序是被组织成包。一个包是在相同目录下的源文件集合,它们会被一起编辑。函数、类型、变量和常量是定义在一个源文件中,是可见对于在相同包下的所有其它源文件。
一个仓库包含一个或多个模块。一个模块是一个相关Go包的集合,它们将一起发布。一个Go仓库通常只包含一个模块,位于仓库的根目录。一个被命名为go.mod
的文件声明了模块的路径:模块中所有包的导入路径的前缀。一个模块包含的包在包含go.mod
文件的目录下,也包含目录下的子目录,直到下一个包含go.mod
文件的子目录(如果有)。
注意:在你构建它之前,你不需要发布你的代码到远程仓库。一个模块能够被定义在本地不用归属于一个仓库。可是,组织你的代码就像你有一天会发布它一样是一个好习惯。
每个模块路径不仅作为包的导入路径的前缀,而且指明go命令应该去哪下载它。例如,模块github.com/google/go-cmp
包含一个包在comp/
目录下。这个包的导入路径是github.com/google/go-cmp/cmp
。标准库中的包没有模块路径前缀。
三、你的第一个程序
为了编译和运行一个简单的程序,首先选择一个模块路径(我们将使用example/user/hello-write-go-code
)并创建一个声明它的go.mod
文件,
$ mkdir hello-write-go-code
$ cd hello-write-go-code/
$ ll
total 0
drwxr-xr-x 2 lifei staff 64 9 18 11:01 ./
drwxr-xr-x 12 lifei staff 384 9 18 11:01 ../
$ go mod init example/user/hello-write-go-code
go: creating new go.mod: module example/user/hello-write-go-code
$ ll
total 8
drwxr-xr-x 3 lifei staff 96 9 18 11:02 ./
drwxr-xr-x 12 lifei staff 384 9 18 11:01 ../
-rw-r--r-- 1 lifei staff 49 9 18 11:02 go.mod
$
Go 源文件中的第一条语句必须是包名。 可执行命令必须始终使用包 main。
接下来,在目录中创建一个文件,命名为hello.go
,并包含下面的代码:
package main
import "fmt"
func main() {
fmt.Println("Hello,world.")
}
现在,你能够使用Go工具构建并安装这个程序:
$ go install example/user/hello-write-go-code
$
这个命令,构建了一个hello-write-go-code
命令,产生了一个可执行的二进制文件。它然后安装这个二进制文件在$HOME/go/bin/hello
:
$ ll ~/go/bin/hello-write-go-code
-rwxr-xr-x 1 lifei staff 1854560 9 18 11:07 /Users/lifei/go/bin/hello-write-go-code*
您可以使用 go env 命令为将来的 go 命令可移植地设置环境变量的默认值:
$ go env -w GOBIN=/somewhere/else/bin
$
要取消设置以前由 go env -w 设置的变量,请使用 go env -u:
$ go env -u GOBIN
$
像 go install
这样的命令适用于包含当前工作目录的模块的上下文中。 如果工作目录不在 example/user/hello-write-go-code
模块中,go install
可能会失败。
为方便起见,go 命令接受相对于工作目录的路径,如果没有给出其他路径,则默认为当前工作目录中的包。 所以在我们的工作目录中,下面的命令都是等价的:
$ go install example/user/hello-write-go-code
$ go install .
$ go install
$
接下来,让我们运行程序确认它是工作的。为了添加方便,我们将添加安装目录到我们PATH
,让二进制运行更容易:
$ export PATH=$PATH:$(dirname $(go list -f '{{.Target}}' .))
$ hello-write-go-code
Hello,world.
$
如果你正在使用一个源代码控制系统,现在是时候初始化一个仓库。添加文件,并提交你的第一个改变。同样,这一步是可选的:您不需要使用源代码管理来编写 Go 代码。
$ git add --all
$ git commit
[main 5e8eda6] 如何写一个Go代码
3 files changed, 118 insertions(+)
create mode 100644 document/2022-09-17-如何写Go代码.md
create mode 100644 projects/hello-write-go-code/go.mod
create mode 100644 projects/hello-write-go-code/hello.go
$
go 命令通过请求相应的 HTTPS URL 并读取嵌入在 HTML 响应中的元数据来定位包含给定模块路径的存储库。许多托管服务已经为包含 Go 代码的存储库提供了该元数据,因此使您的模块可供他人使用的最简单方法通常是使其模块路径与存储库的 URL 匹配。
四、从你的模块导入包
让我们写一个morestrings
程序,使用来自hello-write-go-code
的程序。首先,创建一个目录作为包,命令为hello-write-go-code/morestrings
,然后创建一个名称为reverse.go
的文件,包含下面的内容:
$ mkdir morestrings
$ cd morestrings/
$ touch reverse.go
$
package morestrings
func ReverseRunes(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i , j = i+1,j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
因为我们的ReverseRunnes
函数使用了大写字母开头,它是可导出的,并且能在其它包中使用,通过导入morestrings
包:
让我们测试这个包,使用go build
编译:
$ go build
$ ll
total 8
drwxr-xr-x 3 lifei staff 96 9 18 11:39 ./
drwxr-xr-x 5 lifei staff 160 9 18 11:39 ../
-rw-r--r-- 1 lifei staff 174 9 18 11:46 reverse.go
$
这不会产生一个输出文件,相反,它保存编译包到本地的构建缓存中。
之后,验证morestrings
包的构建,让我们在hello
程序中使用。为了做这个,修改hello-write-go-code/hello.go
去使用morestrings
包:
package main
import (
"fmt"
"example/user/hello-write-go-code/morestrings"
)
func main() {
fmt.Println(morestrings.ReverseRunes("Hello,world."))
}
安装Go程序,并运行新版本的程序,你应该看到一个新的,反转的消息。
$ go install .
$ hello-write-go-code
.dlrow,olleH
$
五、从远程模块导入包
导入路径可以描述如何使用 Git 或 Mercurial 等修订控制系统获取包源代码。go 工具使用此属性自动从远程存储库中获取包。例如,使用github.com/google/go-cmp/cmp
在你的程序中。
package main
import (
"fmt"
"example/user/hello-write-go-code/morestrings"
"github.com/google/go-cmp/cmp"
)
func main() {
fmt.Println(morestrings.ReverseRunes("Hello,world."))
fmt.Println(cmp.Diff("Hello World", "Hello Go"))
}
现在你依赖额外的模块,你需要下载这个模块并记录它的版本在go.mod
文件中。go mod tidy
命令为导入的包添加缺少的模块要求,并删除不再使用的模块的要求。
$ go mod tidy
go: finding module for package github.com/google/go-cmp/cmp
example/user/hello-write-go-code imports
github.com/google/go-cmp/cmp: module github.com/google/go-cmp/cmp: Get "https://proxy.golang.org/github.com/google/go-cmp/cmp/@v/list": dial tcp 142.251.42.241:443: i/o timeout
$ go env -w GOPROXY=https://goproxy.cn
$ go mod tidy
go: finding module for package github.com/google/go-cmp/cmp
go: downloading github.com/google/go-cmp v0.5.9
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.5.9
$ go install example/user/hello-write-go-code
$ hello-write-go-code
.dlrow,olleH
string(
- "Hello World",
+ "Hello Go",
)
$ cat go.mod
module example/user/hello-write-go-code
go 1.18
require github.com/google/go-cmp v0.5.9
$
模块的依赖是自动被下载到pkg/mod
目录下,这个目录是在环境变量GOPATH
指定的。给定版本的模块下载的内容在需要该版本的所有其他模块之间共享,因此 go 命令将这些文件和目录标记为只读。 要删除所有下载的模块,您可以传递 -modcache
标志以进行清理:
$ go clean -modcache
$
六、测试
Go有一个轻量级的测试框架,组成go test
命令和testing
包。
通过写一个带有_test.go
结尾的文件,包含函数名TestXXX
带有签名func (t *testing.T)
,测试框架会运行每一个函数。如果函数调用失败函数,例如 t.Error 或 t.Fail,则认为测试失败。
添加一个测试到morestrings
包中,通过创建一个reverse_test.go
,包含下面的代码:
package morestrings
import "testing"
func TestReverseRunes(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello,world", "dlrow,olleH"},
{"Hello, 世界", "界世 ,olleH"},
{"",""},
}
for _,c := range cases {
got := ReverseRunes(c.in)
if got != c.want {
t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
}
}
}
然后使用go test
运行测试:
$ go test
PASS
ok example/user/hello-write-go-code/morestrings 0.687s
$