在 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()
方法中,你可能期望w
是nil
,但实际上并非如此。当打印该对象时,它显示为空。这是如何发生的呢?
Go 语言中的接口具有特殊的实现方式,它包含两个组成部分:
接口的底层表示是 (T, V)
的形式:
int
、struct
或 *bytes.Buffer
)。只有当 T 和 V 都未设置时,接口值才会被认为是nil
(即 T=nil, V=nil
)。
特别地:
*int
的nil
指针,则接口的类型为*int
,而值为nil
。在这种情况下,接口值并不是nil
。当创建变量 b
时,它的类型是 *bytes.Buffer
,但它的值是 nil
。因此:
check(b)
时,接口值 (T=*bytes.Buffer, V=nil)
并不为nil
,所以 w != nil
条件为真。w
的值是空,但类型信息仍然存在。以下代码进一步说明接口值什么时候为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
第一部分:var e error = nil
e
没有具体的类型,且值为 nil
。e == nil
为真。第二部分:var se *SomeError = nil
e
持有类型 *SomeError
和值 nil
。e == nil
为假,因为 e
的类型部分存在。在使用接口作为函数参数并进行nil
检查时,要特别小心。如果接口的类型部分存在,即使值为nil
,接口本身也不是nil
。
1
AItsuki 31 天前
挺好的,不需要一直判断 nil 。
|
2
COW 31 天前
nil 是 nil ,空值是空值
|
3
sunny352787 31 天前 2
对的,go 这方面就是个深坑,我也不太理解为啥这么设计,等你需要判断接口为 nil 的时候完全没办法
|
4
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 都知道提醒你**注意**😵,这么设计确实感觉怪怪的。 |
5
Nzelites 31 天前
很奇怪的设计 但是传参也确实不应该给函数塞个 nil
|
6
mcfog 31 天前
现象是对的,结论我不太认同。
接受接口后能做的也就检查一下,nil check 没有什么需要改进的。这里有责任的是调用方,因为把实体类型(隐式地)转换为接口类型的是调用方,相当于调用方制造了一个损坏的非预期的接口值传出去 有的时候有的方法可以对 nil 值调用做出合理的行为,nil 值调方法并不一定是一个编程错误,因此接口目前的这个看似反直觉的行为还是有一定的道理的 |
7
CLMan 31 天前 1
tour of go 和《 TGPL 》都应该重点讲过这个问题,但如果很少使用到接口,确实长期下来会遗忘这个问题,所以说是坑也不是没有问题。
至于为什么这么设计,虽然没去查权威来源,但个人的推测是,Go 中 null 是可以作为方法接收者的,所以需要区分带类型的值为 null ,与不带类型且值为 null 的情况。 Go 的 for i range array ,其中 i 是索引值,省略了值。这对于习惯了 JS 和 Java 语法的我,很久没写 Go 再回去写,也经常犯错误,在迭代整数 slice/数组时,把 i 当作元素。 |
8
NotLongNil 31 天前 via iPhone 1
@sunny352787 可以使用反射
if i == nil || (reflect.TypeOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) { // i 是 nil } |
10
sunny352787 31 天前
@NotLongNil 我的意思就是没有简单直接优雅的处理方法,为这么个玩意用反射也不值啊
|
11
CLMan 31 天前 2
细想一下,接口类型的约定是方法调用,而 null 值是可以作为方法接收者(需要调用方保证),因此接收方只需要检查是否提供了类型(即`==nil`判断)。
你唯一需要进一步检查值是否为 nil 的情况,是进行类型断言,断言成功的结果是一个确定的类型而非接口类型,此时你对断言结果进行`==nil`判断也不会存在什么问题。 所以这个设计看似不合理,但其实很合理,除了面试八股,或者研究茴香豆的写法,这个设计并不会导致写出 BUG 代码。 |
12
grzhan 31 天前 1
主要就是 @CLMan 老板提到的接口类型约定的是方法调用,所以大部分场景不需要检查接口的值是否为 nil ,要检查通过类型断言或者方法内部来检查。
Go 的语法细节确实有很多实际运行起来不符合直觉的地方,很多时候一定要结合它的内部机制甚至看源码才能顺畅理解, 虽然这些机制大部分理解了也确实比较简单吧(所谓“大道至简”… |
13
CEBBCAT 31 天前 1
省流:老生常谈,interface 有值和类型两部分
|
14
Desdemor 31 天前
看你没看懂,看楼上看懂了,any 有类型 也算有值吧
|
15
Abirdcfly 31 天前
把 +v 改为 #v 就能看出来
|
16
PTLin 31 天前 1
这个设计就是不行,很反直觉,这种情况真要执行判断甚至需要反射才行,标准库里就有这样搞得 https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/encoding/json/decode.go;l=171
|
17
Leviathann 31 天前
土法炼钢语言是这样的
|
18
lesismal 31 天前
太复杂了, golang 的好多语法细节我都没搞懂, 惭愧, 惭愧
|
19
mizuki9 31 天前 1
我是这样理解的,go 的 nil 和其他语言的 null 不一样。
其他语言例如 Java ,null 是所有类型的子类。 golang 中不存在这种属于任意类型的共同子类,nil 都是带类型的,不存在真正的 null 。 第一个代码片段里其实是 (*bytes.Buffer)nil != (io.Writer)nil |
20
PTLin 31 天前
@mizuki9 你理解错了,概念别硬套,文章的这个问题就是和 go 里 interface 的底层结构相关的,接口是由两个指针组成的元数据部分和 data 部分,一个接口只有这两个部分都指向 nil ,在代码中的==nil 判断才为 true 。
而文章中第一块代码传递参数后 interface 底层的元数据部分就不指向 nil 了,所以判断才和直觉不符合。 |
21
Elaina 31 天前
老坑了,里面长这样
```go type eface struct { _type *_type // 类型信息 data unsafe.Pointer } type iface struct { tab *itab // 里面存储了一个类型信息字段 data unsafe.Pointer } ``` 不管空接口还是带方法的接口,都存了俩玩意儿 |
23
mizuki9 31 天前 via Android
@PTLin 你说的是不是(*bytes.Buffer)nil != (any)nil 。
那确实是我的问题,我不懂 golang ,最近是在自学 |
24
PTLin 31 天前
@mizuki9 你看你上一楼的回答,类型信息肯定是有,还包括其他信息。接口类型也分为 inferface{}和其他的。我想说的是文章里的这个行为必须要知道接口的底层表示,和把一个实现了接口的类型赋值给接口会发生什么,才能理解文章中的问题。
|
25
leonshaw 31 天前 via Android
0 值也是一等公民
|
26
xuanbg 31 天前
有些人特别讨厌 null ,为了米有 null ,于是搞出了一些更奇怪的东西
|
27
liuguang 30 天前
nil 还有类型,这也是 go 语言的一大败笔设计
|
28
CRVV 30 天前
我刚学 Go 的时候也有这个问题,但后来用熟了再也没遇到过。
后来我从来没写过把 nil 传给 interface 的代码,个人观点是 99% 的情况下不需要也不应该这么写。 |
30
Kauruus 27 天前
不明白为什么一个现代的语言还满地的 nil 。
也不明白为什么需要知道它的实现(接口值是类型+值 blah blah )才能理解它的行为。 能不能根据 The Go Programming Language Specification 来解释一下这个行为? |
31
Kauruus 27 天前
> The static type (or just type) of a variable is the type given in its declaration, the type provided in the new call or composite literal, or the type of an element of a structured variable.
静态类型是变量声明时的类型。 > Variables of interface type also have a distinct dynamic type, which is the (non-interface) type of the value assigned to the variable at run time (unless the value is the predeclared identifier nil, which has no type). 接口类型变量还有一个动态的类型,这个类型是运行期赋于的*值的类型*,除非那个值是**预先声明的标识符 nil**,它没有类型。 这里很明确地区分了 标识符 nil 和一个值的类型。 解释了把 (标识符 nil )和 (一个为 nil 的值)赋值给一个 interface 的区别,前者没有类型,后者值为 nil 但是有类型。 > Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil. 两个接口值只有在动态类型和值都相等时相等。 |