V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
pike0002
V2EX  ›  Go 编程语言

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

  •  
  •   pike0002 ·
    sonic0002 · 40 天前 · 3357 次点击
    这是一个创建于 40 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 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 语言中的接口具有特殊的实现方式,它包含两个组成部分:

    • 类型( Type )
    • 值( Value )

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

    • T 是具体类型(比如 intstruct*bytes.Buffer)。
    • V 是具体值,具有类型 T 。

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

    特别地:

    • 如果一个接口持有一个类型为*intnil指针,则接口的类型*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

    31 条回复    2024-11-19 12:37:35 +08:00
    AItsuki
        1
    AItsuki  
       40 天前
    挺好的,不需要一直判断 nil 。
    COW
        2
    COW  
       40 天前
    nil 是 nil ,空值是空值
    sunny352787
        3
    sunny352787  
       40 天前   ❤️ 2
    对的,go 这方面就是个深坑,我也不太理解为啥这么设计,等你需要判断接口为 nil 的时候完全没办法
    Bronya
        4
    Bronya  
       40 天前
    问: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
        5
    Nzelites  
       40 天前
    很奇怪的设计 但是传参也确实不应该给函数塞个 nil
    mcfog
        6
    mcfog  
       40 天前
    现象是对的,结论我不太认同。
    接受接口后能做的也就检查一下,nil check 没有什么需要改进的。这里有责任的是调用方,因为把实体类型(隐式地)转换为接口类型的是调用方,相当于调用方制造了一个损坏的非预期的接口值传出去

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

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

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

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

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

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

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

    type iface struct {
    tab *itab // 里面存储了一个类型信息字段
    data unsafe.Pointer
    }
    ```

    不管空接口还是带方法的接口,都存了俩玩意儿
    mizuki9
        22
    mizuki9  
       40 天前 via Android
    @PTLin 你说的元数据部分,是我认为的 golang 引用类型的 type
    mizuki9
        23
    mizuki9  
       40 天前 via Android
    @PTLin 你说的是不是(*bytes.Buffer)nil != (any)nil 。
    那确实是我的问题,我不懂 golang ,最近是在自学
    PTLin
        24
    PTLin  
       40 天前
    @mizuki9 你看你上一楼的回答,类型信息肯定是有,还包括其他信息。接口类型也分为 inferface{}和其他的。我想说的是文章里的这个行为必须要知道接口的底层表示,和把一个实现了接口的类型赋值给接口会发生什么,才能理解文章中的问题。
    leonshaw
        25
    leonshaw  
       40 天前 via Android
    0 值也是一等公民
    xuanbg
        26
    xuanbg  
       40 天前
    有些人特别讨厌 null ,为了米有 null ,于是搞出了一些更奇怪的东西
    liuguang
        27
    liuguang  
       39 天前
    nil 还有类型,这也是 go 语言的一大败笔设计
    CRVV
        28
    CRVV  
       39 天前
    我刚学 Go 的时候也有这个问题,但后来用熟了再也没遇到过。
    后来我从来没写过把 nil 传给 interface 的代码,个人观点是 99% 的情况下不需要也不应该这么写。
    zhwguest
        29
    zhwguest  
       38 天前
    @CLMan 我来杠一下,error 就是个接口好不好.....
    Kauruus
        30
    Kauruus  
       36 天前
    不明白为什么一个现代的语言还满地的 nil 。

    也不明白为什么需要知道它的实现(接口值是类型+值 blah blah )才能理解它的行为。

    能不能根据 The Go Programming Language Specification 来解释一下这个行为?
    Kauruus
        31
    Kauruus  
       36 天前
    > 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.

    两个接口值只有在动态类型和值都相等时相等。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1079 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:12 · PVG 03:12 · LAX 11:12 · JFK 14:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.