Go 语言中的接口 nil 检查需谨慎

31 天前
 pike0002

在 Go 语言中,特别是在错误检查方面,经常会看到nil 检查,这是由于 Go 语言的特殊错误处理约定。在大多数情况下,nil 检查是直截了当的,但在接口情况下,需要特别小心。

以下是一个代码片段,猜猜它的输出会是什么:

package main

import (
	"bytes"
	"fmt"
	"io"
)

func check(w io.Writer) {
	if w != nil {
		fmt.Println("w is not nil")
	}

	fmt.Printf("w is %+v\n", w)
}

func main() {
	var b *bytes.Buffer

	check(b)

	fmt.Printf("b is %+v", b)
}

输出结果

w is not nil
w is 
b is 

为什么会这样?

check()方法中,你可能期望wnil,但实际上并非如此。当打印该对象时,它显示为空。这是如何发生的呢?


接口的内部机制

Go 语言中的接口具有特殊的实现方式,它包含两个组成部分:

接口的底层表示是 (T, V) 的形式:

只有当 TV 都未设置时,接口值才会被认为是nil(即 T=nil, V=nil)。

特别地:


上述代码解释

当创建变量 b 时,它的类型是 *bytes.Buffer,但它的值是 nil。因此:


更具体的例子

以下代码进一步说明接口值什么时候为nil

package main

import (
	"fmt"
)

type SomeError struct{}

func (se *SomeError) Error() string {
	return "error"
}

func check(e error) {
	if e == nil {
		fmt.Println("e is nil")
	}

	fmt.Printf("e is %+v\n", e)
}

func main() {
	var e error = nil
	check(e)

	var se *SomeError = nil
	check(se)
}

输出结果

e is nil
e is <nil>
e is error

结论

在使用接口作为函数参数并进行nil检查时,要特别小心。如果接口的类型部分存在,即使nil,接口本身也不是nil

3319 次点击
所在节点    Go 编程语言
31 条回复
AItsuki
31 天前
挺好的,不需要一直判断 nil 。
COW
31 天前
nil 是 nil ,空值是空值
sunny352787
31 天前
对的,go 这方面就是个深坑,我也不太理解为啥这么设计,等你需要判断接口为 nil 的时候完全没办法
Bronya
31 天前
问:golang 中怎么判断一个指针是否是 nil ?

GPT:

在 Go 语言中,可以通过直接比较指针是否等于 `nil` 来判断指针是否为 `nil`。例如:

```go
var ptr *int // 定义一个指针变量,初始值为 nil

if ptr == nil {
fmt.Println("指针是 nil")
} else {
fmt.Println("指针不是 nil")
}
```

在上面的代码中,通过 `ptr == nil` 的判断来检查指针是否为 `nil`。

需要注意的是,在使用接口指针时,需要小心判空。接口指针的判空不仅要检查是否为 `nil`,还要确保接口内存储的值也为 `nil`,因为如果接口存储了一个 `nil` 的指针,其本身并不为 `nil`。例如:

```go
var ptr *int = nil
var iface interface{} = ptr

if iface == nil {
fmt.Println("接口为 nil")
} else {
fmt.Println("接口不为 nil")
}
```

即使 `ptr` 是 `nil`,`iface` 也不为 `nil`,因为接口本身存储了一个类型信息。因此,在判断接口是否为 `nil` 时,需特别小心这种情况。

---

ChatGPT 都知道提醒你**注意**😵,这么设计确实感觉怪怪的。
Nzelites
31 天前
很奇怪的设计 但是传参也确实不应该给函数塞个 nil
mcfog
31 天前
现象是对的,结论我不太认同。
接受接口后能做的也就检查一下,nil check 没有什么需要改进的。这里有责任的是调用方,因为把实体类型(隐式地)转换为接口类型的是调用方,相当于调用方制造了一个损坏的非预期的接口值传出去

有的时候有的方法可以对 nil 值调用做出合理的行为,nil 值调方法并不一定是一个编程错误,因此接口目前的这个看似反直觉的行为还是有一定的道理的
CLMan
31 天前
tour of go 和《 TGPL 》都应该重点讲过这个问题,但如果很少使用到接口,确实长期下来会遗忘这个问题,所以说是坑也不是没有问题。

至于为什么这么设计,虽然没去查权威来源,但个人的推测是,Go 中 null 是可以作为方法接收者的,所以需要区分带类型的值为 null ,与不带类型且值为 null 的情况。

Go 的 for i range array ,其中 i 是索引值,省略了值。这对于习惯了 JS 和 Java 语法的我,很久没写 Go 再回去写,也经常犯错误,在迭代整数 slice/数组时,把 i 当作元素。
NotLongNil
31 天前
@sunny352787 可以使用反射

if i == nil || (reflect.TypeOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
// i 是 nil
}
CLMan
31 天前
@CLMan 更正“也不是没有问题”为“也没有问题”
sunny352787
31 天前
@NotLongNil 我的意思就是没有简单直接优雅的处理方法,为这么个玩意用反射也不值啊
CLMan
31 天前
细想一下,接口类型的约定是方法调用,而 null 值是可以作为方法接收者(需要调用方保证),因此接收方只需要检查是否提供了类型(即`==nil`判断)。

你唯一需要进一步检查值是否为 nil 的情况,是进行类型断言,断言成功的结果是一个确定的类型而非接口类型,此时你对断言结果进行`==nil`判断也不会存在什么问题。

所以这个设计看似不合理,但其实很合理,除了面试八股,或者研究茴香豆的写法,这个设计并不会导致写出 BUG 代码。
grzhan
31 天前
主要就是 @CLMan 老板提到的接口类型约定的是方法调用,所以大部分场景不需要检查接口的值是否为 nil ,要检查通过类型断言或者方法内部来检查。

Go 的语法细节确实有很多实际运行起来不符合直觉的地方,很多时候一定要结合它的内部机制甚至看源码才能顺畅理解, 虽然这些机制大部分理解了也确实比较简单吧(所谓“大道至简”…
CEBBCAT
31 天前
省流:老生常谈,interface 有值和类型两部分
Desdemor
31 天前
看你没看懂,看楼上看懂了,any 有类型 也算有值吧
Abirdcfly
31 天前
把 +v 改为 #v 就能看出来
PTLin
31 天前
这个设计就是不行,很反直觉,这种情况真要执行判断甚至需要反射才行,标准库里就有这样搞得 https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/encoding/json/decode.go;l=171
Leviathann
31 天前
土法炼钢语言是这样的
lesismal
31 天前
太复杂了, golang 的好多语法细节我都没搞懂, 惭愧, 惭愧
mizuki9
31 天前
我是这样理解的,go 的 nil 和其他语言的 null 不一样。
其他语言例如 Java ,null 是所有类型的子类。
golang 中不存在这种属于任意类型的共同子类,nil 都是带类型的,不存在真正的 null 。
第一个代码片段里其实是 (*bytes.Buffer)nil != (io.Writer)nil
PTLin
31 天前
@mizuki9 你理解错了,概念别硬套,文章的这个问题就是和 go 里 interface 的底层结构相关的,接口是由两个指针组成的元数据部分和 data 部分,一个接口只有这两个部分都指向 nil ,在代码中的==nil 判断才为 true 。
而文章中第一块代码传递参数后 interface 底层的元数据部分就不指向 nil 了,所以判断才和直觉不符合。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1089816

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX