Go的`nil`类型的幕后发生了什么?

我最近在一个流行的编程论坛上看到 Go 支持一些“无类型”值——特别是nil,Go 的空值/底部类型。我对 Go 的经验相当有限,在这个论坛上的一个声明让我措手不及——也就是说,x := nil用 Go编写是非法的。

果然,带有该行的玩具 Go 程序无法编译,并且编译器错误明确指出论坛中的评论已检查:nil不允许将变量声明和赋值给 to 。这似乎是一个奇怪的限制,但事情变得奇怪了。

通过从函数返回元组来返回部分错误是 Go 中的一个常见习语。像这样的东西:

func might_error() (int, error) {
    return 1, nil
}

func main() int {
    x, err := might_error()
    if err != nil {
        panic("err was not nil!")
    }

    return x
}

据我所知,这至少有两个不一致之处:

  • 首先,尽管nil在纸面上是无类型的,但它采用了error类型(通过实现Error与 Go 的鸭子类型相结合的接口)以符合might_error的返回类型签名。

  • 其次,它似乎nil被用于之前非法的声明和赋值main,并且在(至少在类似语言中)might_error可以被视为 constexpr 的情况下。

    • 怪异的是,替换x, err := might_error()x, err := 1, nil 还是出现了错误有use of untyped nil

我目前的想法是,在函数的类型签名需要它的情况下,Error接口被注入到特定实例中nil,这意味着它不再是无类型的 ,而是在该特定实例的生命周期内nil成为有类型 nilnil,但我不是完全可以肯定这是正确的,因为它似乎是一个奇怪的设计选择,因为它不能清楚地概括。

是什么激发了这些设计选择?为什么没有nil类型化而不是让它成为一个适当的空类型,除非它方便,此时它变成了一个实现Error接口的类型化值(据我所知)?

回答

This is a complete non-problem in the sense that in practice there can be confusion if a nil value is stored in an interface variable making that variable non-nil. This is https://golang.org/doc/faq#nil_error and everyone is bitten a few times by this problem until you learn that interface values containing a nil variable are no longer nil themself. It's a bit like with var s = []*int{nil, nil, nil} where s contains only nils but is non-nil itself.

Technically (from a language design point) you could introduce several "nils", e.g. nil for pointers, null for interfaces, noop for functions and vac for channels. (Exaggerating a bit). With this you could have:

type T whatever
func (t *T) Error() string // make *T implement error
var err error              // interface variable of type error
print(err == null)         // true, null is Zero-Value of interface types
print(err == nil)          // COMPILER ERROR, cannot compare interface to nil (which is for pointers only
var t *T = nil             // a nil pointer to T
err = t                    // store nil *T in err
print(err == null)         // false err is no longer null, contains t

You could even remove the compiler error:

err = t                    // store nil *T in err
print(err == null)         // false, err contains t
print(err == nil)          // "deep" comparison yielding true
err = &T{}                 // store non-nil *T in err
print(err == null)         // still false
print(err == nil)          // false, value inside err is no longer nil

You also could define a default type for nil, e.g. *[]chan*func()string so that you can write x := nil like you can do with f := 3.141 where the default type of the untyped constant 3.141 is float64. But such a default type for nil would be arbitrary and not helpful at all (as my example shows; *[]chan*func()string is uncommon).

If I remember correctly there was a longer discussion about this topic on the golang-nuts mailng list where the rationals about this design was discussed. It boiled down to something like: "The actual real-life problems with having nil multiple meanings and not being a constant are tiny (basically just variants of an error type containing nil not being nil). The 'solution' to this one tiny problem would complicate the language (e.g. by introducing a null literal for the zero value of interface types) considerably. It probably is simpler to teach people that interface values containing a nil are no longer nil themself than introducing typed nils or nulls for interface types."

In more than 10 years of programming in Go I literally never had to think about a nil literal being typed or untyped or constant or whatever. The article you probably are referring to is constructing pure academic, but actually non-problem in practice issue out of a tiny design decision about having just one nil literal for all zero values for pointer, slice, map, channel and function types.

Addendum

First, even though nil is typeless on paper, it takes on the error type (by way of it implementing the Error interface combined with Go's duck typing) for the purposes of conforming to might_error's return type signature.

This is a completely wrong description of what happens.

If you write

func f() (r int) { return 7 }

then 7 is assigned to r of type int and f returns. This works because 7 can be assigned to int.

In

func might_error() (int, error) { return 1, nil }

the same happens, the second return variable of type error (an interface type) is set to nil because nil can be assigned to any interface type like you can assign nil to any pointer type or any function type.

This has nothing to do with "implementing the Error interface combined with Go's duck typing". Absolutely not. nil doesn't implement the error interface at all. Any interface value can be nil like can be any function value or slice value or channel value. Setting a chan to nil basically "clears" the channel variable, it doesn't mean that nil somehow "implements the channel interface". You seem to conflate the zero value of several types and how to set them by assigning nil with implementing interfaces. All this has basically nothing to do with nil being typed or not. The nil literal in source code os overloaded and often can be thought of as just representing the zero value of a type.


以上是Go的`nil`类型的幕后发生了什么?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>