最近业务迁移,大约 100+ 个接口需要从旧的服务,迁到公司框架。遇到几个痛点:
这类工作要么手写(编译期), 要么 reflect 反射实现(运行时)。其中 #1 考滤到性能问题,手写最优,但是结构体太大,同时 100+ 个接口迁移,工作量可以想象
google 开源的 go-cmp[1], 输出美观,反射性能开销大了点。当前业务大量使用,堆机器吧又不是不能用
#2 目前不好解决,可以简单的 json Marshal 再 Unmarshal, 但有些字段类型不一致,同时如何做 json tag 到 pb tag 转换呢?
我们当前的方案是通过解析 ast, 读源码生成结构体树,然后 BFS 遍历自动生成转换代码
//go:generate ast-tools --action convert --target-pkg aaa/dto/geresponse --src-pkg bbb/dto --source aaaResponse --target bbbResponse
结合 go generate 自动生成,这是我们的目标
不搞编译器的大多只需要懂前端,不涉及 IR 与后端,同时 go 官方还提供了大量开箱即用的库 go/ast[2]
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
所有实现 Pos``End
的都是 Node
Comments
注释, //-style 或是 /*-styleDeclarations
声明,GenDecl
(generic declaration node) 代表 import, constant, type 或 variable declaration. BadDecl
代表有语法错误的 nodeStatements
常见的语句表达式,return, case, if 等等File
代表一个 go 源码文件Package
代表一组源代码文件Expr
表达式 ArrayExpr, StructExpr, SliceExpr 等等我们来看一个例子吧,goast可视化界面[3] 更直观一些
// Manager ...
type Manager struct {
Same string
All bool `json:"all"`
Version int `json:"-"`
NormalStruct pkgcmd.RootApp
PointerStruct *pkgcmd.RootApp
SlicesField []int
MapField map[string]string
}
我们定义结构体 Manager
来看一下 goast 输出结果
29 . 1: *ast.GenDecl {
30 . . Doc: nil
31 . . TokPos: foo:7:1
32 . . Tok: type
33 . . Lparen: -
34 . . Specs: []ast.Spec (len = 1) {
35 . . . 0: *ast.TypeSpec {
36 . . . . Doc: nil
37 . . . . Name: *ast.Ident {
38 . . . . . NamePos: foo:7:6
39 . . . . . Name: "Manager"
40 . . . . . Obj: *ast.Object {
41 . . . . . . Kind: type
42 . . . . . . Name: "Manager"
43 . . . . . . Decl: *(obj @ 35)
44 . . . . . . Data: nil
45 . . . . . . Type: nil
46 . . . . . }
47 . . . . }
*ast.GenDecl
通用声明,*ast.TypeSpec
代表是个类型的定义,名称是 Manager
48 . Assign: -
49 . Type: *ast.StructType {
50 . . Struct: foo:7:14
51 . . Fields: *ast.FieldList {
52 . . . Opening: foo:7:21
53 . . . List: []*ast.Field (len = 7) {
54 . . . . 0: *ast.Field {
55 . . . . . Doc: nil
56 . . . . . Names: []*ast.Ident (len = 1) {
57 . . . . . . 0: *ast.Ident {
58 . . . . . . . NamePos: foo:8:2
59 . . . . . . . Name: "Same"
60 . . . . . . . Obj: *ast.Object {
61 . . . . . . . . Kind: var
62 . . . . . . . . Name: "Same"
63 . . . . . . . . Decl: *(obj @ 54)
64 . . . . . . . . Data: nil
65 . . . . . . . . Type: nil
66 . . . . . . . }
67 . . . . . . }
68 . . . . . }
69 . . . . . Type: *ast.Ident {
70 . . . . . . NamePos: foo:8:12
71 . . . . . . Name: "string"
72 . . . . . . Obj: nil
73 . . . . . }
74 . . . . . Tag: nil
75 . . . . . Comment: nil
76 . . . . }
77 . . . . 1:
*ast.StructType
代表类型是结构体,*ast.Field
数组保存结构体成员声明,一共 7 个元素,第 0 个字段名称 Same
, 类型 string
131 . 3: *ast.Field {
132 . . Doc: nil
133 . . Names: []*ast.Ident (len = 1) {
134 . . . 0: *ast.Ident {
135 . . . . NamePos: foo:11:2
136 . . . . Name: "NormalStruct"
137 . . . . Obj: *ast.Object {
138 . . . . . Kind: var
139 . . . . . Name: "NormalStruct"
140 . . . . . Decl: *(obj @ 131)
141 . . . . . Data: nil
142 . . . . . Type: nil
143 . . . . }
144 . . . }
145 . . }
146 . . Type: *ast.SelectorExpr {
147 . . . X: *ast.Ident {
148 . . . . NamePos: foo:11:16
149 . . . . Name: "pkgcmd"
150 . . . . Obj: nil
151 . . . }
152 . . . Sel: *ast.Ident {
153 . . . . NamePos: foo:11:23
154 . . . . Name: "RootApp"
155 . . . . Obj: nil
156 . . . }
157 . . }
158 . . Tag: nil
159 . . Comment: nil
160 . }
*ast.SelectorExpr
代表该字段类型是 A.B,其中 A 代表 package, 具体 B 是什么类型不知道,还需要遍历包 A
221 . 6: *ast.Field {
222 . . Doc: nil
223 . . Names: []*ast.Ident (len = 1) {
224 . . . 0: *ast.Ident {
225 . . . . NamePos: foo:14:2
226 . . . . Name: "MapField"
227 . . . . Obj: *ast.Object {
228 . . . . . Kind: var
229 . . . . . Name: "MapField"
230 . . . . . Decl: *(obj @ 221)
231 . . . . . Data: nil
232 . . . . . Type: nil
233 . . . . }
234 . . . }
235 . . }
236 . . Type: *ast.MapType {
237 . . . Map: foo:14:21
238 . . . Key: *ast.Ident {
239 . . . . NamePos: foo:14:25
240 . . . . Name: "string"
241 . . . . Obj: nil
242 . . . }
243 . . . Value: *ast.Ident {
244 . . . . NamePos: foo:14:32
245 . . . . Name: "string"
246 . . . . Obj: nil
247 . . . }
248 . . }
249 . . Tag: nil
250 . . Comment: nil
251 . }
252 }
*ast.MapType
代表类型是字段,Key
, Value
分别定义键值类型
内容有点多,大家感兴趣自行实验
看懂了 go ast 相关基础,我们就可以遍历获取结构体树形结构,广度 + 深度相结合
func (p *Parser) IterateGenNeighbours(dir string) {
path, err := filepath.Abs(dir)
if err != nil {
return
}
p.visitedPkg[dir] = true
pkgs, err := parser.ParseDir(token.NewFileSet(), path, filter, 0)
if err != nil {
return
}
todo := map[string]struct{}{}
for pkgName, pkg := range pkgs {
nbv := NewNeighbourVisitor(path, p, todo, pkgName)
for _, astFile := range pkg.Files {
ast.Walk(nbv, astFile)
}
// update import specs per file
for name := range nbv.locals {
fmt.Sprintf("IterateGenNeighbours find struct:%s pkg:%s path:%s\n", name, nbv.locals[name].importPkg, nbv.locals[name].importPath)
nbv.locals[name].importSpecs = nbv.importSpec
}
}
for path := range todo {
dir := os.Getenv("GOPATH") + "/src/" + strings.Replace(path, "\"", "", -1)
if _, visited := p.visitedPkg[dir]; visited {
continue
}
p.IterateGenNeighbours(dir)
}
}
这里的工作量比较大,涉及 import 包,调试了很久,有些 linter 只需读单一文件即可,工作量没法比
最后一步就是输出结果,这里要 BFS 广度遍历结构体树,然后渲染模板
var convertSlicePointerScalarTemplateString = `
{% if ArrayLength == "" %}
dst.{{ TargetFieldName }} = make([]{{ TargetType }}, len(src.{{ SrcFieldName }}))
{% endif %}
for i := range src.{{ SrcFieldName }} {
if src.{{ SrcFieldName }}[i] == nil {
continue
}
tmp := *src.{{ SrcFieldName }}[i]
dst.{{ TargetFieldName }}[i] = &tmp
}
上面是转换 [8]*Scalar
可以是数组或切片,模板使用 pongo2[4] 实现的 jinji2 语法,非常强大
// ConvertDtoInsuranceOptionToCommonInsuranceOptionV2 only convert exported fields
func ConvertDtoInsuranceOptionToCommonInsuranceOptionV2(src *dto.InsuranceOption) *common.InsuranceOptionV2 {
if src == nil {
return nil
}
dst := &common.InsuranceOptionV2{}
dst.ID = src.ID
dst.OptionPremium = src.OptionPremium
dst.InsuranceSignature = src.InsuranceSignature
dst.Title = src.Title
dst.Subtitle = src.Subtitle
dst.ErrorText = src.ErrorText
dst.IsIncluded = src.IsIncluded
starCurrency := ConvertDtoCurrencyDTOToCommonCurrencyDTO(src.Currency)
if starCurrency != nil {
dst.Currency = *starCurrency
}
return dst
}
上面是输出结果的样例,整体来讲比手写靠谱多了,遇到个别 case 还是需要手工 fix
工作当中用到编译原理的场景非常多,比如去年高老板分享的[用规则引擎让你一天上线十个需求]
If aa.bb.cc == 1 // 说明是多车型发单
Unmarshal(bb.cc.ee)
看type是否为 4
else // 单车型发单
Unmarshal(bb.cc.ff)
看type是否为 4
(type = 4 的是拼车)
业务需要多种多样,订阅 MQ 根据需求做各种各样的统计,入库,供业务查询。如果业务类型少还好,但是 DIDI 业务复杂,如果每次都人工手写 go 代码效率太低
最后解决思路是 JPATH + Expression Eval
, 需求只需要写表达式,服务解析表达示即可。Eval 库也是现成的 govaluate[5]
jinja2 就是这类的代表
原理非常简单,感兴趣的可以看官方实现
这里要介绍两个项目 pingcap failpoint[6] 和 uber-go 的 gopatch
failpoint 实现很简单,代码里写 Marker
函数,这些空函数在正常编译时会被编译器优化去掉,所以正常运行时 zero-cost
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
故障注入时通过 failctl 将 Marker
函数转换为故障注入函数,这里就用到了 go-ast 做劫持转换
uber-go 的 gopatch 也非常强大,假如你的代码有很多 go func
开启的 goroutine, 你想批量加入 recover
逻辑,如果数据特别多人工加很麻烦,这时可以用 gopatcher
var patchTemplateString = `@@
@@
+ import "runtime/debug"
+ import "{{ Logger }}"
+ import "{{ Statsd }}"
go func(...) {
+ defer func(){
+ if err := recover(); err != nil {
+ statsd.Count1("{{ StatsdTag }}", "{{ FileName }}")
+ logging.Error("{{ LoggerTag }}", "{{ FileName }} recover from panic, err=%+v, stack=%v", err, string(debug.Stack()))
+ }
+ }()
...
}()
`
编写模板,上面的例子自动在 go func(...) {
开头注入 recover
语句块,非常方便
这个库能做的事情特别多,感兴趣自行实验
大部分 linter 工具都是用 go ast 实现的,比如对于大写的 Public 函数,如果没有注释报错
// BuildArgs write a
func BuildArgs() {
var a int
a = a + bbb.c
return a
}
我们看下该代码的 ast 代码
29 . . 1: *ast.FuncDecl {
30 . . . Doc: *ast.CommentGroup {
31 . . . . List: []*ast.Comment (len = 1) {
32 . . . . . 0: *ast.Comment {
33 . . . . . . Slash: foo:7:1
34 . . . . . . Text: "// BuildArgs write a"
35 . . . . . }
36 . . . . }
37 . . . }
38 . . . Recv: nil
39 . . . Name: *ast.Ident {
40 . . . . NamePos: foo:8:6
41 . . . . Name: "BuildArgs"
42 . . . . Obj: *ast.Object {
43 . . . . . Kind: func
44 . . . . . Name: "BuildArgs"
45 . . . . . Decl: *(obj @ 29)
46 . . . . . Data: nil
47 . . . . . Type: nil
48 . . . . }
49 . . . }
linter 只需要检查 FuncDecl
的 Name 如果是可导出的,同时 Doc.CommentGroup
不存在,或是注释不以函数名开头,报错即可
另外如果大家对代码 cycle 有要求,那么是不是可以 ast 扫一遍来发现呢?如果大家要求函数不能超过 100 行,是不是也可以实现呢?
玩法很多 ^^
编译原理虽然难,但是搞业务的只需要前端知识即可,不用研究的太深,有需要的场景,知道 AST 如何解决问题就行
关于 Go AST
大家有什么看法,欢迎留言一起讨论
[1]go-cmp: https://github.com/google/go-cmp,
[2]go ast: https://pkg.go.dev/go/ast,
[3]goast-viewer 可视化界面: https://yuroyoro.github.io/goast-viewer/index.html,
[4]go pongo2 jinja2: github.com/flosch/pongo2,
[5]govaluate: https://github.com/Knetic/govaluate,
[6]pingcap failpoint: https://github.com/pingcap/failpoint,
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ZxqNKbh-tahT41Qqkmk0aA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。