[原文](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)
有疑问加站长微信联系(非本文作者))