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

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


先来看一种最常见的情形

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

比较容易理解

type Male struct{
    Human
}
*Human
*Human*Male*Human
new(Male)new(Human)
Human*FaceMale*FaceHumanDEBUG

这个也可以衍生一个小建议,定义变量尽量用 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
}
err != nil
if err != nil || user == nil {
    write(err.Error())
}
user=nil && err==nilerr.Error()err.xx

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

所以我们换个思路,想想能不能对 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() articlesfunc(articles)

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


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

欢迎讨论。