lang

LangBook

10.接口

Go是强类型的语言,有了接口带来了很大的灵活性。

1. 接口的定义

接口类型就是一系列方法的集合,某类型只要实现了这些方法,就可以传给该接口。

定义一个接口rapper,只要会唱(sing), 跳(dance)它就是一个rappper

type rapper interface {
    sing()
    dance()
}

我们创建一个类型,它有sing, dance的方法

type person struct {
	name string
}
 
func (p person) sing() {
	fmt.Printf("%s 在唱\n", p.name)
}
 
func (p person) dance() {
	fmt.Printf("%s 在跳\n", p.name)
}

再创建一个函数,它接收rapper作为参数, 调用rappersingdance方法

func rap(r rapper) {
	r.sing()
	r.dance()
}

我们可以常见一个person类型变量,把它传人rap函数,因为person已经实现了rapper接口的所有方法,可以把person叫做一个rapper

func main() {
	p := person{name: "lang"}
	rap(p)
}

完整代码:

main.go
package main
 
import "fmt"
 
type rapper interface {
	sing()
	dance()
}
 
type person struct {
	name string
}
 
func (p person) sing() {
	fmt.Printf("%s 在唱\n", p.name)
}
 
func (p person) dance() {
	fmt.Printf("%s 在跳\n", p.name)
}
 
func rap(r rapper) {
	r.sing()
	r.dance()
}
 
func main() {
	p := person{name: "lang"}
	rap(p)
}

运行,输出

lang 在唱
lang 在跳

接口是隐性实现的,只要该类型实现了该接口的所有方法,就实现了该接口,可以赋值给该接口,不需要显示声明实现了该接口。

func main() {
    p := person{name: "lang"}
    var r rapper = p // 赋值给r变量, r是rapper接口类型。 
}

2. 为什么要有接口

接口的最大作用是解耦代码。使依赖和实现分离。

假设我有一个俱乐部club它需要一个rapper

type club struct {
	rap rapper // 需要一个rapper
}

俱乐部会举办了一个演出,演出期间rapper会唱sing和跳dance, 用一个show方法实现:

func (c club) show() {
	c.rap.sing() // rap 唱
	c.rap.dance() // rap 跳
}

好的,现在让club开起来, 使用person类型作为rapper

func main() {
	p := person{name: "lang"}
	c := club{rap: p} // 新建一个club, 使用p做为rapper
	c.show()          // 表演节目
}

完整代码:

main.go
package main
 
import "fmt"
 
type club struct {
	rap rapper // 需要一个rapper
}
 
func (c club) show() {
	c.rap.sing()
	c.rap.dance()
}
 
type rapper interface {
	sing()
	dance()
}
 
type person struct {
	name string
}
 
func (p person) sing() {
	fmt.Printf("%s 在唱\n", p.name)
}
 
func (p person) dance() {
	fmt.Printf("%s 在跳\n", p.name)
}
 
func main() {
	p := person{name: "lang"}
	c := club{rap: p} // 新建一个club, 使用p做为rapper
	c.show()          // 表演节目
}

运行,输出

lang 在唱
lang 在跳

到这里,还体现不出接口的好处。设想一下,有一天person病了,来不了了,club办不了了怎么办?

这时我们可以通过接口无缝切换到另一个rapper

实现另一个rapper, 一只猫也可以成为rapper:

type cat struct {
	name string
}
 
func (p cat) sing() {
	fmt.Printf("%s 在喵喵\n", p.name)
}
 
func (p cat) dance() {
	fmt.Printf("%s 在扭腰\n", p.name)
}

好的,把club开起来

func main() {
	flowerCat := cat{name: "花猫"}
	c := club{rap: flowerCat} // 新建一个club, 使用flowerCat做为rapper
	c.show()                  // 表演节目
}

完整代码:

main.go
package main
 
import "fmt"
 
type club struct {
	rap rapper // 需要一个rapper
}
 
func (c club) show() {
	c.rap.sing()
	c.rap.dance()
}
 
