如何编写 Go 代码

引言

本文演示了如何在模块中开发一个简单的 Go 包,并介绍了 go tool,它是获取、构建和安装 Go 模块、包和命令的标准方法。

代码组织

Go 程序被组织成包。一个 是同一个目录中一起编译的源文件集合。在一个源文件中定义的函数、类型、变量和常量对同一包中的所有其他源文件都可见。

一个仓库包含一个或多个模块。一个 模块 是相关 Go 包的集合,它们一起发布。一个 Go 仓库通常只包含一个模块,位于仓库的根目录。在那里,一个名为 go.mod 的文件声明了 模块路径:模块中所有包的导入路径前缀。该模块包含其 go.mod 文件所在目录中的包以及该目录的子目录中的包,直到下一个包含另一个 go.mod 文件的子目录(如果有)。

请注意,您无需在构建代码之前将其发布到远程仓库。模块可以在本地定义,而无需属于某个仓库。然而,将代码组织起来,就像将来会发布一样,是一个好习惯。

每个模块的路径不仅作为其包的导入路径前缀,还指示 go 命令应在哪里查找以下载它。例如,为了下载模块 golang.org/x/toolsgo 命令会查询 https://golang.ac.cn/x/tools 所指示的仓库(更多描述请参见 此处)。

一个 导入路径 是一个用于导入包的字符串。包的导入路径是其模块路径与模块内子目录的组合。例如,模块 github.com/google/go-cmp 在目录 cmp/ 中包含一个包。该包的导入路径是 github.com/google/go-cmp/cmp。标准库中的包没有模块路径前缀。

您的第一个程序

要编译和运行一个简单的程序,首先选择一个模块路径(我们将使用 example/user/hello),然后创建一个声明该路径的 go.mod 文件

$ mkdir hello # Alternatively, clone it if it already exists in version control.
$ cd hello
$ go mod init example/user/hello
go: creating new go.mod: module example/user/hello
$ cat go.mod
module example/user/hello

go 1.16
$

Go 源文件中的第一条语句必须是 package name。可执行命令必须始终使用 package main

接下来,在该目录中创建一个名为 hello.go 的文件,其中包含以下 Go 代码

package main

import "fmt"

func main() {
    fmt.Println("Hello, world.")
}

现在您可以使用 go 工具构建并安装该程序

$ go install example/user/hello
$

此命令构建 hello 命令,生成一个可执行二进制文件。然后它将该二进制文件安装为 $HOME/go/bin/hello(或者,在 Windows 下,%USERPROFILE%\go\bin\hello.exe)。

安装目录由 GOPATHGOBIN 环境变量 控制。如果设置了 GOBIN,则二进制文件将安装到该目录。如果设置了 GOPATH,则二进制文件将安装到 GOPATH 列表中第一个目录的 bin 子目录。否则,二进制文件将安装到默认 GOPATH$HOME/go%USERPROFILE%\go)的 bin 子目录。

您可以使用 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 模块中,go install 可能会失败。

为方便起见,go 命令接受相对于工作目录的路径,如果没有给出其他路径,则默认为当前工作目录中的包。因此,在我们的工作目录中,以下命令都是等效的

$ go install example/user/hello
$ go install .
$ go install

接下来,让我们运行程序以确保它正常工作。为了方便起见,我们将安装目录添加到 PATH 中,以便轻松运行二进制文件

# Windows users should consult /wiki/SettingGOPATH
# for setting %PATH%.
$ export PATH=$PATH:$(dirname $(go list -f '{{.Target}}' .))
$ hello
Hello, world.
$

如果您正在使用源代码管理系统,现在是初始化仓库、添加文件并提交第一次更改的好时机。再次强调,此步骤是可选的:您不需要使用源代码管理来编写 Go 代码。

$ git init
Initialized empty Git repository in /home/user/hello/.git/
$ git add go.mod hello.go
$ git commit -m "initial commit"
[master (root-commit) 0b4507d] initial commit
 1 file changed, 7 insertion(+)
 create mode 100644 go.mod hello.go
