主页

索引

模块索引

搜索页面

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 测试等
  1. 单元测试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 包的可导出标识符。
        这意味着我们的测试包将无法访问生产代码中的任何内部函数、变量或常量。
    
  2. 性能测试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,指定测试函数执行的超时时间
    
  3. 实例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 只会编译这些函数,但不会执行这些函数
    
  4. 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
    
  5. Mock 测试:

    GoMock 是由 Golang 官方开发维护的测试框架
    实现了较为完整的基于 interface 的 Mock 功能
        能够与 Golang 内置的 testing 包良好集成,也能用于其他的测试环境中。
    GoMock 测试框架包含了 GoMock 包和 mockgen 工具两部分
        a. GoMock 包用来完成对象生命周期的管理
        b. mockgen 工具用来生成 interface 对应的 Mock 类源文件
    
  6. 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 重制版)

主页

索引

模块索引

搜索页面