惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
The Last Watchdog
The Last Watchdog
腾讯CDC
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
Last Week in AI
Last Week in AI
量子位
雷峰网
雷峰网
宝玉的分享
宝玉的分享
美团技术团队
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
P
Privacy International News Feed
NISL@THU
NISL@THU
V
V2EX
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
IT之家
IT之家
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
V
Vulnerabilities – Threatpost
V
Visual Studio Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
Scott Helme
Scott Helme
H
Hacker News: Front Page
罗磊的独立博客
Hacker News - Newest:
Hacker News - Newest: "LLM"
K
Kaspersky official blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Cloudflare Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
月光博客
月光博客
人人都是产品经理
人人都是产品经理
AI
AI
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
PCI Perspectives
PCI Perspectives
A
Arctic Wolf

Mohuishou

如何实现支持多集群的 Kubernetes Operator? 第三方应用如何调用我们 kubebuilder 生成的自定义资源? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 4. localhost 就一定是 localhost 么? Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(四) 漏桶算法 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试 Go工程化(七) Go Module Go工程化(六) 配置管理 Go工程化(五) API 设计下: 基于 protobuf 自动生成 gin 代码 Go工程化(四) API 设计上: 项目结构 & 设计 Go工程化(三) 依赖注入框架 wire Go工程化(二) 项目目录结构 Go工程化(一) 架构整洁之道阅读笔记 Go并发编程(十一) 总结 Go并发编程(十) 深入理解 Channel Go并发编程(九) 深入理解 Context Go并发编程(八) 深入理解 sync.Once Go并发编程(七) 深入理解 errgroup Go并发编程(六) 深入理解 WaitGroup Go并发编程(五) 深入理解 sync/atomic Go并发编程(四) 深入理解 Mutex Go并发编程(三) data race Go并发编程(二) Go 内存模型 Go并发编程(一) goroutine Go错误处理最佳实践 微服务(二) 服务发现&多租户 微服务(一) 微服务概览 5. 栈下: 深入理解 defer 4. 栈上: 如何实现一个计算器 Go Struct 初始化风格的抉择 3. 数组下: 使用 GDB 调试 Golang 代码 2. 数组上: 深入理解 slice 1. 链表: 深入理解container/list&LRU缓存的实现 Go设计模式24-总结(更新完毕) Go设计模式23-中介模式 Go设计模式22-解释器模式 Go设计模式21-命令模式 Go设计模式20-备忘录模式 Go设计模式19-访问者模式 Go设计模式18-迭代器模式 Go设计模式17-状态模式 Go设计模式16-职责链模式(Gin的中间件实现) Go设计模式15-策略模式 Go模板模式14-模板模式 Go设计模式13-观察者模式(实现简单的EventBus) Go设计模式12-享元模式 Go设计模式11-组合模式 Go设计模式10-门面模式 Go设计模式09-适配器模式 Go设计模式08-装饰器模式 Go设计模式07-桥接模式 Go设计模式06-代理模式(generate实现类似动态代理) Go设计模式05-创建型模式总结 Go设计模式04-原型模式 Go设计模式03-建造者模式 Go设计模式02-工厂模式&DI容器 笔记-让你最快速地改善代码质量的20条编程规范 Go设计模式01-单例模式 一点拙见-如何写好一个技术预研报告? Go Web小技巧(四)在单个仓库中支持多个 go mod 模块 Go Web 小技巧(三)Gin 参数绑定 Go Web 小技巧(二)GORM 使用自定义类型 Go Web 小技巧(一)简化Gin接口代码 善用工具之postman高级用法概述 hexo-next-algolia-search全文搜索 docker镜像瘦身&优化 GORM避坑指南之含关联关系的更新 Github Actions介绍&自动构建Github Pages博客 在blog中内嵌在线PPT 记一次net http内存泄漏 使用TravisCI自动部署Blog 使用Goland调试Go程序 一个十分边缘的gorm的bug Httprouter介绍及源码阅读 Gin源码阅读 从0.1开始
go generate and ast
mohuishou <1@lailin.xyz> · 2019-05-07 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

楔(xiē)子

最近写API CURD比较多,为了结构清晰,返回值需要统一错误码,所以在一个统一的errcode包中定义错误码常量,以及其错误信息.

