接口

Golang 世界中,有一种叫 interface 的东西,很是神奇。

一、数据类型 interface{}

如果你事前并不知道变量是哪种数据类型,不知道它是整数还是字符串,但是你还是想要使用它。

Golang 就产生了名为 interface{} 的数据类型,表示并不知道它是什么类型。举例子:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func print(i interface{}) {
  7. fmt.Println(i)
  8. }
  9. func main() {
  10. // 声明一个未知类型的 a,表明不知道是什么类型
  11. var a interface{}
  12. a = 2
  13. fmt.Printf("%T,%v\n", a, a)
  14. // 传入函数
  15. print(a)
  16. print(3)
  17. print("i love you")
  18. // 使用断言,判断是否是 int 数据类型
  19. v, ok := a.(int)
  20. if ok {
  21. fmt.Printf("a is int type,value is %d\n", v)
  22. }
  23. // 使用断言,判断变量类型
  24. switch a.(type) {
  25. case int:
  26. fmt.Println("a is type int")
  27. case string:
  28. fmt.Println("a is type string")
  29. default:
  30. fmt.Println("a not type found type")
  31. }
  32. // 使用反射找出变量类型
  33. t := reflect.TypeOf(a)
  34. fmt.Printf("a is type: %s", t.Name())
  35. }

输出:

  1. int,2
  2. 2
  3. 3
  4. i love you
  5. a is int type,value is 2
  6. a is type int
  7. a is type: int

1.1.基本使用

我们使用 interface{},可以声明一个未知类型的变量 a

  1. // 声明一个未知类型的 a,表明不知道是什么类型
  2. var a interface{}
  3. a = 2
  4. fmt.Printf("%T,%v\n", a, a)

然后给变量赋值一个整数:a=2,这时 a 仍然是未知类型,使用占位符 %T 可以打印变量的真实类型,占位符 %v 打印值,这时 fmt.Printf 在内部会进行类型判断。

我们也可以将函数的参数也定为 interface,和变量的定义一样:

  1. func print(i interface{}) {
  2. fmt.Println(i)
  3. }

使用时:

  1. // 传入函数
  2. print(a)
  3. print(3)
  4. print("i love you")

我们传入 print 函数的参数可以是任何类型,如整数 3 或字符串 i love you 等。进入函数后,函数内变量 i 丢失了类型,是一个未知类型,这种特征使得我们如果想处理不同类型的数据,不需要写多个函数。

当然,结构体里面的字段也可以是 interface{}

  1. type H struct {
  2. A interface{}
  3. B interface{}
  4. }

1.2.判断具体类型

我们定义了 interface{},但是实际使用时,我们有判断类型的需求。有两种方法可以进行判断。

使用断言:

  1. // 使用断言,判断是否是 int 数据类型
  2. v, ok := a.(int)
  3. if ok {
  4. fmt.Printf("a is int type,value is %d\n", v)
  5. }

直接在变量后面使用 .(int),有两个返回值 v, ok 会返回。ok 如果是 true 表明确实是整数类型,这个整数会被赋予 v,然后我们可以拿 v 愉快地玩耍了。否则,okfalsev 为空值,也就是默认值 0。

如果我们每次都这样使用,会很难受,因为一个 interface{} 类型的变量,数据类型可能是 .(int),可能是 .(string),可以使用 switch 来简化:

  1. // 使用断言,判断变量类型
  2. switch a.(type) {
  3. case int:
  4. fmt.Println("a is type int")
  5. case string:
  6. fmt.Println("a is type string")
  7. default:
  8. fmt.Println("a not type found type")
  9. }

swicth 中,断言不再使用 .(具体类型),而是 a.(type)

最后,还有一种方式,使用的是反射包 reflect 来确定数据类型:

  1. // 使用反射找出变量类型
  2. t := reflect.TypeOf(a)
  3. fmt.Printf("a is type: %s", t.Name())

这个包会直接使用非安全指针来获取真实的数据类型:

  1. func TypeOf(i interface{}) Type {
  2. eface := *(*emptyInterface)(unsafe.Pointer(&i))
  3. return toType(eface.typ)
  4. }

一般日常开发,很少使用反射包。

二. 接口结构 interface

