为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

Go
慕森王 2022-08-24 10:35:45
我最近在一个流行的编程论坛上读到,Go支持一些“无类型”值 - 特别是,Go的空值/底部类型。我对Go的经验相当有限,在这个论坛上发表的一个声明让我措手不及 - 也就是说,用Go写作是非法的。nilx := nil果然,带有该行的玩具Go程序无法编译,编译器错误清楚地表明论坛中的注释已签出:不允许声明和分配变量。这似乎是一个奇怪的限制,但事情变得更加奇怪。nilGo 中的一个常见习语是通过从函数返回元组来返回部分错误。像这样: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}据我所知,这至少有两个不一致之处:首先,尽管在纸面上是无类型的,但它采用类型(通过实现与Go的鸭子类型相结合的接口),以符合 的返回类型签名。nilerrorErrormight_error其次,它似乎被用于以前非法的声明和分配,并且在(至少在可比较的语言中)可以被视为constexpr的情况下。nilmainmight_error更奇怪的是,用静止的错误替换掉,使用未打字的nil!x, err := might_error()x, err := 1, nil我目前的想法是,在函数的类型签名需要它的情况下,接口被注入到一个特定的实例中,这意味着它不再是非类型化的,而是在该特定实例的生命周期内成为类型化的,但我完全不确定这是正确的,因为它似乎是一个奇怪的设计选择,因为它本质上不能清楚地推广。Errornilnilnilnil是什么促使了这些设计选择?为什么是非类型化而不是让它成为一个正确的空类型,除非在方便的时候,在这一点上它成为一个实现接口的类型化值(据我所知)?nilError
查看完整描述

2 回答

?
猛跑小猪

TA贡献1858条经验 获得超8个赞

这是一个完全没有问题的问题,因为在实践中,如果将一个nil值存储在接口变量中,使该变量成为非nil,则可能会造成混淆。这是 https://golang.org/doc/faq#nil_error 每个人都会被这个问题咬几次,直到你了解到包含nil变量的接口值本身不再是nil。这有点像只包含 nils 但本身是 non-nil 的。var s = []*int{nil, nil, nil}s


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


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 定义一个默认类型,例如 这样你就可以像编写非类型常量 3.141 的默认类型是 float64 一样进行编写。但是,nil的这种默认类型将是任意的,根本没有帮助(如我的示例所示; 不常见)。*[]chan*func()stringx := nilf := 3.141*[]chan*func()string


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


在10多年的Go编程中,我实际上从未考虑过一个字面上被类型化或非类型化或常量或其他什么。您可能正在引用的文章正在构建纯粹的学术性,但实际上在实践中没有问题,这是一个微小的设计决策,即指针,切片,映射,通道和函数类型的所有零值只有一个文本。nilnil


补遗


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


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


如果你写


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

则 7 被分配给 int 类型的 r,f 返回。这有效,因为可以将 7 分配给 int。



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

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


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


查看完整回答
反对 回复 2022-08-24
?
慕哥6287543

TA贡献1831条经验 获得超10个赞

你的例子不会编译(因为不需要返回任何东西),当你进入一种语言的挑剔和细节领域时,你所有的例子都应该真正起作用。😀main

我目前的想法是,在函数的类型签名需要它的情况下,Error接口被注入到nil的特定实例中......

这不太对。Go 规范告诉我们这是如何工作的:

有三种方法可以从具有结果类型的函数返回值:

  1. 返回值可以在“return”语句中显式列出。每个表达式都必须是单值表达式,并且可以分配给函数结果类型的相应元素。[截图示例]

这是您在示例程序中使用的方法(我清理了一下以在Go Playground中编译和运行)。表达式:

return 1, nil

方法:

unnamed_return_variable_1 = 1unnamed_return_variable_2 = nil

其中,两个未命名的返回变量实际上没有这些名称(毕竟它们是命名的),但确实有类型,这要归功于函数的声明。所以

return 1, nil

根本不像:

x, err := 1, nil

但更像是:

var x int; var err error; x, err = 1, nil

如您所见,这也是非常有效的。

是什么促使了这些设计选择?

为此,你必须问问设计师。

ThinkGoodly在评论中提到

未键入的 0 不会引起这种焦虑。

但是,非类型化常量具有默认类型0

非类型化常量具有默认类型,该类型是在需要类型化值的上下文中(例如,在短变量声明(例如,没有显式类型)中将常量隐式转换为的类型。非类型化常量的默认类型分别为 、 、 或,具体取决于它是布尔值、符文、整数、浮点数、复数还是字符串常量。i := 0boolruneintfloat64complex128string

预先声明的标识符没有类型,甚至没有默认类型。它只是有一系列特殊规则,允许它在各个地方使用。nil

非语言规范的思考方式

虽然这不是它的定义方式(它是由Go规范定义的),但我建议这样考虑这个问题:

  • 预先声明的标识符1 是非类型化的。nil

  • 存在类型化的 nil 值。任何足够像指针的东西都可以是 nil,也可以是 non-nil。例如,任何变量都可以是指向int-nil的指针,就像未初始化的变量一样。此 nil 不是 nil “关键字”(预先声明的标识符,见脚注);这是一个完全不同的“无”,就像布鲁斯(班纳)是一个与布鲁斯(韦恩)完全不同的人。*int

  • 接口值由两部分组成:类型和该类型的值。类型可以为零!这是另一种 nil,不同于 nil(未键入的“关键字”)和 nil(某些特定 pointer-y 类型的某些特定 nil)。如果类型为 nil,则该值也可以为 nil。如果两者都为 nil,则接口值比较等于 <nil,nil>对,当需要与接口值进行比较时,未键入的“关键字”nil 会变成该对。如果任何一个为非零,则比较表明它们不相等。硬件实现是接口的两个部分都是零:如果任何一个部分为非零,则整个事物为非零,因此“不是零”。

当上下文提供了足够的信息时,将从 nil-the-“关键字”(预先声明的标识符)到适当的硬件样式零值进行转换。分配给某个变量(甚至是未命名的变量)可提供上下文。分配给某些位置参数可提供上下文。尝试使用简短声明无法提供上下文,因此会出现错误。


1 个一般来说,Go更喜欢预先声明的标识符到关键字,因此等等实际上并不是关键字。这也排除了关键字集。尽管如此,有些东西——比如布尔常量和,或者预先声明的标识符的神奇行为,只能通过不覆盖预先声明的标识符然后使用它来访问。也就是说,如果您命名某些内容,则在该名称超出范围之前,您将无法获得iota样式功能。这些事情的行为方式通常通过其他语言的关键字来处理。如果你倾向于思考,比如说,C++或Java,你可能想不断提醒自己,这些不是关键字,即使它们闻起来像它们。falsetruenilfalsetrueiotaiota


查看完整回答
反对 回复 2022-08-24
  • 2 回答
  • 0 关注
  • 78 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信