Featured image of post Go中经典nil!=nil问题

Go中经典nil!=nil问题

背景

最近在开发过程中遇到了一个奇怪的现象,对传入参数interface{}的做nil判断的时候,发现有些情况不符合预期,

示例代码

可以猜测一下下方代码执行后的打印结果

func TestIsNilEqual(t *testing.T) {
	PatchConvey("test", t, func() {
		var isNil = func(v interface{}) bool {
			return v == nil
		}

		PatchConvey("test map", func() {
			var testMap map[string]interface{}
			test1 := isNil(map[string]interface{}{})
			test2 := isNil(testMap)
			test3 := testMap == nil
			test4 := isNil(nil)

			t.Logf("result 1: %v", test1)
			t.Logf("result 2: %v", test2)
			t.Logf("result 3: %v", test3)
			t.Logf("result 4: %v", test4)
		})
	})
}

原因解析

非空和空类型

在 go 中类型可以是空或非空

  • 非空类型永远不能为 nil,并且永远不会导致nil-panic(等效于 Java 的 NullPointerException)
  • 处理空类型时,我们仍须谨慎一点,可能会导致nil-panic

非空基本类型

在 go 中,基本类型不可为空。像这样的声明,都会有默认0值

Types Zero value
int, int8, int16, int32, int64 0
uint, uint8, uint16, uint32, uint64 0
uintptr 0
float32,float64 0.0
byte 0
rune 0
string "" (empty string)
complex64, complex128 (0,0i)
arrays of non-nillable types array of zero-values
arrays of nillable types array of nil-values

Non-nillable structs 非空结构体

组合的 struct 类型也是不可空的,并且 struct 的默认值将包含其所有字段的默认值

Nillable 可为空类型

还有一种更高级到 nillable 类型,如果对应的类型未初始化,将会报错,触发 panic

这些可以为 nillabe 类型:

  • 函数
  • 通道
  • Slice切片
  • map
  • 接口
  • 指针

但是,nil-slice 和 nil-maps 仍然可以使用,在我们开始使用它们之前不必进行初始化。

  • nil-maps 可以取值,但是不能赋值
  • nil-slices 在外部引用切片将导致死机,但是len()cap()的操作不会导致死机出现。他们只返回0, 因为对于未初始化的切片,其容量和长度都为零,所以可以在 nil-slice 上成功调用 append

可为空的指针、函数和接口类型会引起 panic

每当处理这些类型时,我们都必须考虑它们是否为零

nil-channel 永远阻塞

尝试从 nil 通道读取或写入 nil 通道将永远受阻。关闭 nil 通道会引起 Panic

nil预声明标识符nil没有默认类型

Go中其它的预声明标识符都有各自的默认类型,比如

  • 预声明标识符true和false的默认类型均为内置类型bool。
  • 预声明标识符iota的默认类型为内置类型int。 但是,预声明标识符nil没有一个默认类型,尽管它有很多潜在的可能类型。 事实上,预声明标识符nil是Go中唯一一个没有默认类型的类型不确定值。 我们必须在代码中提供足够的信息以便让编译器能够推断出一个类型不确定的nil值的期望类型。
package main

func main() {
	// 代码中必须提供充足的信息来让编译器推断出某个nil的类型。
	_ = (*struct{})(nil)
	_ = []int(nil)
	_ = map[int]bool(nil)
	_ = chan string(nil)
	_ = (func())(nil)
	_ = interface{}(nil)

	// 下面这一组和上面这一组等价。
	var _ *struct{} = nil
	var _ []int = nil
	var _ map[int]bool = nil
	var _ chan string = nil
	var _ func() = nil
	var _ interface{} = nil

	// 下面这行编译不通过。
	var _ = nil
}

两个不同类型的nil值可能不能相互比较

// error: 类型不匹配
var _ = (*int)(nil) == (*bool)(nil)
// error: 类型不匹配
var _ = (chan int)(nil) == (chan bool)(nil)

type IntPtr *int
// 类型IntPtr的底层类型为*int。
var _ = IntPtr(nil) == (*int)(nil)

// 任何类型都实现了interface{}类型。
var _ = (interface{})(nil) == (*int)(nil)

// 一个双向通道可以隐式转换为和它的
// 元素类型一样的单项通道类型。
var _ = (chan int)(nil) == (chan<- int)(nil)
var _ = (chan int)(nil) == (<-chan int)(nil)

同一个类型的两个nil值可能不能相互比较

在Go中,map、slice和func类型是不支持比较类型。比较同一个不支持比较的类型的两个值(包括nil值)是非法的。 比如,下面的几个比较都编译不通过。

var _ = ([]int)(nil) == ([]int)(nil)
var _ = (map[string]int)(nil) == (map[string]int)(nil)
var _ = (func())(nil) == (func())(nil)

但是,映射类型、切片类型和函数类型的任何值都可以和类型不确定的裸nil标识符比较。

var _ = ([]int)(nil) == nil
var _ = (map[string]int)(nil) == nil
var _ = (func())(nil) == nil

nil!=nil

  • nil 在 Go语言中只能被赋值给指针和接口。
  • 但是接口在底层的实现有两个部分:
    • type
    • data
  • 在源码中,显式地将 nil 赋值给接口时,接口的 type 和 data 都将为 nil。此时,接口与 nil 值判断是相等的
  • 但如果将一个带有类型的 nil 赋值给接口时,只有 data 为 nil,而 type 为 nil,此时,接口与 nil 判断将不相等。

解决方式

这里提供了一种针对类型不为空但是值为空的判断方式

// IsNil 判断指针,Slice,Map类型的值是否为空
func IsNil(i interface{}) bool {
	// 首先判断i是否为nil
	if i == nil {
		return true
	}

	// 判断值是否为nil
	vi := reflect.ValueOf(i)
	if vi.Kind() == reflect.Ptr ||
		vi.Kind() == reflect.Slice ||
		vi.Kind() == reflect.Map {
		return vi.IsNil()
	}

	return false
}

参考文档

Licensed under CC BY-NC-SA 4.0
Owner Rainbowly
Total [] user views [] site views [] page views
Built with Hugo
主题 StackJimmy 设计