首页 > 解决方案 > Go 的 `nil` 类型的幕后发生了什么?

问题描述

我最近在一个流行的编程论坛上读到,Go 支持一些“无类型”值——特别是nil,Go 的 null 值/底部类型。我在 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
}

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

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

是什么促使了这些设计选择?为什么nil要无类型而不是使其成为正确的空类型,除非方便,此时它成为实现Error接口的类型化值(据我所知)?

标签: gonulltyping

解决方案


从某种意义上说,这是一个完全没有问题的问题,实际上,如果将 nil 值存储在使该变量为非 nil 的接口变量中,则可能会造成混淆。这是https://golang.org/doc/faq#nil_error并且每个人都被这个问题困扰了几次,直到您了解到包含 nil 变量的接口值本身不再是 nil 。这有点像var s = []*int{nil, nil, nil}wheres只包含 nil 而不是 nil 本身。

从技术上讲(从语言设计的角度来看),您可以引入几个“nil”,例如nil指针、null接口、noop函数和vac通道。(有点夸张)。有了这个,你可以:

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

您甚至可以删除编译器错误:

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

您还可以为 nil 定义一个默认类型,例如*[]chan*func()string,这样您就可以x := nilf := 3.141在无类型常量 3.141 的默认类型是 float64 的情况下那样编写。但是这种 nil 的默认类型是任意的并且根本没有帮助(如我的示例所示;*[]chan*func()string不常见)。

如果我没记错的话,在 golang-nuts 邮件列表上有一个关于这个主题的更长的讨论,其中讨论了这个设计的合理性。它归结为:“具有nil多重含义而不是常数的实际现实生活问题很小(基本上只是包含 nil 不是 nil 的错误类型的变体)。这个小问题的“解决方案”将使语言变得相当复杂(例如,通过为接口类型的零值引入null文字)。与为接口类型引入类型化的 nil 或 null 相比,教人们包含 nil 的接口值本身不再是 nil 可能更简单。

在 10 多年的 Go 编程中,我从字面上根本不需要考虑nil文字是有类型的、无类型的、常量或其他什么。您可能所指的文章是构建纯粹的学术问题,但实际上在实践中没有问题的问题来自一个微小的设计决策,即nil指针、切片、映射、通道和函数类型的所有零值只有一个文字。

附录

首先,即使 nil 在纸面上是无类型的,它也采用错误类型(通过它实现 Error 接口并结合 Go 的鸭子类型),以符合 may_error 的返回类型签名。

这是对所发生事情的完全错误的描述。

如果你写

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

然后将 7 分配给 int 类型的 r 并返回 f。这是可行的,因为 7 可以分配给 int。

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

同样的情况,错误类型(接口类型)的第二个返回变量设置为 nil,因为 nil 可以分配给任何接口类型,就像您可以将 nil 分配给任何指针类型或任何函数类型一样。

这与“结合 Go 的鸭子类型实现 Error 接口”无关。绝对不。nil 根本不实现错误接口。任何接口值都可以是 nil,就像可以是任何函数值或切片值或通道值一样。将 chan 设置为 nil 基本上“清除”了通道变量,这并不意味着 nil 以某种方式“实现了通道接口”。您似乎将几种类型的零值以及如何通过将 nil 分配给实现接口来设置它们。所有这些基本上与是否输入 nil 无关。源代码 os 中的nil文字重载,通常可以被认为只是表示类型的零值。


推荐阅读