Go语言接口

323 阅读7分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」。

Go语言接口 ::: danger 接口说明 Go语言中虽然没有传统面向对象语言中类、集成的概念,不过提供了接口的支持,可以使用接口来使用一些面向对象的特性。 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。 在 go 语言中的接口有下面几个特点:

  • 可以包含0个或多个方法的签名
  • 只定义方法的签名,不包含实现
  • 实现接口不需要显式的声明,只需实现相应方法即可 :::

1 接口声明的格式

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

::: tip 对各个部分的说明

  • 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
  • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如: :::
type writer interface{
    Write([]byte) error
}

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

2 实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

2.1 定义一个Speaker接口

// Speaker 接口
type Speaker interface {
	sayHello()
}

2.2 定义实现Speaker的结构体

//定义接口的实现类
type Chinese struct{}
//美国人
type Americans struct{}

//实现接口方法
func (c Chinese) sayHello() {
  fmt.Println("中国人说你好")
}

func (a Americans) sayHello()  {
  fmt.Println("美国人说Hello")
}

3.3 调用接口的方法

那实现了接口有什么用呢? 接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Speaker类型的变量能够存储Americans和Chinese类型的变量

func main() {
	var x Speaker     // 声明一个Speaker类型的变量x
	a := Americans{}  // 实例化一个Americans对象
	c := Chinese{}    // 实例化一个Chinese对象
	x = a             // 可以把Americans实例直接赋值给x
	x.sayHello()      // 美国人说Hello
	x = c             // 可以把Chinese实例直接赋值给x
	x.sayHello()      // 中国人说你好
}

3 空接口

在Go语言中,空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以存储任意类型的变量。定义如下:

interface{}

3.1 空接口举例

package main

import "fmt"

//定义一个全局空接口v3
var v3 interface{} = struct{ X int }{1}
func main() {
	fmt.Printf("接口v3的类型是:%T 值:%v\n", v3, v3)//接口v3的类型是:struct { X int } 值:{1}
	// 定义一个空接口x
	var x interface{}
	s := "Hello World"
	x = s
	fmt.Printf("接口x的类型是:%T 值:%v\n", x, x)//接口x的类型是:string 值:Hello World
	i := 100
	x = i
	fmt.Printf("接口x的类型是:%T 值:%v\n", x, x)//接口x的类型是:int 值:100
	b := true
	x = b
	fmt.Printf("接口x的类型是:%T 值:%v\n", x, x)//接口x的类型是:bool 值:true

如果函数打算接收任何数据类型,则可以将参考声明为interface{}。最典型的例子就是标准库fmt包中的Print和Fprint系列的函数:

func Fprint(w io.Writer, a ...interface{}) (n int, err error)

func Fprintf(w io.Writer, format string, a ...interface{})

func Fprintln(w io.Writer, a ...interface{})

func Print(a ...interface{}) (n int, err error)

func Printf(format string, a ...interface{})

func Println(a ...interface{}) (n int, err error)

3.2 空接口的应用

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

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

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

// 空接口作为map值
	var info = make(map[string]interface{})
	info["name"] = "福小林"
	info["age"] =30
	fmt.Println(info) //map[age:30 name:福小林]

4 接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

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

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

// 接口嵌套
type Animal interface {
	Sayer
	Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现Animal接口:

type cat struct {
	name string
}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("大花猫会跳")
}

func main() {
	var x Animal
	x = cat{name: "哆啦A梦"}
	x.move()
	x.say()
}

5 类型与接口的关系

一个类型可以实现多个接口

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

 一个类型可以实现多个接口

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

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

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

type dog struct {
	name string
}

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

// 实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会动\n", d.name)
}

func main() {
	var x Sayer
	var y Mover

	var a = dog{name: "旺财"}
	x = a
	y = a
	x.say()
	y.move()
}

多个类型实现同一接口

Go语言中不同的类型可以实现同一接口,首先我们定义一个Mover接口,它要求必须由一个move方法。

 多个类型实现同一接口

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

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
	name string
}

type car struct {
	brand string
}

// dog类型实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
	fmt.Printf("%s速度特别快\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了

func main() {
	var x Mover
	var a = dog{name: "旺财"}
	var b = car{brand: "保时捷"}
	x = a
	x.move()
	x = b
	x.move()
}

6 类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。

::: tip 语法格式

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。 :::

6.1 接口值

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

举例如下

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

 接口值

6.2 类型断言举例

package main
import (
    "fmt"
)
func main() {
    var x interface{}
    x = "Hello"
    value, ok := x.(string)
    fmt.Print(value, ",", ok) //Hello,true
}

需要注意如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic。

6.3 类型断言switch使用

package main

import "fmt"

//判断数据类型
func judgeDataType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x 是一个字符串类型,值是 %v\n", v)
	case int:
		fmt.Printf("x是一个整数类型,值是 %v\n", v)
	case bool:
		fmt.Printf("x是一个布尔类型 ,值是 %v\n", v)
	default:
		fmt.Println("没有找到对应的类型……")
	}
}
func main() {
	judgeDataType("Hello") //x 是一个字符串类型,值是 Hello
	judgeDataType(100) //x是一个整数类型,值是 100
}

7 接口总结

::: tip 接口总结

  • interface类型默认是一个指针
  • 使用空接口可以保存任意值
  • 不能比较空接口中的动态值
  • 定义了一个接口,这些方法必须都被实现,这样编译并使用 :::