如下图所示,由于常量是导出字符 -> golint 检测需要编写注释 -> 注释信息其实就是错误信息,已经在下文的msg map[int]string中定义,如果在写就得写两遍

不写,就满屏波浪线,不能忍!

写了,就得Copy一份,还不利于维护,不能忍!

能不能只写一份注释,剩下的msg通过读取注释信息自动生成,将我们宝(hua)贵(diao)的生命,从这些重复繁杂无意义的劳动中解放出来。

为了实现这个伟大的目标, 需要以下两个关键的数据:

  1. 解析源代码获取常量与注释之间的关系 -> 🌲Go 抽象语法树: AST[3]
  2. 从 Go 源码生成 Go 代码 -> 👏 go generate[5]

golang1.4版本中引入了go generate命令,常用于文件生成,例如在 Golang 官方博客[5]中介绍的Stringer可以为枚举自动实现Stringer的方法,从业务代码中解放出来

💻 命令文档

使用go help generate我们可以查看一下命令的帮助文档

1
2
3
▶ go help generate
usage: go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]
...

解释很长,就不贴上来了,简要的概括一下:

  1. 参数说明

    • -run 正则表达式匹配命令行,仅执行匹配的命令(和go test -run类似)
    • -v 打印  已被检索处理的文件。
    • -n 打印出将被执行的命令,此时将不真实执行命令
    • -x 打印已执行的命令
  2. 举个栗子

    1
    2
    3
    4
    # 对当前包下的Go文件进行处理, 并打印已被检索处理的文件。
    go generate -v
    # 打印当前目录下所有文件中将要被执行的命令(实际不会执行)
    go generate -n ./...
  3. go generate会扫描.go源码文件中的注释//go:generate command args..., 并且执行其命令,注意:

    • 这些命令是为了更新或者创建 Go 源文件
    • command必须是可执行的指令,例如在 PATH 中或者使用绝对路径
    • arg如果带引号会被识别成一个参数, 例如: //go:generate command "x1 x2", 这条语句执行的命令只有一个参数
    • 注释中//go之间没有空格
  4. go generate必须手动执行,如果想等着go build, go test, go run 命令执行的时候自动执行,可以洗洗睡了

  5. 为了让别人或者是 IDE 识别代码是通过go generate生成的,请在生成的代码中添加注释(一般放在文件开头)

    1
    2
    # PS: 这是一个正则表达式
    ^// Code generated .* DO NOT EDIT\.$

    举个栗子:

    1
    2
    3
    // Code generated by mohuishou DO NOT EDIT

    package painkiller

  6. go generate在执行的时候会自动注入以下环境变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $GOARCH
    系统架构: arm, amd64 等
    $GOOS
    操作系统: linux, windows 等
    $GOFILE
    当前执行的命令所处的文件名
    $GOLINE
    当前执行的命令在文件中的行号
    $GOPACKAGE
    执行的命令所处的文件的包名
    $DOLLAR
    $ 符号

🌰 Go 官方博客中给出的栗子

源文件: painkiller.go

1
2
3
4
5
6
7
8
9
10
11
12
13
//go:generate stringer -type=Pill

package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

执行命令

1
go generate

生成文件: painkiller_stringer.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// generated by stringer -type Pill pill.go; DO NOT EDIT

package painkiller

import "fmt"

const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"

var _Pill_index = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

从上面的 🌰,我们可以发现,在.go源文件中,添加了一行注释go:generate stringer -type=Pill, 执行命令go generate就调用stringer命令在同目录下生成了一个新的_stringer.go的文件

回想一下上文提到的需求,是不是感觉很类似,从 Go 源文件中,生成了一些不想重复写的业务逻辑

🌲 AST

回到前面的需求,我们需要从源代码中获取常量和注释之前的关系,这时就需要我们的 🌲AST 隆重登场了。

本文不对 AST 过多介绍,可以阅读参考资料中的 AST 标准库文档[3],Go 的 AST(抽象语法树)[4]

简要介绍一下 AST 包

基础的接口类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Node AST树节点
type Node interface {
Pos() token.Pos
End() token.Pos
}

// Expr 所有的表达式都需要实现Expr接口
type Expr interface {
Node
exprNode()
}

