go compile 支持 _ 作为零值

go compile 支持 _ 作为零值

起因

很久以前就有类似的 proposal, 并且官方也有回复, 以下是链接:

个人认为官方还是太过于保守了, 至少目前 go1 的语法下, 在 return 表达式中引入 _ 用于返回错误上还是有一些编写上的方便的(小声 bb).


DIY compile

我的 go 版本 1.10.2, 修改 $GOROOT/src/cmd/compile/internal/gc 下 3 个文件:

  • 增加 hackgc.go
  • 修改 subr.go
  • 修改 typecheck.go

1. 增加 hackgc.go:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package gc

import (
"cmd/compile/internal/types"
"os"
)

var isnilable [NTYPE]bool

func init() {
isnilable[TPTR32] = true
isnilable[TPTR64] = true
isnilable[TINTER] = true
isnilable[TMAP] = true
isnilable[TCHAN] = true
isnilable[TFUNC] = true
isnilable[TSLICE] = true
isnilable[TUNSAFEPTR] = true
}

var (
hackgcblank = os.Getenv("HACKBLANK") != ""
)

func convblanklit(n *Node, t *types.Type) *Node {
if t == nil {
return n
}

nn := *n
n = &nn

var zero interface{}

et := t.Etype
switch {
case isInt[et]:
p := &Mpint{}
p.Rune = (t == types.Runetype || t.Orig == types.Runetype)
zero = p
case isnilable[et]:
zero = (*NilVal)(nil)
case et == TSTRING:
zero = ""
case et == TBOOL:
zero = false
case isFloat[et]:
zero = newMpflt()
case isComplex[et]:
zero = newMpcmplx()
case et == TARRAY:
n.Op = OARRAYLIT
n.Type = t
return n
case et == TSTRUCT:
n.Op = OSTRUCTLIT
n.Type = t
return n
default:
goto bad
}

n.Op = OLITERAL
n.Type = t
n.SetVal(Val{zero})
bad:
return n
}

func hackblank(n *Node, t *types.Type) *Node {
lno := setlineno(n)
n = convblanklit(n, t)
lineno = lno
return n
}

这一步主要是定义了一个由环境变量 HACKBLANK 提供的开关 hackgcblank; 和一个函数 hackblank, 用于把 _ 转化为一个零值 literal.


2. 修改 subr.go

subr.go:958

1
n = defaultlit(n, t)

修改为:

1
2
3
4
5
if hackgcblank && isblank(n) {
n = hackblank(n, t)
} else {
n = defaultlit(n, t)
}

这一步的 assignconvfn 函数提供了一个赋值时检查和转换右值 nType 到左值类型 t 的功能, 所以这里对 _ 做一个特殊处理, 如果一个右值 n_, 那么将它转为左值类型 t 的零值 literal.


3. 修改 typecheck.go

typecheck.go:310

1
if isblank(n) {

修改为

1
if !hackgcblank && isblank(n) {

这一步是检查右值类型是发生的, 我们希望在开关打开时忽略这个检查.

验证

替换编译器:

1
2
3
cd $GOROOT/src/cmd/compile
go clean -cache && go build -i -v
mv compile ../../../pkg/tool/linux_amd64/compile

使用一段代码验证我们的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"errors"
"fmt"
)

type S struct {
A int
B bool
}

func foo() (int, bool, float64, complex128, string, map[int]int, [2]bool, interface{}, S, error) {
return _, _, _, _, _, _, _, _, _, errors.New("oops")
}

func main() {
fmt.Println(foo())
}

It works!

是不是引入了新的问题

看一段代码

1
2
3
4
5
6
7
8
package main

func main() {
n := _
a, b := _
_ = n
_, _ = a, b
}

这段代码编译器会 crash, 因为我们的做法是将左值的类型赋予右值的 _, 但这里左值的类型未知, 我的做法是禁止右值 _ 用于赋值语句.

修复代码

修改 typecheck.go:3265

1
n.Right = typecheck(n.Right, Erv)

修改为

1
2
3
4
5
n.Right = typecheck(n.Right, Erv)
if hackgcblank && isblank(n.Right) && n.Right.Op == ONAME {
n.Right.Type = nil
yyerror("cannot assign _ to left value")
}

修改 typecheck.go:3317

1
typecheckslice(n.Rlist.Slice(), Erv)

修改为

1
2
3
4
5
6
7
8
9
10
typecheckslice(n.Rlist.Slice(), Erv)
if hackgcblank {
for _, rv := range n.Rlist.Slice() {
if isblank(rv) && rv.Op == ONAME {
rv.Type = nil
yyerror("cannot assign _ to left value")
break
}
}
}

检查赋值语句中是否存在 _, 如果存在则认为是编译错误.


结束

到此修改完成, 示例中的代码理论可以运行, 具体还带来哪些编译影响未知, 并可能存在代码导致编译器 crash, 这里只是做了一个简单的修改, 真的对这个功能有需求, 还是应该通过 issue/proposal 向官方提议.