最新文章: 官方的讨论: Why is my nil error value not equal to nil?

最近在项目中踩了一个深坑——“Golang中一个包含nil指针的接口不是nil接口”,总结下分享出来,如果你不是很理解这句话,那推荐认真看下下面的示例代码,避免以后写代码时踩坑。

先一起来看下这段代码,你感觉有没有问题呢?

type IPeople interface {
	hello()
}
type People struct {
}

func (p *People) hello() {
	fmt.Println("github.com/meetbetter")
}

func errFunc1(in int) *People {
	if in == 0 {
		fmt.Println("importantFunc返回了一个nil")
		return nil
	} else {
		fmt.Println("importantFunc返回了一个非nil值")
		return &People{}
	}

}

func main() {
	var i IPeople

	in := 0

	i = errFunc1(in)

	if i == nil {

		fmt.Println("哈,外部接收到也是nil")
	} else {

		fmt.Println("咦,外部接收到不是nil哦")
		fmt.Printf("%v, %T\n", i, i)
	}

}
复制代码

这段代码的执行结果是:

importantFunc返回了一个nil
咦,外部接收到不是nil哦
<nil>, *main.People
复制代码

可以看到在main函数中收到的返回值不是nil, 明明在errFunc1()函数中返回的是nil,到了main函数为什么收到的不是nil呢? 这是因为:将nil赋值给*People后再将*People赋值给interface,*People本身是是个指向nil的指针,但是将其赋给接口时只是接口中的值为nil,但是接口中的类型信息为*main.People而不是nil,所以这个接口不是nil。 是的,Golang中的interface类型包含两部分信息——值信息和类型信息,只有interface的值合并类型都为nil时interface才为nil,interface底层实现可以在后面的源码分析看到。

先来看看正确的处理接口返回值的方法,是直接将nil赋给interface:

func rightFunc(in int) IPeople {
	if in == 0 {
		fmt.Println("importantFunc返回了一个nil")
		return nil
	} else {
		fmt.Println("importantFunc返回了一个非nil值")
		return &People{}
	}

}
复制代码

下面的代码更清晰的证明了一个包含nil指针的接口不是nil接口的结论:

type IPeople interface {
	hello()
}
type People struct {
}

func (p *People) hello() {
	fmt.Println("github.com/meetbetter")
}


func errFunc() *People {
	var p *People

	return p
}


func rightFunc() IPeople {
	var p *People

	return p
}
func main() {

	if errFunc() == nil {

		fmt.Println("对了哦,外部接收到也是nil")
	} else {

		fmt.Println("错了咦,外部接收到不是nil哦")

	}

	if rightFunc() == nil {

		fmt.Println("对了哦,外部接收到也是nil")
	} else {

		fmt.Println("错了咦,外部接收到不是nil哦")

	}

}
复制代码

输出结果:

对了哦,外部接收到也是nil
错了咦,外部接收到不是nil哦
复制代码

下面的注释信息来自参考文章中,从interface底层实现可以看出iface比eface 中间多了一层itab结构, itab 存储_type信息和[]fun方法集,所以即使data指向了nil 并不代表interface 就是nil, 还要考虑_type信息。

type eface struct {      
    _type *_type         
    data  unsafe.Pointer 
}
type iface struct {      
    tab  *itab           
    data unsafe.Pointer  
}
type _type struct {
    size       uintptr  
    ptrdata    uintptr  
    hash       uint32   
    tflag      tflag
    align      uint8    
    fieldalign uint8    
    kind       uint8    
    alg        *typeAlg 
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}
type itab struct {
    inter  *interfacetype  
    _type  *_type          
    link   *itab
    bad    int32
    inhash int32
    fun    [1]uintptr      
}
复制代码

2019-12-13补充

下面代码段:

package test

import "testing"

type Intf interface {
	Write(string)
}

type T struct {}

func (t *T) Write(str string) {}

func CreateT() *T { return nil }

func CreateIntf() Intf { return nil }

func TestInterfaceNil(t *testing.T) {
	// 方式1
	t1 := CreateT()
	t.Log(t1 == nil)

	// 方式2
	var t2 *T
	t2 = CreateT()
	t.Log(t2 == nil)

	// 方式3
	var t3 Intf
	t3 = CreateT()
	t.Log(t3 == nil) // 结果与预期不一致,
	// 为什么会这样?
	// 其实一个interface{}类型包含两个指针, 一个指向值类型一个指向值内容
	// 如果定义了一个interface{}类型, 如同定义了一个结构体一样, 会初始化内部数据的指向.
	//当我们将一个具体类型的值赋值给一个interface类型的变量的时候,就同时把类型和值都赋值给了interface里的两个指针。如果这个具体类型的值是nil的话,interface变量依然会存储对应的类型指针和值指针。

	var t4 Intf
	t4 = CreateIntf()
	t.Log(t4 == nil)


}

如果外层定义了一个接口类型变量, 会自动初始化inerface内部的指针, 如果函数返回的是接口的具体实现类型, 那个go语言会自动使用nil concrete-typed value 而非 nil inerface value, 所以在方式3中判断是t3 != nil, 如果函数返回的就是接口类型, 那么go语言会将返回值作为 nil intface value赋值, 所以, 此时t4 == nil.

以上完整代码均整理在Github-跟着示例代码学Golang项目

参考文章: Golang第一大坑

"一个包含nil指针的接口不是nil接口"的讨论


原网址: 访问