前言

在Go语言里,每一个细节都蕴含着深刻的设计哲学与实现智慧。今天,我们将深入剖析Go语言中三个常被提及却又容易让人困惑的“空”——空结构体、空接口以及nil,并揭开接口底层实现的神秘面纱。

这三个概念,虽然听起来简单,但在Go语言的编程实践中却扮演着举足轻重的角色,它们既是Go语言灵活性和强大功能的体现,也是初学者容易踏入的“陷阱”。

Go语言接口的底层表示

在开始探讨这三个“空”之前,我们有必要了解一下Go语言接口的底层表示。Go语言的接口不同于Java,Go采用隐式接口,只要实现了所有的接口方法就是自动实现接口。这样做可以在不修改代码的情况下抽象出新的代码。

接口的值(注意不是接口本身)的底层表示为runtime/runtime2.go中的iface结构体:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

data指针指向的是我们构建的结构体,itab指针记录一个包含接口类型和实现方法的结构体,代码如下:

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

itab包含如下信息,可以用于做类型断言

  • inter:接口类型
  • type:接口装置的值的类型
  • func:本类型实现了哪些方法,虽然写死为1,但在编译器会变长

类型断言

类型断言是一个使用在接口值上的操作,可以将接口值转换为其他类型值(只要接口兼容),也可以配置switch进行类型断言。

类型断言的语法:

var a Animal = Cat{}
cat := a.(Cat)

类型断言会比较这个接口的底层实现中是否实现了目标接口的所有方法。

switch结合的语法:

switch(c.type){
    case Cat:
        break
    case Dog:
        break
}

结构体和指针实现接口

关于这个点,我们先来看一段代码,我们定义一个Animal接口和Cat结构体,Cat实现了Animal。

type Animal interface {
    Walk()
}

type Cat struct {
    Name string
}

func (c Cat) Walk() {
    fmt.Println(c.Name, " is walking...")
}

func main() {
    var c Animal = Cat{
       Name: "Tom",
    }
    c.Walk()
}

这里main方法能直接通过Animal类型接收Cat的结构体,编译没问题,正常运行。

接着,我们尝试把Cat结构体改成Cat指针,也就是如下代码:

func main() {
    var c Animal = &Cat{
       Name: "Tom",
    }
    c.Walk()
}

此时我们也会发现,系统也不会报错。这是因为当我们通过结构体,即(c Cat)实现了方法时,系统会自动帮我们实现一个(c *Cat)的方法。

我们可以通过编译为Plan9汇编代码发现这一点,编译器帮我们把两个方法都实现了!使用go build -gcflags -S main.go

image.png

image.png

然而需要注意的是,当我们只实现了(c *Cat)的方法,系统是不会帮我们实现(c Cat)方法的,所以此时我们再用Cat{}来初始化就会出错!

空接口

空接口可以承载一切数据。空接口的本质是runtime/runtime2.go下面的eface,里面包含_typedata,这可以表示任意的数据。(任何对象都可以表示为“类型+数据”)

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

空接口常用于方法的入参,表示方法的入参可以是任何类型的数据。底层会在编译的时候,new一个eface,传入eface作为参数。

空结构体

空结构体是Go中特殊的类型,空结构体不是Nil

当空结构体单独出现的时候,其指针不是Nil,而是指向一个zerobase的地址。

  • 用途:节约内存。
  • 应用:
    • Go的Hashset如何实现?Value设置为一个空结构体即可
    • channel如何发送信号,不携带任何信息?使用空结构体即可!
m:=map[string]struct{}{}

a:=make(chan struct{})

nil

nil在Go里面叫做空,但不是Java中的空指针概念。要搞清楚nil到底是什么,我们可以进入builtin/builtin.go

// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

可见,nil就是个变量,它只能表示六种类型,分别是指针、channel、函数、接口、map、slice。注意,Nil不能表示空的结构体!它只表示这六种类型的零值。

当a为Nil,b也为Nil时,a==b依旧是非法的。可见:Nil是有类型的

var a *int
fmt.Println(a == nil)
var b map[string]int
fmt.Println(b == nil)
fmt.Println(a == b) // 报错

nil和空接口

我们来看下面代码:

func main() {
    var a *int
    var b interface{}
    fmt.Println(a == nil) //true
    fmt.Println(b == nil) //true
    b = a
    fmt.Println(b == nil) //false
}

为什么a和b原来都是nil,而当我们给空接口赋一个空指针后,就不是nil了?

原因是对于空接口,其底层实现是eface结构体,包括type和data。一开始type和data都为空,所以是nil。当我们赋值之后,type就不是空了,此时整个接口自然也就不是nil了。可见:只有type和data都是空的时候,接口才是nil

小结

通过今天的探讨,我们深入剖析了Go语言中三个重要的“空”概念——空结构体、空接口以及nil,并揭开了接口底层实现的神秘面纱。下面是面试常考的点:

  • nil是六个类型的零值。
  • 空结构体的指针和值都不是nil。
  • 空接口的type和data都是空的时候,接口才是nil。