Go 的 nil 值判断,千方百计,还是踩坑!

c7f4a8d9c5d8d761faa9094bd4cf0a61.png

今天给大家分享一个实际踩坑的一个示例,以为的 nil 并不是 nil。众所周知,Go 编程中 nil 值的判断可谓是随处可见:

v := findSomething()
if v != nil {
    // do something
}

nil 值究竟是什么?这个在之前的文章也分析过( 深度剖析 Go 的 nil ),并且也提到了在 interface 相关赋值的时候有差异。但是在实际的项目开发的时候,由于流程过于复杂,代码量过于多,还是不可避免的踩到坑。什么坑呢?

以为某个 interface 判断应该是 nil ,但其实判断是 非 nil 的。其实这个知识点以前也理解过,但是又踩到了,太隐蔽了,所以专门分享一次。

5740dfa8592e73ddd8800e4c5510e1b9.png

复习 interface 的 nil 知识

9dc58adad1ee05f1791cb061b0bd9886.png

它的定义长这样:

type iface struct {
    tab *itab
    data unsafe.Pointer
}
type eface struct {
    _type *_type
    data unsafe.Pointer
}
  • interface 变量定义是一个 16 个字节的结构体,首 8 字节是类型字段,后 8 字节是数据指针。普通的 interface 是 iface 结构,interface{} 对应的是 eface 结构;

  • interface 变量新创建的时候是 nil ,则这 16 个字节是全 0 值;

  • interface 变量的 nil 判断,汇编逻辑是判断首 8 字节是否是 0 值;

3f0002fd6ce5cc5040674d07408fcb04.png

踩坑记录

f25ae5d7bf654bfa2858b3ade34887d5.png

奇伢的踩坑操作很简单,但也还是把我下了一身冷汗。在一个函数里明明返回了 nil 值,但是到了外面判断却是非 nil

type Worker interface {
    Work() error
}

type Qstruct struct{}

func (q *Qstruct) Work() error {
    return nil
}

// 返回一个 nil 
func findSomething() *Qstruct {
    return nil
}

func main() {
    // 定义好接口
    var v Worker

    v = findSomething()
    if v != nil {
        // 走的是这个分支
        fmt.Printf("v(%v) != nil\n", v)
    } else {
        fmt.Printf("v(%v) == nil\n", v)
    }
}

震惊!findSomething 这个函数返回的明确是 nil,但是 if 判断的分支走的是 != nil 这个分支。当时我确实愣了 10 秒,然后才反应过来。虽然知识点以前知道,但是实际编程还是不免踩坑。

 1   深究下这里的原因是啥?

回到 v = findSomething() 这行代码。这个是关键。这个是一个赋值操作,左边是一个接口变量,函数 findSomething 返回的是一个具体类型指针。所以,它一定会把接口变量 iface 前 8 字节设置非零字段的,因为有具体类型呀(无论具体类型是否是 nil 指针)。而判断 interface 是否是 nil 值,则是只根据 iface 的前 8 字节是否是零值判断的。

划重点:具体类型到接口的赋值一定会导致接口非零(不考虑编译不过等问题)。

这个问题的原理就这样简单。

 2   那么怎么改呢?

记住一个原则:如果任何地方有判断接口是否为 nil 值的逻辑,那我建议你一定不要写任何有 接口 = 具体类型(nil) 逻辑的代码。如果是 nil 值就直接赋给接口,而不要过具体类型的转换。

所以上面的改动很简单:

// 如果 findSomething 需要返回 nil 值,那么直接返回 nil 的 interface 
func findSomething() Worker {
    return nil
}

这样,findSomething 需要返回 nil 的时候,则是直接返回 nil 的 interface,这是一个 16 个字节全零的变量。而在外面赋值给 v 的时候,则是 interface 到 interface 的赋值,所以 v = findSomething() 的赋值之后,v 还是全 0 值。

3abdeaab065d63578d12c8b44d2b5116.png

总结

2baae3a1528cecc489bff152c5e040b5.png

  1. 千万要注意。如果 interface 被具体类型变量赋值过,变量的类型会被赋值到首 8 字节。从而导致 interface 非 nil 。无论具体类型变量本身是不是 nil;

  2. 如果函数返回具体类型,然后在其他地方又要赋值给接口,那还不如函数直接返回接口类型;

  3. 过一遍 interface 的原理,背诵:千万不要写任何可能存在 接口 = 具体类型(nil) 的代码,如果有 nil 值,直接赋给接口吧,不要再过中间商了;

  4. 再背一遍;

推荐阅读:

太疯狂了,Go 程序说 nil 不是 nil...

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!827edb72477416c244d4b6854c7ff8a5.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值