1. go generate

go generate命令运行时,将找到源代码中所有包含//go:generate的特殊注释,提取并执行//go:generate后附加的命令。
基本语法:

//go:generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

需要注意的几点:

  • 该特殊注释必须在.go源码文件中。
  • 每个源码文件可以包含多个generate特殊注释。
  • go generate只在运行go generate命令时运行,go build, go get, go test等其他命令不会运行它。
  • 命令串行执行的,如果出错,就终止后面的执行。
  • 特殊注释必须以"//go:generate"开头,双斜线后面没有空格。

简单的例子:

package main

import "fmt"

//go:generate echo "world"
func main() {
	fmt.Println("hello")
}

运行结果:
20210528175852

go generate命令中,还可以使用一些环境变量:

    $GOARCH
        The execution architecture (arm, amd64, etc.)
    $GOOS
        The execution operating system (linux, windows, etc.)
    $GOFILE
        The base name of the file.
    $GOLINE
        The line number of the directive in the source file.
    $GOPACKAGE
        The name of the package of the file containing the directive.
    $DOLLAR
        A dollar sign.

go generate的参数

-run 正则表达式匹配命令行,仅执行匹配的命令
-v 输出被处理的包名和源文件名
-n 只显示要执行的命令,但不执行
-x 显示要执行的命令并执行

2. 使用go generate自动生成mock接口

在我们对代码进行单元测试时,某段代码可能有一些依赖项,一般情况下我们可以手动去构建这些依赖项。但当依赖项过多时,手动去构建每一个依赖项就是一项复杂、艰巨且无聊的工作。这时候,就到了mock大显身手的时候。mock会为你提供一些虚拟的依赖项,并规定它们的行为,从而你可以方便的在自己的测试中使用它。

gomock针对接口进行mock操作。

例如:我们有一个Client接口,这个接口的Address方法返回其地址,Serve方法中需要用到这个ClientAddress。假设现在我们还没有配置完成Client,但需要先对Serve的行为进行测试。这时我们可以"mock"出一个Client

type Client interface {
    Address() string
}

func Serve(c Client) {
    // ...
    address := client.Address() 
    // ...
}

接下来我们应该怎么做呢?

  1. 安装gomock

  2. 生成mock文件

  3. 在单元测试中规定Client的操作以检查Serve的行为

  4. 安装gomock

$ go install github.com/golang/mock/mockgen@v1.5.0
  1. 生成mock文件 在你的项目目录中运行如下命令:
$ mockgen -destination=mock_client.go -package=mock . Client

其中-destination指定了需要生成的文件名, -package指定了生成的mock文件的包名,若不指定,则默认为mock_后跟输入文件的包名, 点.指定了源目录,最后的Client指需要mock该接口。
运行上面命令后,就可以在本地生成mock_client.go文件。

  1. 在单元测试中规定Client的操作以检查Serve的行为
func Test_Serve(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    c := NewMockClient(ctrl)  // 构建出mock的Client

    c.EXCEPT().Address().RETURN("127.0.0.1").AnyTimes()  // 当使用Client.Address()方法时返回”127.0.0.1“,可使用任意次

    Server(c)
    // ...
}

每次使用c.EXCEPT().Address().RETURN()仅能为一次Address()指定返回,可多次使用该方法以返回不同的值。

最后,mock和go generate有啥关系?
我们可以将go generate特殊注释写在接口的头部,然后直接使用go generate命令来方便快捷的生成mock文件。如下:

//go:generate mockgen -destination=mock_client.go -package=mock . Clien
type Client interface {
    Address() string
}

3. 使用go generate生成错误码

stringer是用于自动创建满足fmt.Stringer方法的工具。给定一个变量/常量名T, stringer可以生成类似下面方法的代码:

func (t T) String string 

以下内容主要参考了: 深入理解Go之generate

在我们的服务中,经常会使用一些错误码,这时就需要我们去定义errCode和ErrMsg, 这里介绍一种优雅的方法解决这个问题。

