本文主要来探讨在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,如下图:
一般来讲,在编译时我们无法知道一个接口的动态类型会是什么,所以通过接口来做方法的调用,必然需要使用动态分发机制。编译器会生成一段代码来从类型描述符拿到名为GetGender方法的地址,再间接调用该地址。调用的方法的接受者为接口的动态值。而在第一种情况下,我们传入的是nil,也就是说当在PrintGender()中调用GetGender()方法时,接口对象p为空,也就是p的动态类型为空,进而就是说此种情况不会发生动态分发,所以当调用其方法时会导致程序崩溃。
我们再来看第二种情况,此时传入了一个Male类型的空指针,于是接口对象p的动态类型就变成了*Male,如下:
我们把此时的p叫做包含空指针的非空接口。如上所述,此时通过p调用其方法GetGender()时,动态分发机制决定了最终会调用(*Male).GetGender(),也是说明了第二种情况下方法的调用没有导致程序崩溃的原因,这样是合法的。但是,现在仍然存在一个问题,p的动态值为空,那么也决定了调用(*Male).GetGender()时,其接受者为空,也就是说接受者没有指向一个具体的Male实例,所以在16行访问接受者成员变量Gender时,最终导致了程序的崩溃。