本文主要来探讨在go日常开发中,通过接口对象调用其方法时,导致程序崩溃,出现如下错误的原因:

panic: runtime error: invalid memory address or nil pointer dereference

现有如下代码:

1 package main
 2 
 3 import (
 4     "fmt"
 5 )
 6 
 7 type Person interface {
 8     GetGender() string
 9 }
10 
11 type Male struct {
12     Gender string
13 }
14 
15 func (b *Male) GetGender() string {
16     return b.Gender
17 }
18 
19 func PrintGender(p Person) {
20     fmt.Println(p.GetGender())
21 }

如上述代码所示,Male结构类型实现了Person接口。并有一独立函数负责在终端打印Person对象的性别

现在我们来看看,在调用上述独立函数时,当传入不同情况的实参,会导致程序出现什么问题。

1. 传入空值:

调用如下:

1 func main() {
2     PrintGender(nil)
3 }

程序在运行时崩溃:

1 panic: runtime error: invalid memory address or nil pointer dereference
2 [signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x482096]
3 
4 goroutine 1 [running]:
5 main.PrintGender(0x0, 0x0)
6     /home/rf/RF/gopath/src/practice/method/src/main.go:20 +0x26
7 main.main()
8     /home/rf/RF/gopath/src/practice/method/src/main.go:25 +0x29

我们可以看到程序是在20行通过Person对象p调用方法GetGender()时崩溃的,原因稍后详述。

2. 传入Male类型的空指针:

调用如下:

1 func main() {
2     var m *Male
3     PrintGender(m)
4 }

程序亦于运行时崩溃:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x482075]

goroutine 1 [running]:
main.(*Male).GetGender(0x0, 0xc42000e1d0, 0xc42004ff38)
    /home/rf/RF/gopath/src/practice/method/src/main.go:16 +0x5
main.PrintGender(0x4c3980, 0x0)
    /home/rf/RF/gopath/src/practice/method/src/main.go:20 +0x35
main.main()
    /home/rf/RF/gopath/src/practice/method/src/main.go:25 +0x36

此时程序是在16行访问接受者Male的成员变量Gender时崩溃的。

 

分析:

我们知道,Go语言中接口的值其实有两部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。而且在Go中如果变量定义的时候没有初始化的话,编译器会给其初始化一个特定的零值,接口也不例外。接口的零值就是把它的动态类型和值都设置为nil,如下图:

Grafana 显示接口调用时间 接口调用出错_动态类型

一般来讲,在编译时我们无法知道一个接口的动态类型会是什么,所以通过接口来做方法的调用,必然需要使用动态分发机制。编译器会生成一段代码来从类型描述符拿到名为GetGender方法的地址,再间接调用该地址。调用的方法的接受者为接口的动态值。而在第一种情况下,我们传入的是nil,也就是说当在PrintGender()中调用GetGender()方法时,接口对象p为空,也就是p的动态类型为空,进而就是说此种情况不会发生动态分发,所以当调用其方法时会导致程序崩溃。

我们再来看第二种情况,此时传入了一个Male类型的空指针,于是接口对象p的动态类型就变成了*Male,如下:

Grafana 显示接口调用时间 接口调用出错_Grafana 显示接口调用时间_02

我们把此时的p叫做包含空指针的非空接口。如上所述,此时通过p调用其方法GetGender()时,动态分发机制决定了最终会调用(*Male).GetGender(),也是说明了第二种情况下方法的调用没有导致程序崩溃的原因,这样是合法的。但是,现在仍然存在一个问题,p的动态值为空,那么也决定了调用(*Male).GetGender()时,其接受者为空,也就是说接受者没有指向一个具体的Male实例,所以在16行访问接受者成员变量Gender时,最终导致了程序的崩溃。