// Stmt 所有的语句都需要实现Stmt接口
type Stmt interface {
Node
stmtNode()
}

// Decl 所有的声明都需要实现Decl接口
type Decl interface {
Node
declNode()
}

等会儿可能会用到的ValueSpec

1
2
3
4
5
6
7
8
// ValueSpec 表示常量声明或者变量声明
type ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}

在 godoc[3]的 Example 中可以发现有一个CommentMap例子

1
2
// CommentMap把AST节点和其关联的注释列表进行映射
type CommentMap map[Node][]*CommentGroup
  1. 通过parse读取源码创建一个 AST

    1
    2
    3
    4
    5
    fset := token.NewFileSet() // positions are relative to fset
    f, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
    if err != nil {
    panic(err)
    }
  2. 从 AST 中新建一个CommentMap

    1
    cmap := ast.NewCommentMap(fset, f, f.Comments)

需求实现

1. 获取常量和注释的关联关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
file := os.Getenv("GOFILE")
// 保存注释信息
var comments = make(map[string]string)

// 解析代码源文件,获取常量和注释之间的关系
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
checkErr(err)

// Create an ast.CommentMap from the ast.File's comments.
// This helps keeping the association between comments
// and AST nodes.
cmap := ast.NewCommentMap(fset, f, f.Comments)
for node := range cmap {
// 仅支持一条声明语句,一个常量的情况
if spec, ok := node.(*ast.ValueSpec); ok && len(spec.Names) == 1 {
// 仅提取常量的注释
ident := spec.Names[0]
if ident.Obj.Kind == ast.Con {
// 获取注释信息
comments[ident.Name] = getComment(ident.Name, spec.Doc)
}
}
}

2. 获取注释信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// getComment 获取注释信息,来自AST标准库的summary方法
func getComment(name string, group *ast.CommentGroup) string {
var buf bytes.Buffer

for _, comment := range group.List {
// 注释信息会以 // 参数名,开始,我们实际使用时不需要,去掉
text := strings.TrimSpace(strings.TrimPrefix(comment.Text, fmt.Sprintf("// %s", name)))
buf.WriteString(text)
}

// replace any invisibles with blanks
bytes := buf.Bytes()
for i, b := range bytes {
switch b {
case '\t', '\n', '\r':
bytes[i] = ' '
}
}

return string(bytes)
}

3. 生成代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const suffix = "_msg_gen.go"

// tpl 生成代码需要用到模板
const tpl = `
// Code generated by github.com/mohuishou/gen-const-msg DO NOT EDIT

// {{.pkg}} const code comment msg
package {{.pkg}}

// noErrorMsg if code is not found, GetMsg will return this
const noErrorMsg = "unknown error"

// messages get msg from const comment
var messages = map[int]string{
{{range $key, $value := .comments}}
{{$key}}: "{{$value}}",{{end}}
}

// GetMsg get error msg
func GetMsg(code int) string {
var (
msg string
ok bool
)
if msg, ok = messages[code]; !ok {
msg = noErrorMsg
}
return msg
}
`

// gen 生成代码
func gen(comments map[string]string) ([]byte, error) {
var buf = bytes.NewBufferString("")

data := map[string]interface{}{
"pkg": os.Getenv("GOPACKAGE"),
"comments": comments,
}

t, err := template.New("").Parse(tpl)
if err != nil {
return nil, errors.Wrapf(err, "template init err")
}

err = t.Execute(buf, data)
if err != nil {
return nil, errors.Wrapf(err, "template data err")
}

return format.Source(buf.Bytes())
}

总结

从一个简单的效率需求引申到go generateast的使用,顺便阅读了一下ast的源码,花费的时间其实可能是这个工具节约的时间的几倍了,但是收获也是之前没有想到的。

  1. 使用了这么久的go命令,详细的阅读了go help command的说明之后,发现之前可能连了解都算不上
  2. 标准库的godoc是最好的使用说明,第二好的是它的源代码

参考资料

  1. go-const-msg 本文实现的源代码

  2. Golang Generate 命令说明与使用

  3. AST 标准库文档

  4. Go 的 AST(抽象语法树)

  5. GO 官方博客: Generating code

关注我获取更新

猜你喜欢