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 · 21 小时 44 分钟前 · 1881 次点击

    在 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

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

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

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

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

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

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

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

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

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

    不管空接口还是带方法的接口,都存了俩玩意儿
    mizuki9
        22
    mizuki9  
       17 小时 20 分钟前 via Android
    @PTLin 你说的元数据部分,是我认为的 golang 引用类型的 type
    mizuki9
        23
    mizuki9  
       17 小时 15 分钟前 via Android
    @PTLin 你说的是不是(*bytes.Buffer)nil != (any)nil 。
    那确实是我的问题,我不懂 golang ,最近是在自学
    PTLin
        24
    PTLin  
       17 小时 13 分钟前
    @mizuki9 你看你上一楼的回答,类型信息肯定是有,还包括其他信息。接口类型也分为 inferface{}和其他的。我想说的是文章里的这个行为必须要知道接口的底层表示,和把一个实现了接口的类型赋值给接口会发生什么,才能理解文章中的问题。
    leonshaw
        25
    leonshaw  
       15 小时 51 分钟前 via Android
    0 值也是一等公民
    xuanbg
        26
    xuanbg  
       15 小时 32 分钟前
    有些人特别讨厌 null ,为了米有 null ,于是搞出了一些更奇怪的东西
    liuguang
        27
    liuguang  
       21 分钟前
    nil 还有类型,这也是 go 语言的一大败笔设计
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2867 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 03:58 · PVG 11:58 · LAX 19:58 · JFK 22:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.