Go 开发、编译、发布体系

开发环境搭建

Go 环境搭建步骤

参考文档:Download and install - The Go Programming Language

  1. 前往以下官网下载地址下载指定版本的 go 安装包,以 go1.20.5.linux-amd64.tar.gz 为例

https://go.dev/dl/

  1. 解压 go 安装包到 /usr/local/go
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.5.linux-amd64.tar.gz
  1. 配置 go 环境变量
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
source ~/.profile
  1. 测试是否配置成功
go version
 
# 输出
go version go1.20.5 linux/amd64

Go 代码编辑软件

参考文档:Editor plugins and IDEs - The Go Programming Language

目前主流的 go 代码编辑软件有:GoLandVisual Studio Code 等,由于 GoLand 收费,所以选择开源的 vscode 配合对应的 go 插件来编写 go 代码。

  1. 下载 vscode go 插件

Image

  1. 安装/更新 go tools

快捷键:ctrl + shift + p 调出命令窗口,输入 Go: Install/Update Tools,点击第一项

Image

选择所有工具,点击确定

Image

等待所有工具安装完毕

Vscode Go 扩展取决于 gopls,dlv 和其他可选工具。如果缺少任何依赖项,则分析工具会显示警告⚠️ 。可以单击警告以下载依赖项。 有关扩展所依赖的工具的完整列表,请参阅工具文档

  1. Go tools 在开发中的使用

golang / vscode-go

Go 代码风格

见 google 的 go 代码风格指南:styleguide

  • 文件名

使用全小写字母或下划线组成,如:main.go, port_allocator.go

  • 包名

使用全小写字母组成,如:tabwriter

  • struct,函数,变量命名规则

使用驼峰命名,需要导出则首字母设置为大写。不需要导出设置首字母为小写

type Person struct {
    name string
    Age  int
}
 
func (p Person) Name() string {
    return "name: " + p.name
}
 
func (p *Person) SetName(n string) {
    p.name = strings.ToLower(n)
}
  • 接收者名称

使用开头首字母或缩写的小写形式,如:

Bad                       =>  Better
func (tray Tray)          =>  func (t Tray)
func (info *ResearchInfo) =>  func (ri *ResearchInfo)
func (self *Scanner)      =>  func (s *Scanner)
  • 常量命名

使用首字母大写的驼峰命名,如:

const MaxPacketSize = 512
 
const (
    ExecuteBit = 1 << iota
    WriteBit
    ReadBit
)
  • 缩写

名称中的单词如果是首字母缩写或缩略语(例如,URL和NATO)应该有相同的大小写。URL应该显示为URL或url(如urlPony,或URLPony),而不是Url。这也适用于ID,当它是 “标识符” 的缩写时;写 appID 而不是 appId。

  • 代码注释
// Options configure the group management service.
type Options struct {
    // General setup:
    Name  string
    Group *FooGroup
 
    // Dependencies:
    DB *sql.DB
 
    // Customization:
    LargeGroupThreshold int // optional; default: 10
    MinimumMembers      int // optional; default: 2
}
 
 
// WithTimeout returns a context that will be canceled no later than d duration
// from now.
//
// The caller must arrange for the returned cancel function to be called when
// the context is no longer needed to prevent a resource leak.
func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func())

Go 项目的工程结构

文档:Tutorial: Get started with Go - The Go Programming Language

创建一个 go 工程

  1. 创建一个项目文件夹
mkdir go-demo
cd go-demo
  1. 初始化 go 模块
go mod init example.com/go-demo

模块路径 example.com/go-demo 应该是源代码的储存库位置,见:Go 文档 Naming a module

例如:github.com/gin-gonic/gin

  1. 创建一个 main.go 文件作为项目入口点,并写入如下内容
package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, World!")
}
  • 入口函数所在包名必须为 main
  • 入口函数名为 main,并且没有入参和返回值
  1. 运行代码
go run .
  1. go 工程目录

Go 没有标准的代码目录组织规范。但有一个使用较多的非官方项目结构规范,见讨论:this is not a standard Go project layout我来告诉你Go项目标准结构如何布局 | Tony Bai

一个最小的 go 工程目录

go-demo
├── go.mod  # 用于记录项目的模块信息、依赖项和版本管理(类似 maven 的 pow.xml)
├── go.sum  # 用于记录项目所使用的依赖项的完整性校验和信息(由 go 的模块管理自动生成和修改)
└── main.go  # go 代码直接放到根目录下或者组织为目录树