type rapper interface {
	sing()
	dance()
}
 
type person struct {
	name string
}
 
func (p person) sing() {
	fmt.Printf("%s 在唱\n", p.name)
}
 
func (p person) dance() {
	fmt.Printf("%s 在跳\n", p.name)
}
 
type cat struct {
	name string
}
 
func (p cat) sing() {
	fmt.Printf("%s 在喵喵\n", p.name)
}
 
func (p cat) dance() {
	fmt.Printf("%s 在扭腰\n", p.name)
}
 
func main() {
	flowerCat := cat{name: "花猫"}
	c := club{rap: flowerCat} // 新建一个club, 使用flowerCat做为rapper
	c.show()                  // 表演节目
}

运行,输出

花猫 在喵喵
花猫 在扭腰

接口就是这样,让依赖和实现分离,你可以切换自如。把接口想象成你电脑的USB接口,只要形状一样,就可以插进来。这样一个接口可以同时支持键盘,鼠标,麦克风等不同功能。如果一个设备坏了,例如鼠标坏了,直接换另一个新的鼠标插入就好,不需要拆开电脑换。

3 接口进行嵌套

有两个接口: rapper 和 programmer

type rapper interface {
	sing()
	dance()
}
 
type programmer interface {
    code()
}

你可以组合成另一个接口rapprogrammer

type rapprogrammer interface {
	rapper
    programmer
}

其实就是等价于

type rapprogrammer interface {
	sing()
	dance()
    code()
}

实现了rapprogrammer接口也就实现了rapperprogrammer接口,很好理解对吧,只要类型实现了某接口的所有方法就是实现了该接口。

4 空接口

我们经常使用any类型,可以存放所有类型的值,如何做到的?

Go全局设置了这个:

type any interface{}

any就是空接口interface{}的别名。空接口没有方法集,所有类型都实现这个接口,因为没有方法要实现嘛!很巧妙,是不是。

我们知道,接口的零值是nil

main.go
package main
 
import "fmt"
 
func main() {
	var a any // 声明一个a变量,它的类型是any 空接口
	fmt.Println(a == nil)
}

输出

true

5 接口占多少内存呢

main.go
package main
 
import (
	"fmt"
	"unsafe"
)
 
func main() {
	var a any = 1 // 声明一个空接口类型的变量
	fmt.Println(unsafe.Sizeof(a))
}

输出

16

其实,所有的接口都占16个字节,你也可以使用其他接口来尝试。为什么只占16个字节呢?我们来看看接口是怎么实现的。

// 简化写法
type interface struct {
    typPtr unsafe.Pointer // 类型指针,指向实现该接口的类型信息,8byte
    valPtr unsafe.Pointer // 值指针,指向实现该接口的值, 8byte
}

根据结构体内存对齐的规则,接口占8+8=16个字节。

a为例

var a any = 1

在内存中:

当然,接口的具体实现更为复杂,这里进行了简化帮助更好地理解。

6. 接口进行比较

接口是可比较的,要求实现该接口的类型不能是不可比较的类型(切片,map, 函数类型)。两个结构体是相等的,要求接口的动态类型(类型指针),和动态值(值指针)都相等。

  1. 接口与nil进行比较
main.go
package main
 
import "fmt"
 
func main() {
	var i interface{} = (*int)(nil) // 把nil转换成*int类型
	fmt.Println(i == nil)
}

输出

false

为什么不是true呢? i的值就是nil呀。接口与nil进行比较,要求接口的动态类型和动态值都是nil,但i的动态类型是*int, 所以不相等。

  1. 接口与接口进行比较
main.go
package main
 
import "fmt"
 
func main() {
	var i1 interface{} = int8(1) // i1的动态类型是int8,动态值是1
	var i2 interface{} = int32(1) // i2的动态类型是int32, 动态值是1
	fmt.Println(i1 == i2)
}

输出

false

i1i2动态类型不相等,所以i1 != i2

只有两个接口的动态类型和动态值都相等,接口才相等。

On this page