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")
}
运行结果:
在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
方法中需要用到这个Client
的Address
。假设现在我们还没有配置完成Client
,但需要先对Serve
的行为进行测试。这时我们可以"mock"出一个Client
。
type Client interface {
Address() string
}
func Serve(c Client) {
// ...
address := client.Address()
// ...
}
接下来我们应该怎么做呢?
安装
gomock
生成mock文件
在单元测试中规定
Client
的操作以检查Serve
的行为安装
gomock
$ go install github.com/golang/mock/mockgen@v1.5.0
- 生成mock文件 在你的项目目录中运行如下命令:
$ mockgen -destination=mock_client.go -package=mock . Client
其中-destination
指定了需要生成的文件名, -package
指定了生成的mock文件的包名,若不指定,则默认为mock_
后跟输入文件的包名, 点.
指定了源目录,最后的Client
指需要mock该接口。
运行上面命令后,就可以在本地生成mock_client.go
文件。
- 在单元测试中规定
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}
复制代码可以看到确实通过注释生成了错误消息。