Skip to content

十二、接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

接口是一种类型,其关键字是interface。其定义格式如下:

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

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

比如:

plain
type carer interface{
    run()
}

上面定义了carer类型名的接口,其他对象只要其有如上的run()方法,那么都可以实现这个接口。

实现接口的条件

只要实现接口中的方法,那么就可以实现这个接口。

比如,我们定义一个接口:

plain
type speaker interface{
    speak()
}

然后定义两个结构体,如下:

plain
type cat struct{}
type dog struct{}

要让catdog能够实现接口,只需要让它们分别有speck()方法即可。如下:

plain
func (d dog) speak(){
    fmt.Println("汪汪汪~")
}
func (c cat) speak(){
    fmt.Println("喵喵喵~")
}

接口的使用

就像上面那样定义好了接口,也定义好了对象及其方法,我们该如何使用呢?

接口类型变量能够存储所有实现了该接口的实例,只要这个对象满足接口的条件,那么就可以像下面这样使用:

plain
package main

import "fmt"

// 定义一个接口
type speaker interface {
	speak()
}

// 定义两个结构体
type dog struct {
	name string
}

type cat struct {
	name string
}

// 实现对象的方法
func (d dog) speak() {
	fmt.Printf("名字叫%s的狗在汪汪汪的叫~\n", d.name)
}

func (c cat) speak() {
	fmt.Printf("名字叫%s的猫在汪汪汪的叫~\n", c.name)
}

func main() {
	// 声明类型为interface的变量
	var s speaker

	// 声明类型为dog的结构体
	d := dog{
		name: "小黑",
	}
	c := cat{
		name: "黑猫警长",
	}

	// 使用接口
	s = d	// 把dog的实例直接赋值给s
	s.speak()
	s = c	// 把cat的实例直接赋值给s
	s.speak()
}

值接收者和指针接收者实现接口的区别

我们通过例子来看它们的区别。

值接收者

plain
package main

import "fmt"

// 定义一个接口
type speaker interface {
	speak()
}

// 定义两个结构体
type dog struct {
	name string
}

// 实现对象的方法
func (d dog) speak() {
	fmt.Printf("名字叫%s的狗在汪汪汪的叫~\n", d.name)
}

func main() {
	// 声明类型为interface的变量
	var s speaker

	// 声明类型为dog的结构体
	d1 := dog{
		name: "小黑",
	}

	// 使用接口
	s = d1    // 将值类型的结构体对象传递给s
	s.speak() // 名字叫小黑的狗在汪汪汪的叫~

	// 声明指针类型的dog结构体
	d2 := &dog{
		name: "小白",
	}
	s = d2    // 将指针类型的结构体对象传递给s
	s.speak() // 名字叫小白的狗在汪汪汪的叫~
}

从上面可以看出,如果是值接收者实现的接口,不论结构体类型是值类型还是指针类型,接口变量都可以接受。

指针类型

plain
package main

import "fmt"

// 定义一个接口
type speaker interface {
	speak()
}

// 定义两个结构体
type dog struct {
	name string
}

// 实现对象的方法
func (d *dog) speak() {
	fmt.Printf("名字叫%s的狗在汪汪汪的叫~\n", d.name)
}

func main() {
	// 声明类型为interface的变量
	var s speaker

	// 声明类型为dog的结构体
	d1 := dog{
		name: "小黑",
	}

	// 使用接口
	s = d1    // 将值类型的结构体对象传递给s,报错
	s.speak() //

	// 声明指针类型的dog结构体
	d2 := &dog{
		name: "小白",
	}
	s = d2    // 将指针类型的结构体对象传递给s
	s.speak() // 名字叫小白的狗在汪汪汪的叫~
}

报错:

plain
dog does not implement speaker (speak method has pointer receiver)go

从上可知,如果是指针接收者的接口,只能接受指针类型的对象。

类型与接口的关系

一个类型可以定义多个接口

一个类型可以定义多个接口,接口之间彼此独立,不知道对方实现。

如下:

plain
package main

import "fmt"

type mover interface {
	move()
}

type eater interface {
	eat()
}

type dog struct {
	name string
}

func (d dog) move() {
	fmt.Printf("%s会跑了", d.name)
}

func (d dog) eat() {
	fmt.Printf("%s会吃东西了", d.name)
}

func main() {
	var m mover
	var e eater
	d := dog{
		name: "小黑",
	}
	m = d
	m.move()
	e = d
	e.eat()
}

多个类型实现同一个接口

plain
package main

import "fmt"

// 定义一个接口
type mover interface {
	move()
}

// 定义一个动物的结构体
type animals struct {
	name string
}

// 定义一个汽车的结构体
type cars struct {
	name string
}

// 为动物和汽车添加move方法
func (a animals) move() {
	fmt.Printf("%s会跑了\n", a.name)
}

func (c cars) move() {
	fmt.Printf("%s的速度达到了200马\n", c.name)
}

func main() {
	var m mover
	a := animals{
		name: "小猫",
	}
	m = a
	m.move()
	c := cars{
		name: "奥迪",
	}
	m = c
	m.move()
}

我们上面定义两个不同的类型:汽车动物。他们都实现了接口mover。所以接口其实并不关心你是什么类型,只要你实现了它里面的所有方法就可以实现这个接口。

接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

go
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

图解如下:

接口的嵌套

接口也可以嵌套,嵌套出来是一个新接口,它拥有被嵌套接口的功能,如下:

plain
package main

import "fmt"

type animals interface {
	mover
	eater
}

type mover interface {
	move()
}

type eater interface {
	eat()
}

type dog struct {
	name string
}

func (d dog) move() {
	fmt.Printf("%s会跑了", d.name)
}

func (d dog) eat() {
	fmt.Printf("%s会吃东西了", d.name)
}

func main() {
	var a animals
	d := dog{
		name: "小黑",
	}
	a = d
	a.eat()
	a.move()
}

空接口

空接口是里面没有定义任何方法的接口,也就是说任何类型都可以实现的接口。

其定义格式如下:

plain
interface{}

空接口可以接受任何类型,如下:

plain
package main

import "fmt"

func main() {
	var x interface{}
	a := 100
	x = a
	fmt.Println(x)
	b := "小黑"
	x = b
	fmt.Println(x)
	c := false
	x = c
	fmt.Println(c)
}

空接口的应用

作为函数参数

空接口是可以接受任何类型,所以其作为函数参数是可以接受任意类型的变量,如fmt.Print()函数。

例子:

plain
func f1(a interface{}){
    fmt.Println(a)
}

作为 map 的值

我们之前在定义 map 的时候是需要指定类型值,只要我们指定了类,就代表之后的值都必须是这个类型,但是有时候我们定义一个 map,想让其可以存放任意类型的值,我们就可以用空接口作为它的值。如下:

plain
package main

import "fmt"

func main() {
	var m = make(map[string]interface{})
	m["s1"] = 29
	m["s2"] = "小黑"
	m["s3"] = false
	fmt.Println(m)
}

类型断言

由于空接口可以接受任意类型的值,我们有时候需要判断其传入的值到底是什么类型,这时候就要使用到类型断言。

其语法如下:

plain
x.(T)

其中:

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

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

比如:

go
package main

import "fmt"

func main() {
	var inter interface{}
	a := 100
	inter = a
	v, ok := inter.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型判断失败")
	}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

go
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!")
	}
}

使用接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

最近更新