Go 开发、编译、发布体系
开发环境搭建
Go 环境搭建步骤
- 前往以下官网下载地址下载指定版本的 go 安装包,以
go1.20.5.linux-amd64.tar.gz
为例
- 解压 go 安装包到
/usr/local/go
中
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.5.linux-amd64.tar.gz
- 配置 go 环境变量
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
source ~/.profile
- 测试是否配置成功
go version
# 输出
go version go1.20.5 linux/amd64
Go 代码编辑软件
目前主流的 go 代码编辑软件有:GoLand,Visual Studio Code 等,由于 GoLand 收费,所以选择开源的 vscode 配合对应的 go 插件来编写 go 代码。
- 下载 vscode go 插件:
- 安装/更新 go tools
快捷键:ctrl + shift + p
调出命令窗口,输入 Go: Install/Update Tools
,点击第一项
选择所有工具,点击确定
等待所有工具安装完毕
Vscode Go 扩展取决于 gopls,dlv 和其他可选工具。如果缺少任何依赖项,则分析工具会显示警告⚠️ 。可以单击警告以下载依赖项。 有关扩展所依赖的工具的完整列表,请参阅工具文档。
- Go tools 在开发中的使用
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 工程
- 创建一个项目文件夹
mkdir go-demo
cd go-demo
- 初始化 go 模块
go mod init example.com/go-demo
模块路径 example.com/go-demo 应该是源代码的储存库位置,见:Go 文档 Naming a module
例如:github.com/gin-gonic/gin
- 创建一个
main.go
文件作为项目入口点,并写入如下内容
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
- 入口函数所在包名必须为
main
- 入口函数名为
main
,并且没有入参和返回值
- 运行代码
go run .
- 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 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
- 打印模块间的依赖关系图
- 以文本形式打印模块间的依赖关系图。输出的每一行行有两个字段(通过空格分割);模块和其所有依赖中的一个。
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 虽然没有固定的中央仓库但可以配置一个 GOPROXY 指定 Go Modules 的代理服务地址。指定 GOPROXY 后,go modules 会从 GOPROXY 配置的代理服务器中获取依赖。
常见的 Go modules 代理服务:
- Go 官方:
https://proxy.golang.org
- 中国 Go 社区(开源):
https://proxy.golang.com.cn
- 七牛云(开源):
https://goproxy.cn
- 阿里云:https://mirrors.aliyun.com/goproxy/
以七牛云为例配置 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 项目依赖第三方库的方式
- 内网中开放一个 Go modules 代理服务的地址,然后 Go 环境下配置 GOPROXY 为这个代理服务地址
- 内网中部署一个开源的 Go modules 代理服务器,然后 Go 环境下配置 GOPROXY 这个服务器的地址
开源的 Go Proxy 项目:
编译方式
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
:入口文件
交叉编译
- 列出支持交叉编译的系统架构
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
- 交叉编译为指定系统架构(windows 平台会自动添加 .exe 后缀)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o demo main.go
CGO_ENABLED
:是否启用 cgo,交叉编译不支持(CGO 可以支持与 C 语言接口的互通)GOOS
:编译的操作系统类型GOARCH
:编译的系统框架
发布形态
Go 项目的一般发布形态
编译为不同平台的二进制可执行文件发布
工具类项目的一般发布形态
Go 的第三方库是直接通过版本控制系统发布源代码。
- 进入项目根目录
- 运行
go mod tidy
,删除项目没有使用到的依赖。
go mod tidy
- 使用
git tag
命令给项目打上新的版本号
git commit -m "mymodule: changes for v0.1.0"
git tag v0.1.0
- 推送新的 tag 到远程仓库
git push origin v0.1.0
- 使用者通过
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
在语义版本模型中,已发布的模块会有一个版本号,如下图所示:
版本阶段 | 例子 | 描述 |
---|---|---|
开发中 | 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.go
结尾。
- 程序需要编写在
xxx_test.go
文件中(go 编译时会自动忽略掉后缀为_test.go
的文件) - 测试函数的命名必须以单词
Test
开始 - 测试函数只接受一个参数
t *testing.T
字符串反转的单元测试例子:
- 创建一个 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
- 将字符串反转的函数写入
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)
}
- 在
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)
}
}
}
- 运行测试用例
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
- 方法的子测试(一个用例下的多个测试场景)
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
主流测试框架
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)
}
}