习惯上包名目录名一致,如果多层目录嵌套,则使用最后一级目录名

Go 版本选择

Go 的发行版本

发布政策

Go 语言团队每年发布两个主要版本,一般稳定控制在每年的二月和八月。

维护周期

Go 语言团队对最新的两个主要的 Go 版本提供支持,比如当前最新的主要版本是 Go 1.20 版本,那么Go语言团队对 Go 1.20和Go 1.19版本提供支持,支持的范围主要包括修复版本中存在的重大问题、文档变更以及安全问题更新等。

依赖管理

Go 项目对第三方库的依赖方式

文档: Managing dependencies - The Go Programming Language Go Modules Reference - The Go Programming Language Go 的依赖管理是通过 Go Modules 来实现的,依赖关系定义在 go.mod 文件中。通过执行依赖管理命令都可以管理项目的依赖

一个典型的 go.mod 文件:

// 项目名
module example.com/go-demo
 
// 需要的 go 的最小版本
go 1.20
 
// 直接引入的外部库依赖(项目中使用到的依赖)
require (
    github.com/gin-gonic/gin v1.9.1
    go.mongodb.org/mongo-driver v1.12.0
    github.com/joho/godotenv v1.5.1
)
 
// 间接引入的外部库依赖(直接引入的外部库依赖的依赖,后缀会增加 `// indirect` 注释)
require (
    github.com/bytedance/sonic v1.9.2 // indirect
    github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
    github.com/gabriel-vasile/mimetype v1.4.2 // indirect
    github.com/gin-contrib/sse v0.1.0 // indirect
    github.com/go-playground/locales v0.14.1 // indirect
    github.com/go-playground/universal-translator v0.18.1 // indirect
    github.com/go-playground/validator/v10 v10.14.1 // indirect
    github.com/goccy/go-json v0.10.2 // indirect
    github.com/golang/snappy v0.0.4 // indirect
    github.com/json-iterator/go v1.1.12 // indirect
    github.com/klauspost/compress v1.16.6 // indirect
    github.com/klauspost/cpuid/v2 v2.2.5 // indirect
    github.com/leodido/go-urn v1.2.4 // indirect
    github.com/mattn/go-isatty v0.0.19 // indirect
    github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
    github.com/modern-go/reflect2 v1.0.2 // indirect
    github.com/montanaflynn/stats v0.7.1 // indirect
    github.com/pelletier/go-toml/v2 v2.0.8 // indirect
    github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
    github.com/ugorji/go/codec v1.2.11 // indirect
    github.com/xdg-go/pbkdf2 v1.0.0 // indirect
    github.com/xdg-go/scram v1.1.2 // indirect
    github.com/xdg-go/stringprep v1.0.4 // indirect
    github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
    golang.org/x/arch v0.3.0 // indirect
    golang.org/x/crypto v0.10.0 // indirect
    golang.org/x/net v0.11.0 // indirect
    golang.org/x/sync v0.3.0 // indirect
    golang.org/x/sys v0.9.0 // indirect
    golang.org/x/text v0.10.0 // indirect
    google.golang.org/protobuf v1.30.0 // indirect
    gopkg.in/yaml.v3 v3.0.1 // indirect
)

Go 的依赖管理一般不需要手动编辑 go.mod 文件,直接通过命令进行管理。当使用 go get 引入一个依赖但这个依赖还没有被项目使用时会被标记为 // indirect。当项目文件中有使用这个依赖后可以通过调用 go mod tidy 命令更新依赖。此时// indirect标记会被自动删除,且直接引用的依赖和间接引用的依赖会分开。

  • 初始化模块(生成 go.mod 和 go.sum 文件)
go mod init <name>
 
# 例如
go mod init example.com/go-demo
  • 添加依赖
# 添加一个依赖
go get <name>
# 添加一个指定版本的依赖
go get <name>@<version>
# 添加一个最新版本的依赖
go get <name>@latest
 
# 例如
go get example.com/theirmodule
go get example.com/theirmodule@v1.3.4
go get example.com/theirmodule@latest
  • 发现依赖更新
# 列出作为当前模块依赖项的所有模块的每个可用的最新版本
go list -m -u all
 
# 显示可用于特定模块的最新版本
go list -m -u example.com/theirmodule
 
# 更新特定模块为指定版本
go get -u example.com/theirmodule@v1.3.4
  • 同步代码的依赖项

