golang虽然是门很火的语言,但是其缺点也是很明显的。由于最初目标就是替换C语言,考虑到复杂性等各种原因没有引入泛型,而是采用了interface{}
这个带了类型的void*
。
所以可以看得到golang中极度恶心的sort.Interface
、sync.Map
等接口。
虽然go是门静态强类型语言,但是套上interface{}
这个大锅之后,代码可以写的风生水起,堪比脚本语言。当然与其相应的就是各种运行时的panic也是层出不穷,很大程度上失去了静态强类型语言的优势。
如何解决这些问题呢?完善的测试是一方面,之前在看KotlinConf 2019的视频是有一页ppt很让我印象深刻。
宏观来看,类型检查、编译、单元测试、集成测试、系统测试都可以是测试的一方面。而越底层的方式成本越低,所以相比真正的跑单元测试、系统测试,如果能在编译时解决这些问题当然更加高效。
golang中编译时没有检查的东西,一是reflect相关的代码,二是struct tag。本文就使用golang官方提供的代码分析库实现一个tag检查的工具。在构建时以linter的方式检查出代码中的错误。
这个包是golang专门用来做代码静态检查的包,使用很简单。只需要实现analysis.Analyzer
这个结构体即可
这个结构体最核心的就是一个Run func(*Pass) (interface{}, error)
方法
这个方法传入一个Pass
,对应这静态分析器的一个Pass
而我们就可以通过这个Pass来遍历整个所有代码做检查。这次我们的目标是检查tag的时候是否正确。
golang自带的json库支持自定义json key的名字,同时自带两个option:string
和omitempty
string代表序列化的时候使用json字符串格式,但是golang结构体中使用别的标量类型,比如int
,boolean
omitempty则代表如果该字段是零值那就不序列化到json中。
这里常见的误用就是将string option添加到类型就是string的字段上去。
接下来的事情就是遍历代码,检查tag了。
func(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
st, ok := node.(*ast.StructType)
if !ok {
return true
}
if st.Fields == nil {
return true
}
for _, f := range st.Fields.List {
if f.Tag == nil {
continue
}
tv, err := strconv.Unquote(f.Tag.Value)
if err != nil {
pass.Reportf(f.Tag.Pos(), "invalid tag:%q", render(pass.Fset, f.Tag))
continue
}
tags := reflect.StructTag(tv)
tag, ok := tags.Lookup(key())
if !ok {
continue
}
//TODO do some check
}
return true
})
}
return nil, nil
}
核心逻辑其实很简单,就是遍历pass中的每一个文件,然后遍历文件中的每一个语法元素,当遇到结构体时就检查结构体的字段的tag,这套代码对于检查任何tag都是通用的。
核心的check逻辑则如下
jsonTag := ParseJsonTag(tag)
if jsonTag.Skip {
return
}
//check tag name
if !isValidTag(jsonTag.Name) {
pass.Reportf(f.Tag.Pos(), "invalid name:%q", render(pass.Fset, f.Tag))
}
//check for string option
if jsonTag.String {
if !isStringable(pass.TypesInfo.TypeOf(f.Type)) {
pass.Report(analysis.Diagnostic{
Pos: f.Tag.Pos(),
End: f.Tag.End(),
Message: fmt.Sprintf("string must use on scalar field:%q", render(pass.Fset, f.Tag)),
})
}
}
这里的ParseJsonTag
、isStringable
其实是从go标准库里抠出来的,为了保持和标准库的逻辑一致。
如果不是string可以使用的类型,就调用Report上报错误。
核心函数包装成结构体之后,再使用golang.org/x/tools/go/analysis/singlechecker
包装一把,一个简单的linter就是实现了。
完整代码可以参见https://github.com/okhowang/gotaglint
其实golang的tag和java的annotation几乎一模一样,绝大部分场景都是运行时解析然后做一些特殊逻辑,差就差在golang的tag没有任何检查逻辑,而java的annotation是强类型。最终每个库的tag语法真的是千差万别。有像json这种逗号分隔的,复杂点像https://github.com/go-playground/validator这种带层级的,还有xorm这种带括号的,一个比一个复杂。
真正写代码的时候除非对库有很深入的了解,很难整体把握tag的使用方式。而第三方来实现tag的lint也缺少具体的解析代码,需要库作者把tag解析代码声明成大写...
最好的方式还是库作者同时实现一个tag lint来方便开发者检查自己的tag编写是否正确。避免真正跑到具体逻辑的时候再panic。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有