golang硬核技术(六)编译器开发,自定义语法糖,告别 if err != nil { return err }

wdshihaoren · · 300 次点击 · · 开始浏览    

[原文](https://juejin.cn/post/7413356439476109327) ## 前言 不管是在其他语言的社区里,还是go的社区里,下面这三行代码都不断的被吐槽 ```go if err != nil { return err } ``` 这归因于go语言独特的错误处理,这里我们不讨论这样的错误处理是否合理,单纯从代码之美的角度看,这样的写法是如此的丑陋。 尤其,当你有很多错误需要处理的时候,就会发现通篇都是这三行。 因此,本文我们将修改编译器,优化这一写法。 ## 目标 在所有语言的错误处理中,我比较喜欢的是rust的处理方式,同样是返回错误,我们只需要在函数的末尾加上`?`号,即可将错误返回,如下示例: ```rust fn main() ->io::Result<()> { //读取文件并打印,如果出现错误则返回错误,并停止执行 let data = std::fs::read_to_string("test.json") ? ; println!("{}",data); Ok(()) } ``` 所以,我们的目标是给go增加一个"?"的语法,能够将错误返回出来。 * 思考:go的返回值可以有多个,如果我们直接`return err`会导致多返回值的函数无法使用,所以我们需要让函数声明默认的返回值变量,我们只需要`return`就可以了。 预期达到的效果如下: ```go func EasyRetError(info string) (err error) { err = NewError(info)? //todo } ``` 它等价于下面这段函数 ```go func EasyRetError(info string) (err error) { err = NewError(info) if err != nil { return } //todo } ``` ## 实现 ### 0. 思路 go的编译过程大概分为这麽几个阶段: 1. 扫描解析源文件 2. 类型检查和AST生成 3. 生成SSA中间代码,并进行一定优化 4. 生成机器码 基于这个过程,我们的"?"号语法糖,只需要在编译器解析源码的时候,将?号扩展为`if err != nil { return } `即可 ### 1. 拉取go的源代码,并尝试编译。 在正式开始开工之前,需要在本地先安装一个go的执行环境,并且尽量用最新的版本。然后clone go的源码包。下面我们所有的操作,都相对于这个目录来完成。 ```bash git clone https://github.com/golang/go.git ``` 如果你不想把go源码中,最新的pull request 带到你的编译器中,也可以从go的release版本中,下载一个。但要注意,你本地已经安装的go的版本,尽量只比你要编译的版本小一个版本号。 比如我这里用`go version go1.21.11 darwin/amd64`编译`go version go1.22.0 darwin/amd64` 代码拉下来后,可以先编译一下,确定默认编译不会出问题。 * all.bash 编译完成后,会自动进行测试,时间还是比较长的。并且给了多系统的命令文件 all.bat all.rc * make.bash 仅编译,我的电脑大约20s就能编译完成。 ```bash cd go/src //编译并测试 ./all.bash //仅编译 ./make.bash ``` 编译完成后,可以在bin目录下看到go文件,运行下面的命令,打印版本号,表示编译成功。 * 为了和本地环境区分开,需要指定GOROOT为我们下载的go源码的文件夹路径。 ```bash GOROOT=<go path> bin/go version //输出: go version go1.22.0 darwin/amd64 ``` ### 2. 增加?标识符和具体语法节点 首先增加?号标识符,在`syntax`目录下,这个目录的主要功能就是做scan和parser。 * 路径:`src/cmd/compile/internal/syntax/tokens.go` * 在token里面增加一个\_RetErr用来表示?号。注释必须按照下面的格式写,自动生成需要。 ```go // go:generate stringer -type token -linecomment tokens.go const ( _ token = iota ... _Semi // ; _RetErr // ? _Colon // : ... ) ``` 上边的注释 `// go:generate stringer -type token -linecomment tokens.go`表示我们需要go generate一下。 * stringer 较新的版本中这个包是内置的,通常不需要安装,如果提示不存在,则手动安装一下。 ```bash //syntax目录下执行 go generate tokens.go ``` 查看`token_string.go`文件,如下图,将我们新加的token也生成上去了。 ![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/fcb84e9a2eec4518ab689e8129ebf4d0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5q2k5Lq65pyq6K6-572u5pi156ew:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTAyMjk5Mjk4MzgwMTM3MyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1726733587&x-orig-sign=Ctl8s0T190WgopojwRqFJP5wMtA%3D) 我们还需要创建一个?号对应的具体语法树的节点,因为我们这里做的是一个语法糖,所以直接组合一下if对应的IfStmt结构就可以了,如下: ```go IfStmt struct { Init SimpleStmt Cond Expr Then *BlockStmt Else Stmt // either nil, *IfStmt, or *BlockStmt stmt } RetErrStmt struct { IfStmt } ``` 简单介绍一下if结构的几个字段的作用: * Init:在执行if条件判断前的代码块,如:`if _,ok:=get();ok{ todo }`中,`_,ok:=get()`部分就是Init,它可以为空。 * Cond:判断条件, * Then:条件为真时,执行这个代码。 * Else:条件为假时,执行的代码,可以为空。 ### 3. 语法解析 go的源码解析主要有两个结构体: * scanner :负责按照token维度扫描源文件 * parser:将扫描的token组装成具体语法树 我们先增加对于?号标识符的扫描识别,`src/cmd/compile/internal/syntax/scanner.go` ```go func (s *scanner) next() { ... case ';': s.nextch() s.lit = "semicolon" s.tok = _Semi case '?': s.nextch() s.tok = _RetErr ... } ``` 然后增加对?的语法解析,我们参考IfStmt的解析方法: * 路径:`src/cmd/compile/internal/syntax/parser.go` * stmtOrNil : 在这个函数中增加标识符的解析方法 * retErr :实际上我们增加了一个IfStmt的语法糖。 ```go func (p *parser) stmtOrNil() Stmt { ... case _RetErr: return p.retErr() case _If: return p.ifStmt() ... } func (p *parser) retErr() *RetErrStmt { if trace { defer p.trace("ifStmt")() } //判断条件:err != nil condExpr := &Operation{ X: &Name{Value: "err"}, Op: Neq, Y: &Name{Value: "nil"}, } condExpr.pos = p.pos() // 表示 Then 块: return thenBlock := &BlockStmt{ List: []Stmt{ &ReturnStmt{ //Results: &Name{Value: "err"}, }, }, Rbrace: p.pos(), } //组装RetErrStmt s := new(RetErrStmt) s.pos = p.pos() s.Cond = condExpr s.Then = thenBlock //继续向下扫描 p.next() //如果存在else则继续解析 if p.got(_Else) { switch p.tok { case _If: s.Else = p.ifStmt() case _Lbrace: s.Else = p.blockStmt("") default: p.syntaxError("else must be followed by if or statement block") p.advance(_Name, _Rbrace) } } return s } ``` ### 4. 边界判断 go对写法有比较严格的要求,尤其是换行和边际的判断,所以我们需要告诉编译器,?号不需要处理边际,因为已经解析成了其他的结构。 同样在`parser.go`文件中的stmtList函数: ```go func (p *parser) stmtList() (l []Stmt) { if trace { defer p.trace("stmtList")() } for p.tok != _EOF && p.tok != _Rbrace && p.tok != _Case && p.tok != _Default { s := p.stmtOrNil() p.clearPragma() if s == nil { break } l = append(l, s) //跳过RetErrStmt的检查 if _, ok := s.(*RetErrStmt); ok { continue } //?;} 都属于正常的边界。 if !p.got(_Semi) && p.tok != _RetErr && p.tok != _Rbrace { p.syntaxError("at end of statement") p.advance(_Semi, _Rbrace, _Case, _Default) p.got(_Semi) // avoid spurious empty statement } } return } ``` 至此,一个初步的?号语法糖已经制作完成,用make.base重新编译项目。 ## 测试 我么在这个go目录下,新建一个测试文件`reterr.go`,内容如下: * 因为源码中我们处理else的情况,所有理论上`?else{}`和`?else if xx {}`也同样是支持的。 * EasyRetError:简单返回错误,可以看到代码量明显减少 * IfElseRetError和MulIfElseRetError:可以继续做else判断,并且不影响返回多个值 ```go package main import ( "errors" "fmt" ) func NewError(text string) error { if text == "" { return nil } else { return errors.New(text) } } func EasyRetError(info string) (err error) { err = NewError(info)? return } func IfElseRetError(info string)(str string, err error){ err = NewError(info)?else{ return info,nil } } func MulIfElseRetError(info string,ok bool)(str string, err error){ err = NewError(info)?else if ok{ return "test",nil }else{ return "success",nil } } ``` 在main函数中,我们写一下预期的结果,不符合预期则panin。 ```go func main() { var err error var info = "" //简单情况测试 if err = EasyRetError("Err");err == nil { panic("EasyRetError.Err") } if err = EasyRetError("");err != nil { panic("EasyRetError.nil") } //if else 分支情况测试 if info,err = IfElseRetError(info); err != nil || info != "" { panic("IfElseRetError.err not nil") } info = "test" if info,err = IfElseRetError(info); err == nil || info != "" { panic("IfElseRetError.info = test") } // 多分支测试 info = "test" if _,err = MulIfElseRetError(info,true); err == nil { panic("MulIfElseRetError.err not nil") } if info,err = MulIfElseRetError("",true); err != nil || info != "test" { panic("MulIfElseRetError.test") } if info,err = MulIfElseRetError("",false); err != nil || info != "success" { panic("MulIfElseRetError.success") } fmt.Println("success") } ``` 用我们重新编译的go,运行上边的代码: ```bash GOROOT=~/project/work/github/go bin/go run reterr.go ``` 测试成功 ## 尾语 改编译器是条不归路,目前司内对go的编译器做了大量改造,基于这个编译器,积累了大量的业务代码,已经积重难返。 不过风险和收益是成正比的,客户也被绑死了。 [原文](https://juejin.cn/post/7413356439476109327)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

300 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传