接口(interface)定义了一个对象的行为规范,只定义规范具体对象来实现规范的细节。Go语言中接口(interface)是一种抽象类型。与C#中接口的定义是一样的,相较于具体类型比如字符串、切片、结构体等(更注重“我是什么”),接口类型更注重“能做什么”。接口类型像是一种约定。Go语言中提倡使用面向接口的编程方式实现解耦。

接口类型#

接口是一种由程序员定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。相较于使用结构体类型,当使用接口类型说明:相比于它是什么更关心它能做什么

接口定义#

每个接口类型由任意个方法签名组成,定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    
}

其中:

  • 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,就像C#中接口定义通常以I 开头,接口名要能突出该接口的类型含义
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略

比如,定义一个包含Write方法的Writer接口

type Writer interface{
    Write([]byte) error
}

当看到一个Writer接口类型的值时,不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事。

实现接口的条件#

接口规定了一个需要实现的方法列表Go 语言中一个类型只要实现了接口中规定的所有方法,就称它实现了这个接口。在C#或者Java语言中都需要显式声明类实现了哪些接口,Go语言中则使用隐式声明的方式实现接口。

值接收者和指针接收者#

之前介绍了在定义结构体方法时既可以使用值接收者也可以使用指针接收者。那么对于实现接口来说使用值接收者和使用指针接收者有什么区别呢?通过一个例子来看一下。

我们定义一个Mover接口,它包含一个Move方法。

type Move interface {
	Move()
}

值接收者实现接口#

定义一个Dog结构体类型,并使用值接收者为其定义一个Move方法

type Dog struct{}

// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
	fmt.Println("狗会动")
}

此时实现Mover接口的是Dog类型。

var x Mover    // 声明一个Mover类型的变量x

var d1 = Dog{} // d1是Dog类型
x = d1         // 可以将d1赋值给变量x
x.Move()

var d2 = &Dog{} // d2是Dog指针类型
x = d2          // 也可以将d2赋值给变量x
x.Move()
狗会动
狗会动

上面的代码中可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。

指针接收者实现接口#

再来测试一下使用指针接收者实现接口有什么区别。

// Cat 猫结构体类型
type Cat struct{}

// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
	fmt.Println("猫会动")
}

此时实现Mover接口的是*Cat类型,可以将*Cat类型的变量直接赋值给Mover接口类型的变量x

	var c2 = &Cat{} // c2是*Cat类型
	x = c2          // 可以将c2当成Mover类型
	x.Move()

但是不能将Cat类型的变量赋值给Mover接口类型的变量x

// 下面的代码无法通过编译
var c1 = Cat{} // c1是Cat类型
x = c1         // 不能将c1当成Mover类型

由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。

类型与接口#

一个类型实现多个接口#

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。可以分别定义Sayer接口和Mover接口,代码示例如下。

// Sayer 接口
type Sayer interface {
	Say()
}

// Mover 接口
type Mover interface {
	Move()
}

Dog既可以实现Sayer接口,也可以实现Mover接口。

type Dog struct {
	name string
}

// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
	fmt.Println("狗会动")
}

// 实现Sayer接口
func (d Dog) Say() {
	fmt.Printf("%s会叫汪汪汪\n", d.name)
}

同一个类型实现不同的接口互相不影响使用。

var d = Dog{Name: "旺财"}

var s Sayer = d
var m Mover = d

s.Say()  // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法

多种类型实现同一接口#

Go语言中不同的类型还可以实现同一接口。例如不仅狗可以动,汽车也可以动。可以使用如下代码体现:

// 实现Mover接口
func (d Dog) Move() {
	fmt.Printf("%s会动\n", d.Name)
}

// Car 汽车结构体类型
type Car struct {
	Brand string
}

// Move Car类型实现Mover接口
func (c Car) Move() {
	fmt.Printf("%s速度70迈\n", c.Brand)
}

这样在代码中就可以把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用Move方法即可。

	var x Mover

	x = Dog{name: "旺财"}
	x.Move()

	x = Car{brand: "宝马"}
	x.Move()

一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

//  洗衣机
type WashingMachine interface {
    // 清洗
	wash()
    // 烘干
	dry()
}

// 海尔洗衣机
type haier struct {
	dryer // 嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
	fmt.Println("洗刷刷")
}

// 甩干机
type dryer struct{}

func (d dryer) dry() {
	fmt.Println("甩一甩")
}

接口组合#

接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例。

// src/io/io.go

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
	Reader
	Writer
}

// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
	Reader
	Closer
}

// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
	Writer
	Closer
}

对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。

接口也可以作为结构体的一个字段,看一段Go标准库sort源码中的示例。

// src/sort/sort.go

// Interface 定义通过索引对元素排序的接口类型
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}


