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

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


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

欢迎讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容