$

go 命令通过请求相应的 HTTPS URL 并读取 HTML 响应中嵌入的元数据来定位包含给定模块路径的仓库(请参阅 go help importpath)。许多托管服务已经为包含 Go 代码的仓库提供了该元数据,因此让您的模块可供他人使用的最简单方法通常是使其模块路径与仓库的 URL 匹配。

从您的模块导入包

让我们编写一个 morestrings 包并在 hello 程序中使用它。首先,为该包创建一个名为 $HOME/hello/morestrings 的目录,然后在该目录中创建一个名为 reverse.go 的文件,其中包含以下内容

// Package morestrings implements additional functions to manipulate UTF-8
// encoded strings, beyond what is provided in the standard "strings" package.
package morestrings

// ReverseRunes returns its argument string reversed rune-wise left to right.
func ReverseRunes(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)
}

因为我们的 ReverseRunes 函数以大写字母开头,所以它是 导出 的,可以在导入我们 morestrings 包的其他包中使用。

让我们用 go build 测试包是否编译

$ cd $HOME/hello/morestrings
$ go build
$

这不会产生输出文件。相反,它将编译后的包保存在本地构建缓存中。

在确认 morestrings 包可以构建之后,让我们在 hello 程序中使用它。为此,修改您原来的 $HOME/hello/hello.go 以使用 morestrings 包

package main

import (
    "fmt"

    "example/user/hello/morestrings"
)

func main() {
    fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
}

安装 hello 程序

$ go install example/user/hello

运行新版本的程序,您应该会看到一条新的、反转的消息

$ hello
Hello, Go!

从远程模块导入包

导入路径可以描述如何使用版本控制系统(例如 Git 或 Mercurial)获取包源代码。go 工具利用此特性自动从远程仓库获取包。例如,要在您的程序中使用 github.com/google/go-cmp/cmp

package main

import (
    "fmt"

    "example/user/hello/morestrings"
    "github.com/google/go-cmp/cmp"
)

func main() {
    fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
    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
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.5.4
$ go install example/user/hello
$ hello
Hello, Go!
  string(
-     "Hello World",
+     "Hello Go",
  )
$ cat go.mod
module example/user/hello

go 1.16

require github.com/google/go-cmp v0.5.4
$

模块依赖项会自动下载到 GOPATH 环境变量指示的目录的 pkg/mod 子目录中。给定版本模块的下载内容由所有 require 该版本的其他模块共享,因此 go 命令将这些文件和目录标记为只读。要删除所有已下载的模块,您可以将 -modcache 标志传递给 go clean

$ go clean -modcache
$

测试

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

您通过创建一个名称以 _test.go 结尾的文件来编写测试,该文件包含名为 TestXXX 且签名为 func (t *testing.T) 的函数。测试框架会运行每个这样的函数;如果该函数调用了失败函数,例如 t.Errort.Fail,则测试被认为是失败的。

通过创建文件 $HOME/hello/morestrings/reverse_test.go,其中包含以下 Go 代码,为 morestrings 包添加一个测试。

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 运行测试

$ cd $HOME/hello/morestrings
$ go test
PASS
ok  	example/user/hello/morestrings 0.165s
$

运行 go help test 并查看 testing 包文档 以获取更多详细信息。

下一步

订阅 golang-announce 邮件列表,以便在新稳定版 Go 发布时收到通知。

参阅 Effective Go,获取编写清晰、地道的 Go 代码的技巧。

参加 A Tour of Go 学习语言本身。

访问 文档页面,获取一系列关于 Go 语言及其库和工具的深入文章。

获取帮助

如需实时帮助,请在社区运营的 gophers Slack 服务器 上向乐于助人的 gophers 提问(在此获取邀请)。

讨论 Go 语言的官方邮件列表是 Go Nuts

使用 Go issue tracker 报告错误。