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:

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

n = defaultlit(n, t)

修改为:

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

if isblank(n) {

修改为

if !hackgcblank && isblank(n) {

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

验证

替换编译器:

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

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

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!

是不是引入了新的问题

看一段代码

package main

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

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

修复代码

修改 typecheck.go:3265

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

修改为

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

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

修改为

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 向官方提议.

标签: go

添加新评论