本文主要用于分析几类 Golang 空指针引起的 panic 场景,这些场景遍布在日常开发代码中。但是出现 panic 时,真的能根据空指针信息准确定位出原因,找出故障代码吗?
来看看熟悉的 panic 信息
panic: runtime error: invalid memory address or nil pointer dereference
见识一下“熟悉”的 panic
空 map 赋值
source code
package main
func main() {
var m map[int]int m[1] = 1
}
// Output:
// panic: assignment to entry in nil map
第一个需要避免的就是对未初始化的 map 进行赋值,当然,这个并不是本篇文章需要讨论的,但是笔者认为 map 作为常用的组件,还是需要在编码时避免此场景发生。
结构体指针未初始化
可以看出 t.n
结构体会跟随者 T
的初始化而完成初始化,此时 t.n.Log0()
和t.n.Log()
调用都成功了。
但是 t.np
结构体指针并不会随着 T
的初始化而完成初始化,此时 t.np
实际类型是 (*N)nil
,它是有类型的,类型为 *N
,但是 t.np
值为 nil。因此 t.np.Log()
能正常调用,但是 t.np.Log0()
则会 panic
。
source code
package main
import (
"fmt"
)
type T struct {
n N
np *N
}
type N struct {
n int
}
func (n N) Log0() {
fmt.Println("n log0")
}
func (n *N) Log() {
fmt.Println("n log")
}
func main() {
t := T{}
t.n.Log()
t.n.Log0()
t.np.Log()
t.np.Log0()
}
// Output:
// n log
// n log0
// n log
// panic: runtime error: invalid memory address or nil pointer dereference
interface{} 未初始化
相较于结构体指针,当成员变量是一个接口(interface{}
)时,结果又会变得不一样。
此时 t.m
是一个接口 M
,当初始化创建 T
时,如果不主动初始化 M
,那么此时 M
是一个 nil
。调用 t.m.Log()
会造成 panic
。
source code 1
package main
import (
"fmt"
)
type T struct {
m M
}
type M interface {
Log()
}
type M1 struct {
}
func (m *M1) Log() {
fmt.Println("m1 log")
}
func main() {
t := T{
m: &M1{},
}
t.m.Log()
t = T{}
t.m.Log()
}
// Output:
// m1 log
// panic: runtime error: invalid memory address or nil pointer dereference
source code 2
package main
type M interface {
Log()
}
func main() {
var m M
m.Log()
}
// Output:
// panic: runtime error: invalid memory address or nil pointer dereference
interface{} assign nil
这个 case 在代码中常有发生,根因还是在于 Golang 的 nil 机制有个特殊的地方,可以看source code 1。
source code 1
package main
import (
"fmt"
)
func main() {
var i interface{}
var n *int32
if n == nil {
fmt.Println("n is nil")
}
if i == nil {
fmt.Println("i is nil")
}
i = n
if i != nil {
fmt.Println("after assign, i is not nil")
}
}
// Output:
// n is nil
// i is nil
// after assign, i is not nil
基于这个规则,常见的业务中出现 panic
的场景可以看如下代码。在这个场景下,panic
的原因同结构体指针未初始化的场景一致,根因是对于变量是否为 nil 的判断上出了问题。
source code 2
package main
import (
"fmt"
)
type M interface {
Log()
}
type M1 struct {
}
func (m M1) Log0() {
fmt.Println("log0")
}
func (m *M1) Log() {
fmt.Println("log")
}
func main() {
var i interface{}
var m M
var s *M1
if i == nil {
fmt.Println("i is nil")
}
i = m
if i == nil {
fmt.Println("after assign m, i is nil")
}
i = s
if i != nil {
fmt.Println("after assign s, i is not nil")
}
// 业务中常用 == nil 判断,然后快速失败 return
if i == nil {
return
}
// 业务中会认为此时的 i 就是非 nil 的了,然后开始进行方法调用
// 和结构体指针未初始化的场景一样,这里调用 *M 的方法是可以进行的
i.(*M1).Log()
// 但是如果调用的是 M 的方法,那就出问题了,panic 随之而来
i.(*M1).Log0()
}
// Output:
// i is nil
// after assign m, i is nil
// after assign s, i is not nil
// log
// panic: runtime error: invalid memory address or nil pointer dereference
要改变是否为 nil 的判断,确保这种 case 不会发生。可以使用:
func IsNil(x interface{}) {
return x == nil || (reflect.ValueOf(x).Kind() == reflect.Ptr && reflect.ValueOf(x).IsNil())
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nickchenyx@gmail.com