Golang interface 类型的 nil 居然不等于字面量 nil?

20/Sep/2023 · 2 minute read

问题描述

昨天开发的一段代码在运行时遇到了奇怪的 panic 问题,报错:

runtime error: invalid memory address or nil pointer dereference

但是奇怪的是,代码中 panic 出处,我是有判断 nil 的:

// 这是相关的结构体定义
type Options struct {
    sourceFrame fmt.Stringer // 错误根源栈页
}

// 省略一堆无关代码

if options.sourceFrame != nil {   // 这里判断了 options.sourceFrame 是否为 nil
    metaData[metaKeySourceFrame] = options.sourceFrame.String()  // 这一行代码触发 panic
}

好奇怪,我不是都判断了是否为 nil 了吗?为什么还会报错呢?

问题分析

经过一番搜索和学习,最终发现 golang 的一个坑:interface 类型的 nil 不等于字面量 nil

Under the hood, an interface in Golang consists of two elements: type and value.

在我的实际代码中,sourceFrame 变量的值是 {fmt.Stringer | *frame.Frame} *frame.Frame(nil),也就是说,对于 golang 运行时,我的 sourceFrame 是“类型为 frame.Frame 的指针,且值为空”,当用它去和 nil 对比的时候,它们虽然值是一样的,但是类型不一样,字面量 nil 的类型为 nil

示例程序

程序1:有类型的 nil

package main

import "fmt"

func main() {
	var a interface{}
	fmt.Printf("%T\n", nil)
	fmt.Printf("%T\n", a)

	a = (*string)(nil)
	fmt.Printf("%T\n", (*string)(nil))
	fmt.Printf("%T\n", a)
}

这个代码测试不同类型的 nil,输出结果为:

<nil>
<nil>
*string
*string

程序2:测试 interface 类型的 nil 和字面量 nil 是否相等

package main

import (
	"fmt"
)

func main() {
	emptyStringPtr := (*string)(nil)
	fmt.Println(emptyStringPtr == nil) // true

	var a interface{}
	fmt.Println(a == nil)              // true

	a = emptyStringPtr
	fmt.Println(a == nil)              // false

	fmt.Println(a == (*string)(nil))   // true
	fmt.Println(a == emptyStringPtr)   // true
    fmt.Println(a.(*string) == nil)   // true
}

这个代码分别测试了具体类型的空指针和类型为空接口的空指针和字面量 nil 的比较,输出结果已经对应注释在对应代码末尾。

所以通过这个测试可以确认:

  1. 具体类型的空指针和字面量 nil 是相等的;
  2. 类型为接口的空指针是否等于字面量 nil,取决于它的值是否为 nil,并且它的类型也为 nil;
  3. 如果想要将类型为接口的变量和字面量 nil 进行比较,需要将它们转换为具体类型的指针,然后再进行比较。

但是这里也有一个好玩的事情,就是按照这个规律,三个变量之间的相等性,是没有传递性的,因为 a == emptyStringPtr,且 emptyStringPtr == nil,但是 a != nil。😳 猜测这是因为 golang 在中间做了隐式类型转换,因为前两种等式可以推导具体类型,而最后一种等式无法推导具体类型,所以无法进行隐式类型转换。

如何优雅地判断 interface 类型的 nil?

除了前面说的转换为具体类型再比较,但是通常来说运行时具体类型不可预知。另一种可行的方法是通过反射来实现:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a interface{} = (*string)(nil)
	fmt.Println(a == nil)                                                               // false
	fmt.Println(reflect.ValueOf(a).Kind() == reflect.Ptr && reflect.ValueOf(a).IsNil()) // true
}

参考资料

版权声明:本文为原创文章,转载请注明来源:《Golang interface 类型的 nil 居然不等于字面量 nil? - Hackerpie》,谢绝未经允许的转载。