Go 里减少空指针异常的小经验

原文地址:https://romatic.net/post/avoid_npe_in_go/

空指针异常 NPE 在所有编程语言里都是个很麻烦的事情,Go 在设计之初已经在尽力减少 null 的使用范围。但是由于 Go 刻意隐藏了值和引用的概念,很多新手在编码时容易搞混空引用和空值,引发了不少 panic。

这里试图提供一些减少 NPE 的方法出来。经验之谈,供参考。


先来看一种最常见的情形

定义嵌套结构体时,尽可能不嵌套指针

比较容易理解

type Male struct{
    Human
}

组合时优先用 Human 而不是 *Human

有人会顾虑,那我想用 *Human 的方法怎么办,其实,*Male 其实是包含 *Human 的方法的。

这样做最主要的原因,也是很多人在 new(Male) 时忘记 new(Human),导致给上层抛了个 nil。如果这个 struct 直接转成 json 抛了出去,下游恰好对 null 也没处理好,这就是个跨端 bug 了。

帮同事查问题时还遇到过更隐藏的坑,这个 Human 里可能还有个结构体指针假如是 *Face,代码从 Male 直接调 *Face 的方法,自然就 panic 了。悲催的是,在 IDE 里帮他调代码,会直接跳过 Human 这一层,在阅读代码时没有直接找到问题所在,不得不搬出 DEBUG 才看到。

这个也可以衍生一个小建议,定义变量尽量用 struct 而不是指针,传参的时候再使用。不过到底有多少收益,还值得商榷。

函数尽可能不返回 nil

看一个连环坑

// 获取 user 对象
func GetUser() (*User, error)

func main() {
    user,err := GetUser()
    if err != nil { 
        write(err.Error())
        return 
    }

    println(user.Name)  // panic user=nil
}

一般的,我们会觉得既然我都判 error 了,user 的值总该是正常了吧。只能说 too naive,真正垃圾的代码是没有底线的。反应快的人可能马上想到解决办法,在 err != nil 的地方也判一下 user:

if err != nil || user == nil {
    write(err.Error())
}

然后,就悲催的发现还是 panic 了。因为当 user=nil && err==nil 时,也会走到 err.Error() 这里,这里的 err.xx 又是一个 NPE!

老老实实的一个个处理固然是好办法,但是难保谁一个手抖。

所以我们换个思路,想想能不能对 GetUser 这个函数做一些要求。问题就变成了有什么简单的办法让函数不返回 nil。

不说中间的尝试了,直接说我们的结论:

函数返回值可能返回 nil 时,定义返回值必须 带上变量名,并且在函数体内 首行进行初始化。函数返回时 不带变量名

给个例子:

func GetUsers() (users []*User, err error) {
    users = make([]*User, 0, 32)
    // function body
    return
}

三个条件

  • 必须有变量名
  • 必须首行初始化
  • return 无参数

这三点共同保证第一个目的:函数在任何地方 return,都不会给上层抛出 nil

具体解释一下,为什么 变量名放在函数签名里而不在 return 里。是因为当函数很复杂需要多个 return 时,每个 return 时 users 里是啥你心里不一定有概念。也顾不上去考虑。索性把这个任务就交给定义阶段了。

另外,返回值在函数开头就一起定义&初始化了。在 code review 时也更容易注意到。在看函数体的时候也不用再去想这个问题了。


调用函数时尽可能不传 nil

在 Go 里有个很普遍的情况,函数的最后一个入参其实表示的是函数返回值。看例子:

func getUserArticles(userId int, articles map[int]Article) {
    articles[1] = &Article{}    // panic: articles 未初始化
}

好说,那我 new 一个吧。一般没问题。

但是如果 articles 里已经有一部分数据了,这里只是需要你 append 呢?更常见的,articels 是个结构体指针,里面有一些字段是需要的,你不能给删咯。

还有,如果这个参数传了好多层,鬼还记得他里面到底是啥。

针对这种 case,我们也做了一些简单的约定:

谁定义,谁初始化

参照这个例子来说,

  • 如果函数为 func() articles,那我来初始化,保证不返回 nil,如果保证呢?参照上面那条规范。
  • 如果函数为 func(articles),那调用方来初始化,保证不传 nil

两个简单的约束,保证绝大多数参数简单稳定地运行。


下班时突然心血来潮想整理一下,休息一下。未完待续。。

欢迎讨论。

你可能感兴趣的:(Go 里减少空指针异常的小经验)