// reverse 结构体中嵌入了Interface接口
type reverse struct {
    Interface
}

通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以改写该接口的方法。

// Less 为reverse类型添加Less方法,重写原Interface接口类型的Less方法
func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

Interface类型原本的Less方法签名为Less(i, j int) bool,此处重写为r.Interface.Less(j, i),即通过将索引参数交换位置实现反转。

在这个示例中还有一个需要注意的地方是reverse结构体本身是不可导出的(结构体类型名称首字母小写),sort.go中通过定义一个可导出的Reverse函数来让使用者创建reverse结构体实例。

func Reverse(data Interface) Interface {
	return &reverse{data}
}

这样做的目的是保证得到的reverse结构体中的Interface属性一定不为nil,否者r.Interface.Less(j, i)就会出现空指针panic。

此外在Go内置标准库database/sql中也有很多类似的结构体内嵌接口类型的使用示例。

空接口#

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为这个特性,空接口类型的变量可以存储任意类型的值。

// Any 不包含任何方法的空接口类型
type Any interface{}

type Person struct{}

// 空接口类型的变量可以存储任意类型的值
func emptyInterfaceTest() {
	// 声明空接口类型变量
	var x Any

	x = "你好" // 字符串型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = 100 // int型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = true // 布尔型
	fmt.Printf("type:%T value:%v\n", x, x)
	x = Person{} // 结构体类型
	fmt.Printf("type:%T value:%v\n", x, x)
}

通常在使用空接口类型时不必使用type关键字声明,可以像下面的代码一样直接使用interface{}

var x interface{}  // 声明一个空接口类型变量x

空接口作为函数参数#

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func print(a interface{}) {
	fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值#

使用空接口实现可以保存任意值的字典。

func emptyInterfaceTest4() {
	var person = make(map[string]interface{})
	person["name"] = "wangpengliang"
	person["age"] = 18
	person["married"] = false
	fmt.Println(person)
}

接口值#

由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,称之为接口的动态类型动态值

通过一个示例来加深对接口值的理解,示例代码中,定义了一个Mover接口类型和两个实现了该接口的DogCar结构体类型。

type Mover interface {
	Move()
}

type Dog struct {
	Name string
}

func (d *Dog) Move() {
	fmt.Println("狗在跑~")
}

type Car struct {
	Brand string
}

func (c *Car) Move() {
	fmt.Println("汽车在跑~")
}

首先创建一个Mover接口类型的变量m

var m Mover

此时,接口变量m是接口类型的零值,也就是它的类型和值部分都是nil,就如下图所示。

可以使用m == nil来判断此时的接口值是否为空。

fmt.Println(m == nil)  // true

注意:不能对一个空接口值调用任何方法,否则会产生panic。

m.Move() // panic: runtime error: invalid memory address or nil pointer dereference

接下来将一个*Dog结构体指针赋值给变量m,此时m的动态类型会被设置为*Dog,动态值为结构体变量的拷贝。

m = &Dog{Name: "旺财"}

然后给接口变量m赋值为一个*Car类型的值。此时接口值的动态类型为*Car,动态值为nil

m = new(Car)

注意:此时接口变量mnil并不相等,因为它只是动态值的部分为nil,而动态类型部分保存着对应值的类型。

fmt.Println(m == nil) // false

接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。

var (
	x Mover2 = new(Dog2)
	y Mover2 = new(Car2)
)
fmt.Println(x == y) // false

有一种特殊情况需要特别注意,如果接口值的保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发 panic

var z interface{} = []int{1, 2, 3}
fmt.Println(z == z) // panic: runtime error: comparing uncomparable type []int

类型断言#

接口值可能赋值为任意类型的值,如何从接口值获取其存储的具体数据呢?可以借助标准库 fmt 包的格式化打印获取到接口值的动态类型。

var m Mover

m = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dog

m = new(Car)
fmt.Printf("%T\n", m) // *main.Car

fmt包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。而想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。

x.(T)

其中:

  • x:表示接口类型的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

比如:

var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
	fmt.Println("类型断言成功")
	v.Name = "富贵" // 变量v是*Dog类型
} else {
	fmt.Println("类型断言失败")
}

如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现。

// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}

由于接口类型变量能够动态存储不同类型值的特点,所以很多人会滥用接口类型(特别是空接口)来实现编码过程中的便捷。只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。

在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。切记:接口是一种类型,一种抽象的类型。它是一个只要求实现特定方法的抽象类型。

小技巧: 下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型。

// 摘自gin框架routergroup.go
type IRouter interface{ ... }

type RouterGroup struct { ... }

var _ IRouter = &RouterGroup{}  // 确保RouterGroup实现了接口IRouter

上面的代码中也可以使用 var _ IRouter = (*RouterGroup)(nil) 进行验证。