确保 go.mod 与模块中的源代码一致。它添加构建当前模块的包和依赖所必须的任何缺少的模块,删除不提供任何有价值的包的未使用的模块。它也会添加任何缺少的条目至 go.mod 并删除任何不需要的条目。

go mod tidy
  • 删除依赖

设置依赖版本为 none 即可删除依赖

go get <name>@none
 
# 例如
go get example.com/theirmodule@none
  • 打印模块间的依赖关系图
  1. 以文本形式打印模块间的依赖关系图。输出的每一行行有两个字段(通过空格分割);模块和其所有依赖中的一个。
go mod graph
  • 列出依赖项
# 列出项目中的所有依赖
go list -m all
 
# 列出所有直接依赖
go list -m -f '{{if not .Indirect}}{{.}}{{end}}' all

Go 对第三方库的管理方式

Go 中没有类似于 java maven 的中央仓库,Go Modules 支持各种版本控制系统,并从这些版本控制系统中获取第三方库代码。第三方依赖一般发布在 github.com 上,Go 官方的库发布在 golang.org 上,因此 Go 引入的依赖可能来自多个地址。

Go modules 代理服务

文档:Go Modules Reference - The Go Programming Language

Go 虽然没有固定的中央仓库但可以配置一个 GOPROXY 指定 Go Modules 的代理服务地址。指定 GOPROXY 后,go modules 会从 GOPROXY 配置的代理服务器中获取依赖。

常见的 Go modules 代理服务:

以七牛云为例配置 GOPROXY

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

可使用 , 配置多个地址,当前一个地址不可用时会自动使用下一个地址,direct 表示直接连接到存储库。

GOPROXY=https://proxy.golang.org,https://goproxy.cn,direct

在内网中 Go 项目依赖第三方库的方式

  1. 内网中开放一个 Go modules 代理服务的地址,然后 Go 环境下配置 GOPROXY 为这个代理服务地址
  2. 内网中部署一个开源的 Go modules 代理服务器,然后 Go 环境下配置 GOPROXY 这个服务器的地址

开源的 Go Proxy 项目:

编译方式

文档:go build command

Go 是一种编译型语言可以直接编译为可执行文件,并且 Go 支持交叉编译可以直接编译为多个平台的可执行文件

编译为当前系统架构下的可执行文件

进入项目根目录下执行

go build [-o output] [build flags] [packages]
 
# 例如,执行后项目根目录下会生产一个名为 demo_linux_amd64_v1.0.0 的可执行文件
go build -o demo_linux_amd64_v1.0.0 main.go
  • -o:强制 build 将生成的可执行文件或对象写入指定的输出文件或目录
  • packages:入口文件

交叉编译

  1. 列出支持交叉编译的系统架构
go tool dist list
 
# 输出
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
freebsd/riscv64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/loong64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
windows/arm64
  1. 交叉编译为指定系统架构(windows 平台会自动添加 .exe 后缀)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o demo main.go
  • CGO_ENABLED:是否启用 cgo,交叉编译不支持(CGO 可以支持与 C 语言接口的互通)
  • GOOS:编译的操作系统类型
  • GOARCH:编译的系统框架

发布形态

Go 项目的一般发布形态

编译为不同平台的二进制可执行文件发布

工具类项目的一般发布形态

文档:Publishing a module - The Go Programming Language

Go 的第三方库是直接通过版本控制系统发布源代码。

  1. 进入项目根目录
  2. 运行go mod tidy,删除项目没有使用到的依赖。
go mod tidy
  1. 使用git tag命令给项目打上新的版本号
git commit -m "mymodule: changes for v0.1.0"
git tag v0.1.0
  1. 推送新的 tag 到远程仓库
git push origin v0.1.0
  1. 使用者通过 go get 命令获取你发布的库
go get example.com/mymodule@v0.1.0

Go 工程使用的版本编号命名格式

文档: Module release and versioning workflow - The Go Programming Language Module version numbering - The Go Programming Language

在语义版本模型中,已发布的模块会有一个版本号,如下图所示:

Image

版本阶段例子描述
开发中v**0**.x.x标志着该模块仍在开发中,不稳定。这个版本没有向后兼容性或稳定性保证。
主要版本(Major)v**1**.x.x标志着向后兼容的公共API变化。该版本不保证与之前的主要版本向后兼容。
次要版本(Minor)vx.**4**.x标志着向后兼容的公共API变化。这个版本保证了向后的兼容性和稳定性。
修订版本(Patch)vx.x.**1**标志着不影响模块的公共API或其依赖关系的变化。这个版本保证了向后的兼容性和稳定性。
预发布版本(Pre-release)vx.x.x-**beta.2**标志着这是一个预发布的里程碑,如 alpha 或 beta。这个版本没有稳定性保证。

