2.1.8. 代码规范¶
Makefile¶
编写一个高质量的 Makefile 4 个方法:
1. 打好基础,也就是熟练掌握 Makefile 的语法
2. 做好准备工作,也就是提前规划 Makefile 要实现的功能
3. 进行规划,设计一个合理的 Makefile 结构
4. 掌握方法,用好 Makefile 的编写技巧
目录实例:
├── Makefile
├── scripts
│ ├── gendoc.sh
│ ├── make-rules
│ │ ├── gen.mk
│ │ ├── golang.mk
│ │ ├── image.mk
│ │ └── ...
│ └── ...
技巧 1:善用通配符和自动变量:
通配符是 %: 可以使不同的目标使用相同的规则,从而使 Makefile 扩展性更强,也更简洁
如:
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
下面实例均可以使用上面定义的规则:
make tools.verify.swagger
make tools.verify.mockgen
自动变量 $*,用来指代被匹配的值 swagger、mockgen
技巧 2:善用函数:
Makefile 常用函数列表:
@todo
技巧 3:依赖需要用到的工具:
如果 Makefile 某个目标的命令中用到了某个工具,可以将该工具放在目标的依赖中
技巧 4:把常用功能放在 /Makefile 中,不常用的放在分类 Makefile 中:
一个项目,尤其是大型项目,有很多需要管理的地方,其中大部分都可以通过 Makefile 实现自动化操作。
不过,为了保持 /Makefile 文件的整洁性,我们不能把所有的命令都添加在 /Makefile 文件中。
一个比较好的建议是:
将常用功能放在 /Makefile 中
不常用的放在分类 Makefile 中,并在 /Makefile 中 include 这些分类 Makefile
技巧 5:编写可扩展的 Makefile:
定义: 可扩展的 Makefile:
可以在不改变 Makefile 结构的情况下添加新功能
扩展项目时,新功能可以自动纳入到 Makefile 现有逻辑中
技巧 6:将所有输出存放在一个目录下,方便清理和查找:
通常我们可以把它们放在_output 这类目录下,这样清理时就很方便,只需要清理_output 文件夹就可以
.PHONY: go.clean
go.clean:
@echo "===========> Cleaning all build output"
@-rm -vrf $(OUTPUT_DIR)
注意: 要用 -rm,而不是 rm,防止在没有_output 目录时,执行 make go.clean 报错
技巧 7:使用带层级的命名方式:
例如 tools.verify.swagger ,我们可以实现目标分组管理。
这样做的好处有很多:
1. 首先,当 Makefile 有大量目标时,通过分组,我们可以更好地管理这些目标
2. 其次,分组也能方便理解,可以通过组名一眼识别出该目标的功能类别
3. 最后,这样做还可以大大减小目标重名的概率
技巧 8:做好目标拆分:
比如,我们可以将安装工具拆分成两个目标:
1. 验证工具是否已安装
2. 安装工具
通过这种方式,可以给我们的 Makefile 带来更大的灵活性
例如:我们可以根据需要选择性地执行其中一个操作,也可以两个操作一起执行
技巧 9:设置 OPTIONS:
1. 首先,在 /Makefile 中定义 USAGE_OPTIONS:
define USAGE_OPTIONS
Options:
...
BINS The binaries to build. Default is all of cmd.
...
...
V Set to 1 enable verbose build. Default is 0.
endef
export USAGE_OPTIONS
2. 在 scripts/make-rules/common.mk 文件中,通过判断有没有设置 V 选项来选择不同的行为:
ifndef V
MAKEFLAGS += --no-print-directory
endif
or:
ifeq ($(origin V), undefined)
MAKEFLAGS += --no-print-directory
endif
技巧 10:定义环境变量:
GO := go
GO_SUPPORTED_VERSIONS ?= 1.13|1.14|1.15|1.16|1.17
GO_LDFLAGS += -X $(VERSION_PACKAGE).GitVersion=$(VERSION) \
-X $(VERSION_PACKAGE).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PACKAGE).GitTreeState=$(GIT_TREE_STATE) \
-X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
ifneq ($(DLV),)
GO_BUILD_FLAGS += -gcflags "all=-N -l"
LDFLAGS = ""
endif
GO_BUILD_FLAGS += -tags=jsoniter -ldflags "$(GO_LDFLAGS)"
...
FIND := find . ! -path './third_party/*' ! -path './vendor/*'
作用:只要修改一处,就可以使很多地方同时生效,避免了重复的工作
一个大型 Go 项目通常需要实现以下功能:
1. 代码生成类命令
2. 格式化类命令
3. 静态代码检查
4. 测试类命令
5. 构建类命令
6. Docker 镜像打包类命令
7. 部署类命令
8. 清理类命令
测试¶
测试用例函数必须以 Test、Benchmark、Example 开头:
1. TestXxx: 单元测试
2. BenchmarkXxx: 性能测试
3. ExampleXxx: 示例测试
其他测试类型:
4. TestMain 函数
5. Mock 测试
6. Fake 测试等
单元测试TestXxx:
*testing.T 为了清晰地表达函数的实际输出和预期输出 可以将这两类输出命名为 expected/actual,或者 got/want: 如: if c.expected != actual { t.Fatalf("Expected User-Agent '%s' does not match '%s'", c.expected, actual) } or if got, want := diags[3].Description().Summary, undeclPlural; got != want { t.Errorf("wrong summary for diagnostic 3\ngot: %s\nwant: %s", got, want) } 包的命名规范: Go 的测试可以分为白盒测试和黑盒测试。 a. 白盒测试 将测试和生产代码放在同一个 Go 包中,这使我们可以同时测试 Go 包中可导出和不可导出的标识符。 当我们编写的单元测试需要访问 Go 包中不可导出的变量、函数和方法时,就需要编写白盒测试用例。 b. 黑盒测试 将测试和生产代码放在不同的 Go 包中。 这时,我们仅可以测试 Go 包的可导出标识符。 这意味着我们的测试包将无法访问生产代码中的任何内部函数、变量或常量。
性能测试BenchmarkXxx:
*testing.B 函数内以 b.N 作为循环次数 其中 N 会在运行时动态调整,直到性能测试函数可以持续足够长的时间,以便能够可靠地计时 func BenchmarkRandInt(b *testing.B) { for i := 0; i < b.N; i++ { RandInt() } } 使用: $ go test -bench=".*" goos: linux goarch: amd64 pkg: github.com/marmotedu/gopractise-demo/31/test BenchmarkRandInt-4 97384827 12.4 ns/op PASS ok github.com/marmotedu/gopractise-demo/31/test 1.223s 说明: BenchmarkRandInt-4: BenchmarkRandInt 表示所测试的测试函数名 4 表示有 4 个 CPU 线程参与了此次测试,默认是 GOMAXPROCS 的值。 90848414: 说明函数中的循环执行了 90848414 次。 12.8 ns/op: 说明每次循环的执行平均耗时是 12.8 纳秒,该值越小,说明代码性能越高。 B 类型的性能测试还支持下面 4 个参数: a. benchmem,输出内存分配统计 b. benchtime,指定测试时间和循环执行次数(格式需要为 Nx,例如 100x) c. cpu,指定 GOMAXPROCS d. timeout,指定测试函数执行的超时时间
实例ExampleXxx:
说明: 有 fmt.Println/fmt.Printf 这类输出的时候使用 示例测试可能包含: 1. 以Output: 开头的注释 2. 以Unordered output: 开头的注释 这些注释放在函数的结尾部分 Unordered output: 开头的注释会忽略输出行的顺序 示例: func ExampleMax() { fmt.Println(Max(1, 2)) // Output: // 2 } 使用: $ go test -v -run='Example.*' === RUN ExampleMax --- PASS: ExampleMax (0.00s) PASS ok github.com/marmotedu/gopractise-demo/31/test 0.004s 说明: 当示例测试不包含 Output: 或者 Unordered output: 注释时 执行 go test 只会编译这些函数,但不会执行这些函数
TestMain 函数:
*testing.M 简介: 1. 先做一些准备工作 2. 其他单元测试... 3. 最后再做一些收尾工作 说明: TestMain 是一个特殊的函数(相当于 main 函数) 测试用例在执行时,会先执行 TestMain 函数,然后可以在 TestMain 中调用 m.Run() 函数执行普通的测试函数 在 m.Run() 函数前面我们可以编写准备逻辑,在 m.Run() 后面我们可以编写清理逻辑 示例: // https://github.com/marmotedu/gopractise-demo/blob/master/31/test/math_test.go func TestMain(m *testing.M) { fmt.Println("do some setup") m.Run() fmt.Println("do some cleanup") } 执行: $ go test -v do some setup === RUN TestAbs --- PASS: TestAbs (0.00s) ... === RUN ExampleMax --- PASS: ExampleMax (0.00s) PASS do some cleanup ok github.com/marmotedu/gopractise-demo/31/test 0.006s
Mock 测试:
GoMock 是由 Golang 官方开发维护的测试框架 实现了较为完整的基于 interface 的 Mock 功能 能够与 Golang 内置的 testing 包良好集成,也能用于其他的测试环境中。 GoMock 测试框架包含了 GoMock 包和 mockgen 工具两部分 a. GoMock 包用来完成对象生命周期的管理 b. mockgen 工具用来生成 interface 对应的 Mock 类源文件
Fake 测试:
对于比较复杂的接口,可以 Fake 一个接口实现,来进行测试 所谓 Fake 测试,其实就是针对接口实现一个假(fake)的实例 至于如何实现 Fake 实例,需要你根据业务自行实现
测试框架:
1. Testify 框架
Testify 是 Go test 的预判工具,它能让你的测试代码变得更优雅和高效,测试结果也变得更详细
2. GoConvey 框架
GoConvey 是一款针对 Golang 的测试框架
可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性
Mock 工具:
1. sqlmock
可以用来模拟数据库连接。数据库是项目中比较常见的依赖,在遇到数据库依赖时都可以用它
2. httpmock
可以用来 Mock HTTP 请求
3. bouk/monkey
猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现
如果 golang/mock、sqlmock 和 httpmock 这几种方法都不能满足我们的需求
可以尝试用猴子补丁的方式来 Mock 依赖
可以这么说,猴子补丁提供了单元测试 Mock 依赖的最终解决方案
参考¶
陈皓, 《跟我一起写 Makefile》 (PDF 重制版)