我们现在都是函数式编程,或者是结构体方法式的编程,难道没有其他语言那种面向对象,对象继承的特征吗?有,Golang 语言叫做面向接口编程。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. // A 定义一个接口,有一个方法
  7. type A interface {
  8. Println()
  9. }
  10. // B 定义一个接口,有两个方法
  11. type B interface {
  12. Println()
  13. Printf() int
  14. }
  15. // A1Instance 定义一个结构体
  16. type A1Instance struct {
  17. Data string
  18. }
  19. // Println 结构体实现了Println()方法,现在它是一个 A 接口
  20. func (a1 *A1Instance) Println() {
  21. fmt.Println("a1:", a1.Data)
  22. }
  23. // A2Instance 定义一个结构体
  24. type A2Instance struct {
  25. Data string
  26. }
  27. // Println 结构体实现了Println()方法,现在它是一个 A 接口
  28. func (a2 *A2Instance) Println() {
  29. fmt.Println("a2:", a2.Data)
  30. }
  31. // Printf 结构体实现了Printf()方法,现在它是一个 B 接口,它既是 A 又是 B 接口
  32. func (a2 *A2Instance) Printf() int {
  33. fmt.Println("a2:", a2.Data)
  34. return 0
  35. }
  36. func main() {
  37. // 定义一个A接口类型的变量
  38. var a A
  39. // 将具体的结构体赋予该变量
  40. a = &A1Instance{Data: "i love you"}
  41. // 调用接口的方法
  42. a.Println()
  43. // 断言类型
  44. if v, ok := a.(*A1Instance); ok {
  45. fmt.Println(v)
  46. } else {
  47. fmt.Println("not a A1")
  48. }
  49. fmt.Println(reflect.TypeOf(a).String())
  50. // 将具体的结构体赋予该变量
  51. a = &A2Instance{Data: "i love you"}
  52. // 调用接口的方法
  53. a.Println()
  54. // 断言类型
  55. if v, ok := a.(*A1Instance); ok {
  56. fmt.Println(v)
  57. } else {
  58. fmt.Println("not a A1")
  59. }
  60. fmt.Println(reflect.TypeOf(a).String())
  61. // 定义一个B接口类型的变量
  62. var b B
  63. //b = &A1Instance{Data: "i love you"} // 不是 B 类型
  64. b = &A2Instance{Data: "i love you"}
  65. fmt.Println(b.Printf())
  66. }

输出:

  1. a1: i love you
  2. &{i love you}
  3. *main.A1Instance
  4. a2: i love you
  5. not a A1
  6. *main.A2Instance
  7. a2: i love you
  8. 0

我们可以定义一个接口类型,使用 type 接口名 interface,这时候不再是 interface{}

  1. // A 定义一个接口,有一个方法
  2. type A interface {
  3. Println()
  4. }
  5. // B 定义一个接口,有两个方法
  6. type B interface {
  7. Println()
  8. Printf() int
  9. }

可以看到接口 AB 是一种抽象的结构,每个接口都有一些方法在里面,只要结构体 struct 实现了这些方法,那么这些结构体都是这种接口的类型。如:

  1. // A1Instance 定义一个结构体
  2. type A1Instance struct {
  3. Data string
  4. }
  5. // Println 结构体实现了Println()方法,现在它是一个 A 接口
  6. func (a1 *A1Instance) Println() {
  7. fmt.Println("a1:", a1.Data)
  8. }
  9. // A2Instance 定义一个结构体
  10. type A2Instance struct {
  11. Data string
  12. }
  13. // Println 结构体实现了Println()方法,现在它是一个 A 接口
  14. func (a2 *A2Instance) Println() {
  15. fmt.Println("a2:", a2.Data)
  16. }
  17. // Printf 结构体实现了Printf()方法,现在它是一个 B 接口,它既是 A 又是 B 接口
  18. func (a2 *A2Instance) Printf() int {
  19. fmt.Println("a2:", a2.Data)
  20. return 0
  21. }

我们要求结构体必须实现某些方法,所以可以定义一个接口类型的变量,然后将结构体赋值给它:

  1. // 定义一个A接口类型的变量
  2. var a A
  3. // 将具体的结构体赋予该变量
  4. a = &A1Instance{Data: "i love you"}
  5. // 调用接口的方法
  6. a.Println()

我们写成 a = &A1Instance{Data:"i love you"} 而不是 a = A1Instance{Data:"i love you"},是因为只有指针类型的对象实现了该接口,而结构体类型的结构没有实现该接口。

对应上面的例子来说,只有 &A1Instance 实现了 Println 接口,而 A1Instance根本没有实现该接口,写成 a = A1Instance{Data:"i love you"} 将编译不通过,无法编译二进制。

我们也可以使用断言和反射来判断接口类型是属于哪个实际的结构体 struct

  1. // 断言类型
  2. if v, ok := a.(*A1Instance); ok {
  3. fmt.Println(v)
  4. } else {
  5. fmt.Println("not a A1")
  6. }
  7. fmt.Println(reflect.TypeOf(a).String())

Golang 很智能判断结构体是否实现了接口的方法,如果实现了,那么该结构体就是该接口类型。我们灵活的运用接口结构的特征,使用组合的形式就可以开发出更灵活的程序了。

附录

代码下载:

  1. https://github.com/hunterhug/goa.c/blob/master/code/interface/main.go
  2. https://github.com/hunterhug/goa.c/blob/master/code/interface/main2.go