接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
go语言使用组合实现对象特性的描述。对象的内部使用结构体内嵌组合表示对象具有的特性,对外通过接口暴露对象能够使用的特性。

为什么要使用接口

计算几何图形的面积和周长

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. )
  6. type Geometry interface {
  7. Area() float64
  8. Prim() float64
  9. }
  10. type Rectangle struct {
  11. width, height float64
  12. }
  13. type Circle struct {
  14. radius float64
  15. }
  16. func (r Rectangle) Area() float64 {
  17. return r.width * r.height
  18. }
  19. func (r Rectangle) Prim() float64 {
  20. return 2 * (r.height + r.width)
  21. }
  22. func (c Circle) Area() float64 {
  23. return c.radius * c.radius * math.Pi
  24. }
  25. func (c Circle) Prim() float64 {
  26. return 2 * c.radius * math.Pi
  27. }
  28. func getArea(g Geometry) {
  29. fmt.Println("Area =", g.Area())
  30. }
  31. func getPrim(g Geometry) {
  32. fmt.Println("Prim =", g.Prim())
  33. }
  34. func main() {
  35. // 声明了一个接口
  36. var rectangle Geometry
  37. // 实现了这个接口中所有方法的结构体可以视为这个接口类型
  38. rectangle = Rectangle{
  39. width: 100,
  40. height: 50,
  41. }
  42. getArea(rectangle)
  43. getPrim(rectangle)
  44. //circle := Circle{
  45. // radius: 100,
  46. //}
  47. var circle Geometry
  48. circle = Circle{
  49. radius: 100,
  50. }
  51. getArea(circle)
  52. getPrim(circle)
  53. }

矩形,圆形都能计算周长和面积,可以将其当成“几何图形”来处理。
Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
正是这种只关心实现的功能(方法),不关心内部属性(结构体)才出现这样的抽象的类型—接口。

定义

每个接口由数个方法组成,格式如下

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

实现接口的条件

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

接口类型变量

那实现了接口可以用在那里
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中, Geometry 类型的变量能够存储 CircleRectangle 类型的变量。

  1. var circle Geometry
  2. circle = Circle{
  3. radius: 100,
  4. }
  5. getArea(circle)
  6. getPrim(circle)

Tips: 观察下面的代码,体味此处_的妙用

  1. // 摘自gin框架routergroup.go
  2. type IRouter interface{ ... }
  3. type RouterGroup struct { ... }
  4. var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter

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

我们有一个Mover接口和一个dog结构体。

  1. type Mover interface {
  2. move()
  3. }
  4. type dog struct {}

值接收者实现接口

  1. func (d dog) move() {
  2. fmt.Println("dog can move...")
  3. }

此时实现接口的是dog类型:

  1. func main() {
  2. var x Mover
  3. var wangcai = dog{} // 旺财是dog类型
  4. x = wangcai // x可以接收dog类型
  5. var fugui = &dog{} // 富贵是*dog类型
  6. x = fugui // x可以接收*dog类型
  7. x.move()
  8. }

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是 结构体dog 还是 结构体指针*dog 类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

同样的, 再来测试一下使用指针接收者有什么区别:

  1. func (d *dog) move() {
  2. fmt.Println("dog can move...")
  3. }
  4. func main() {
  5. var x Mover
  6. var fugui = &dog{} // 富贵是*dog类型
  7. x = fugui // x可以接收*dog类型
  8. var wangcai = dog{} // 旺财是dog类型
  9. x = wangcai // x不可以接收dog类型
  10. }

结构体 dogmove 方法的接收者是指针类型,也就是 Mover 接口可以接收的是指针类型的变量,故不能接收值类型的变量。

类型与接口的关系

一个类型实现多个接口

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

  1. // Sayer 接口
  2. type Sayer interface {
  3. say()
  4. }
  5. // Mover 接口
  6. type Mover interface {
  7. move()
  8. }

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

  1. type dog struct {
  2. name string
  3. }
  4. // 实现Sayer接口
  5. func (d dog) say() {
  6. fmt.Printf("%s会叫汪汪汪\n", d.name)
  7. }
  8. // 实现Mover接口
  9. func (d dog) move() {
  10. fmt.Printf("%s会动\n", d.name)
  11. }
  12. func main() {
  13. var x Sayer
  14. var y Mover
  15. var a = dog{name: "旺财"}
  16. x = a
  17. y = a
  18. x.say()
  19. y.move()
  20. }

多个类型实现同一接口

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

  1. // Mover 接口
  2. type Mover interface {
  3. move()
  4. }

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

  1. type dog struct {
  2. name string
  3. }
  4. type car struct {
  5. brand string
  6. }
  7. // dog类型实现Mover接口
  8. func (d dog) move() {
  9. fmt.Printf("%s会跑\n", d.name)
  10. }
  11. // car类型实现Mover接口
  12. func (c car) move() {
  13. fmt.Printf("%s速度70迈\n", c.brand)
  14. }

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

  1. func main() {
  2. var x Mover
  3. var a = dog{name: "旺财"}
  4. var b = car{brand: "保时捷"}
  5. x = a
  6. x.move()
  7. x = b
  8. x.move()
  9. }

上面的代码执行结果如下:

  1. 旺财会跑
  2. 保时捷速度70

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

  1. package main
  2. import "fmt"
  3. // WashingMachine 洗衣机
  4. type WashingMachine interface {
  5. Wash()
  6. Dry()
  7. }
  8. // 甩干器
  9. type Dryer struct{}
  10. // 实现WashingMachine接口的dry()方法
  11. func (d Dryer) Dry() {
  12. fmt.Println("甩一甩")
  13. }
  14. // 海尔洗衣机
  15. type Haier struct {
  16. Dryer //嵌入甩干器, 也就实现了甩干器的方法
  17. }
  18. // 实现WashingMachine接口的wash()方法
  19. func (h Haier) Wash() {
  20. fmt.Println("洗刷刷")
  21. }
  22. func main() {
  23. var wm WashingMachine := Haier{}
  24. wm = hair
  25. wm.Wash()
  26. wm.Dry()
  27. }

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。故,空接口类型的变量可以存储任意类型的变量。

空接口的应用

空接口作为函数的参数

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

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

空接口作为map的值

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

  1. // 空接口作为map值
  2. // 标准写法
  3. // var studentInfo map[string]interface{} = make(map[string]interface{})
  4. var studentInfo = make(map[string]interface{})
  5. studentInfo["name"] = "沙河娜扎"
  6. studentInfo["age"] = 18
  7. studentInfo["married"] = false
  8. fmt.Println(studentInfo)

类型断言

空接口可以存储任意类型的值。

接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值
我们来看一个具体的例子:

  1. var w io.Writer
  2. w = os.Stdout
  3. w = new(bytes.Buffer)
  4. w = nil

interface.png

断言

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

  1. x.(T)

其中:

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

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

  1. func main() {
  2. var x interface{}
  3. x = "Hello 沙河"
  4. v, ok := x.(string)
  5. if ok {
  6. fmt.Println(v)
  7. } else {
  8. fmt.Println("类型断言失败")
  9. }
  10. }

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

  1. func justifyType(x interface{}) {
  2. switch v := x.(type) {
  3. case string:
  4. fmt.Printf("x is a string,value is %v\n", v)
  5. case int:
  6. fmt.Printf("x is a int is %v\n", v)
  7. case bool:
  8. fmt.Printf("x is a bool is %v\n", v)
  9. default:
  10. fmt.Println("unsupport type!")
  11. }
  12. }

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

注意

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

其他

  1. 关于泛型的讨论
  2. 接口类型与其他数据类型不同,没法被实例化