测试环境搭建

单元测试

文档: go test command Go 自带的测试包 testing

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。

  • 程序需要编写在 xxx_test.go 文件中(go 编译时会自动忽略掉后缀为_test.go的文件)
  • 测试函数的命名必须以单词 Test 开始
  • 测试函数只接受一个参数 t *testing.T

字符串反转的单元测试例子:

  1. 创建一个 go 工程
mkdir go_test
cd go_test
 
go mod init example/go_test
 
touch reverse.go
touch reverse_test.go
go-test
├── go.mod
├── reverse.go
└── reverse_test.go
  1. 将字符串反转的函数写入 reverse.go
package main
 
import "fmt"
 
func Reverse(s string) string {
    b := []byte(s)
    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}
  1. reverse_test.go 中编写 func Reverse 的测试代码
  • 白盒测试,测试文件和待测试文件在同一个包下,可以引用包中未被导出的标识符
package main
 
import "testing"
 
func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev := Reverse(tc.in)
        if rev != tc.want {
            t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}
  • 黑盒测试,测试文件和待测试文件不在同一个包下,可以引用包中导出的标识符
package main_test
 
import (
    reverse "example/go_test"
    "testing"
)
 
func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev := reverse.Reverse(tc.in)
        if rev != tc.want {
            t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}
  1. 运行测试用例
go test -v
 
 
# 测试通过的输出
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok      example/go_test 0.001s
 
 
# 测试不通过的输出
=== RUN   TestReverse
    reverse_test.go:18: Reverse: "dlrow ,olleH", want "dlro ,olleH"
--- FAIL: TestReverse (0.00s)
FAIL
exit status 1
FAIL    example/go_test 0.001s
  1. 方法的子测试(一个用例下的多个测试场景)
func TestMul(t *testing.T) {
    t.Run("sub_test1", func(t *testing.T) {
        // ...
    })
    t.Run("sub_test2", func(t *testing.T) {
        // ...
    })
}
 
 
// go test -v 输出
=== RUN   TestReverse
=== RUN   TestReverse/sub_test1
=== RUN   TestReverse/sub_test2
--- PASS: TestReverse (0.00s)
    --- PASS: TestReverse/sub_test1 (0.00s)
    --- PASS: TestReverse/sub_test2 (0.00s)
PASS
ok      example/go_test 0.001s

go test 运行测试用例

  • -v:打印每个测试函数的名字和运行时间
  • -run:对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test测试命令运行
# 运行当前目录下的所有测试用例
go test
 
# 运行当前目录及其所有子目录的测试用例
go test ./...
 
# 运行某个测试文件
go test reverse_test.go
 
# 运行 reverse_test.go 中名为 TestReverse 的测试方法
go test -run ^TestReverse$ reverse_test.go

测试覆盖率

  • -cover:显示测试覆盖率
  • -coverprofile:将测试覆盖率信息输出到文件

在输出的测试信息中显示测试覆盖率结果

go test -cover
 
# 输出
PASS
        example/go_test coverage: 100.0% of statements
ok      example/go_test 0.001s

通过浏览器查看具体的覆盖率信息

# 输出测试覆盖率信息到 c.out 文件中
$ go test -coverprofile=c.out
 
# 生成一个 HTML 报告
$ go tool cover -html=c.out
HTML output written to /tmp/cover83233279/coverage.html

浏览器打开 /tmp/cover83233279/coverage.html

Image

主流测试框架

https://github.com/stretchr/testify

包含的特性:

package yours
 
import (
  "testing""github.com/stretchr/testify/assert"
)
 
func TestSomething(t *testing.T) {
 
  // assert equality
  assert.Equal(t, 123, 123, "they should be equal")
 
  // assert inequality
  assert.NotEqual(t, 123, 456, "they should not be equal")
 
  // assert for nil (good for errors)
  assert.Nil(t, object)
 
  // assert for not nil (good when you expect something)
  if assert.NotNil(t, object) {
 
    // now we know that object isn't nil, we are safe to make
    // further assertions without causing any errors
    assert.Equal(t, "Something", object.Value)
 
  }
}