漫谈 Golang 空指针 panic 场景

  1. 见识一下“熟悉”的 panic
    1. 空 map 赋值
    2. 结构体指针未初始化
    3. interface{} 未初始化
    4. interface{} assign nil

本文主要用于分析几类 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

Title:漫谈 Golang 空指针 panic 场景

Count:1k

Author:nickChen

Created At:2022-07-07, 22:23:14

Updated At:2023-05-08, 23:27:10

Url:http://nickchenyx.github.io/2022/07/07/golang_nil_pointer_panic/

Copyright: 'Attribution-non-commercial-shared in the same way 4.0' Reprint please keep the original link and author.