定义错误码的传统方式

定义错误码:

package errcode

import "fmt"

// 定义错误码
const (
    ERR_CODE_OK = 0 // OK
    ERR_CODE_INVALID_PARAMS = 1 // 无效参数
    ERR_CODE_TIMEOUT = 2 // 超时
    // ...
)

// 定义错误码与描述信息的映射
var mapErrDesc = map[int]string {
    ERR_CODE_OK: "OK",
    ERR_CODE_INVALID_PARAMS: "无效参数",
    ERR_CODE_TIMEOUT: "超时",
    // ...
}

// 根据错误码返回描述信息
func GetDescription(errCode int) string {
    if desc, exist := mapErrDesc[errCode]; exist {
        return desc
    }
    
    return fmt.Sprintf("error code: %d", errCode)
}

使用错误码:

package main

import (
    "github.com/darjun/errcode"
)

func main() {
    code := errcode.ERR_CODE_INVALID_PARAMS
    fmt.Println(code, errcode.GetDescription(errCode))
    
    // 输出: 1 无效参数
}

为了使用方便,我们可以为错误码定义一个新的类型,然后为该类型定义String()方法,这样就不用手动调用GetDescription函数了。修改如下:

type ErrCode int

const (
    ERR_CODE_OK ErrCode = 0 // OK
    ERR_CODE_INVALID_PARAMS ErrCode = 1 // 无效参数
    ERR_CODE_TIMEOUT ErrCode = 2 // 超时
)

func (e ErrCode) String() string {
    return GetDescription(e)
}

这种方式有什么问题呢? 每次增加错误码时,都需要修改错误码到错误信息的map,有时候可能会忘, 另外,错误信息在注释和map中都出现了,有一定的冗余。 那能不能只写注释,然后自动生成代码呢?

使用

stringer有两种模式,默认是根据变量/常量名来生成字符串描述。我们在常量定义上增加注释:

//go:generate stringer -type ErrCode

选项-type指定stringer命令作用的类型名。 然后在同一个目录下执行:

$ go generate

这会在同一个目录下生成一个文件errcode_string.go。文件名格式是类型名小写_string.go。也可以通过-output选项指定输出文件名,例如下面就是指定输出文件名为code_string.go:

//go:generate stringer -type ErrCode -output code_string.go

我们来看看这个文件的内容:

// Code generated by "stringer -type ErrCode -output errcode_string.go"; DO NOT EDIT.

package errcode

import "strconv"

const _ErrCode_name = "ERR_CODE_OKERR_CODE_INVALID_PARAMSERR_CODE_TIMEOUT"

var _ErrCode_index = [...]uint8{0, 11, 34, 50}

func (i ErrCode) String() string {
	if i < 0 || i >= ErrCode(len(_ErrCode_index)-1) {
		return "ErrCode(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _ErrCode_name[_ErrCode_index[i]:_ErrCode_index[i+1]]
}

复制代码生成的代码做了一些优化,减少了字符串对象的数量。

这时ERR_CODE_INVALID_PARAMS.String()返回的描述信息是ERR_CODE_INVALID_PARAMS。在一些上下文中甚至不需要自己调用String()方法,如fmt.Println。因为ErrCode实现了fmt.Stringer,一些上下文中会自动调用。

这样errcode.go文件中mapErrDesc全局变量和getDescription函数都可以去掉了。

但是我们更希望的是能返回后面的注释作为错误描述。
这就需要使用stringer-linecomment选项。修改go:generate如下:

//go:generate stringer -type ErrCode -linecomment -output code_string.go

复制代码然后,执行go generate命令。生成的code_string.go与之前的有所不同,如下:

const _ErrCode_name = "OK无效参数超时"

var _ErrCode_index = [...]uint8{0, 2, 14, 20}

复制代码可以看到确实通过注释生成了错误消息。

Reference

深入理解Go之generate
GoMock
stringer
Package generate
go generate介绍