空指针异常 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)
两个简单的约束,保证绝大多数参数简单稳定地运行。
下班时突然心血来潮想整理一下,休息一下。未完待续。。
欢迎讨论。