9. 接口

上一章介绍了 Go 语言中的结构体的概念,如果说结构体是 Go 语言中数据的载体,那么接口(Interface)就是行为的抽象。接口是 Go 面向对象编程中实现多态的核心机制,也是构建松耦合、可测试、可扩展系统的基石。与其他面向对象语言不同,Go 的接口采用隐式实现,这让代码更加灵活和解耦。

9.1. 接口的概念

在 Go 中,本质上接口是一组方法的声明,并不包含方法的实现,也不包含任何数据字段。当一个类型(如结构体)实现了接口中定义的所有方法时,我们就说该类型实现了这个接口,这就是上面所说的在 Go 语言中的接口采用隐式实现。

9.1.1. 接口的定义与声明

在 Go 语言中,接口的定义语法如下:

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

如我们有如下的结构体:

type Speaker interface {
    Speak() string   // 方法签名
}

接口名和方法的首字母的大小写规则同样遵循对外可见性。

9.1.2. 隐式实现

定义了接口后,在 Go 语言中无需显式声明 implements 来实现该接口,取而代之的是只要类型的方法集中包含接口所需的所有方法,它就自动、隐式地满足该接口。如上一章中的结构体 Person,我们对其进行修改,如下代码:

type Person struct {
    Name string
    Age  int
    Gender string
}

// 实现接口中的方法 Speak
func (person Person) Speak() string {
	var build strings.Builder
	build.WriteString("name: ")
	build.WriteString(person.Name)
	build.WriteString(", age: ")
	build.WriteString(strconv.Itoa(person.Age))
	build.WriteString(", gender: ")
	build.WriteString(person.Gender)
	return build.String()
}

这样结构体 Person 便实现了接口 Speaker。

9.2. 值接收者 vs 指针接收者

在使用接口时,同样会遇到与结构体中一样的方法是值接收者还是指针接收者。

// 值接收者
func (person Person) Speak() string {
	var build strings.Builder
	build.WriteString("name: ")
	build.WriteString(person.Name)
	build.WriteString(", age: ")
	build.WriteString(strconv.Itoa(person.Age))
	build.WriteString(", gender: ")
	build.WriteString(person.Gender)
	return build.String()
}

// 指针接收者
func (person *Person) Speak() string {
	var build strings.Builder
	build.WriteString("name: ")
	build.WriteString(person.Name)
	build.WriteString(", age: ")
	build.WriteString(strconv.Itoa(person.Age))
	build.WriteString(", gender: ")
	build.WriteString(person.Gender)
	return build.String()
}

9.2.1. 值接收者

现在给出值接收者的代码,如下所示:

type Speaker interface {
    Speak() string   // 方法签名
}

type Person struct {
    Name string
    Age  int
    Gender string
}

// 实现接口中的方法 Speak
func (person Person) Speak() string {
	var build strings.Builder
	build.WriteString("name: ")
	build.WriteString(person.Name)
	build.WriteString(", age: ")
	build.WriteString(strconv.Itoa(person.Age))
	build.WriteString(", gender: ")
	build.WriteString(person.Gender)
	return build.String()
}

上述代码中 Speak() 函数是值接收者,现在我们将 Person 实例赋值给 Speaker 接口类型的变量,如下:

func main() {
	var p1 Speaker = Person{Name: "张三", Age: 30, Gender: "男"}
	fmt.Println(p1.Speak())

	var p2 Speaker = &Person{Name: "张三", Age: 30, Gender: "男"}
	fmt.Println(p2.Speak())
}

我们会发现,这两者赋值方式都能正常编译,我们现在用下面的方式打印下变量 p1 和变量 p2 的类型:

fmt.Println(reflect.TypeOf(p1)) // main.Person
fmt.Println(reflect.TypeOf(p2)) // *main.Person

那么方法集中并没有实现指针接收者的方法,为什么也能正常运行呢?其实这是 Go 语言中的规则:如果一个类型的方法集使用的是值接收者,那么该类型的值和指针都自动实现了该接口。

9.2.2. 指针接收者

再来看指针接收者,代码如下:

type Speaker interface {
    Speak() string   // 方法签名
}

type Person struct {
    Name string
    Age  int
    Gender string
}

// 实现接口中的方法 Speak
func (person *Person) Speak() string {
	var build strings.Builder
	build.WriteString("name: ")
	build.WriteString(person.Name)
	build.WriteString(", age: ")
	build.WriteString(strconv.Itoa(person.Age))
	build.WriteString(", gender: ")
	build.WriteString(person.Gender)
	return build.String()
}

上述代码中 Speak() 函数是指针接收者,这个时候我们再将 Person 实例赋值给 Speaker 接口类型的变量,如下:

func main() {
	var p1 Speaker = &Person{Name: "张三", Age: 30, Gender: "男"}
	fmt.Println(reflect.TypeOf(p1)) // *main.Person
	fmt.Println(p1.Speak())
}

当使用指针类型时,能狗正常运行,再使用值类型,代码如下:

func main() {
	var p1 Speaker = Person{Name: "张三", Age: 30, Gender: "男"}
	fmt.Println(reflect.TypeOf(p1)) // *main.Person
	fmt.Println(p1.Speak())
}

我们发现程序在编译时出现错误:cannot use Person{…} (value of struct type Person) as Speaker value in variable declaration: Person does not implement Speaker (method Speak has pointer receiver)。这表明代码中没有实现值接收者的 Speaker 函数。在 Go 语言中规定:如果一个类型的方法集使用的是指针接收者,那么只有该类型的指针实现了该接口。

综上,在 Go 语言中:

  • 如果一个类型的方法集使用的是值接收者,那么该类型的值和指针都自动实现了该接口。
  • 如果一个类型的方法集使用的是指针接收者,那么只有该类型的指针实现了该接口。

9.3. 空接口与 any

如果在接口中不定义任何的方法,即 interface{},我们称之为空接口。因为其中没有任何方法,这就意味着任何类型都实现了空接口。空接口的作用是可以表示任何的类型,如下代码:

func main() {
	// 定义 a 为任意类型
	var a interface{}
	a = "hello world"
	a = 1
	fmt.Println(a)
}

在 Go 1.18 引入泛型后,any 成为了 interface{} 的别名,二者完全等价,如下代码:

func main() {
	// 定义 a 为任意类型
	var a any
	a = "hello world"
	a = 1
	fmt.Println(a)
}

9.4. 接口嵌入

与结构体组合类似,接口也支持嵌入其他接口,从而创建更复杂的接口。如下代码:

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

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

// 接口嵌入:组合 Reader 和 Writer
type ReadWriter interface {
    Reader
    Writer
}

任何实现了 ReadWrite 方法的类型,自动满足 ReadWriter

9.5. 类型断言与类型选择

前面介绍到接口也是一种类型,当把一个具体的值赋值给接口后,有时需要判断其原始类型,这个时候就可以使用类型断言或者类型选择。

9.5.1. 类型断言

类型断言用于判断一个接口变量中实际保存的值是否为某种具体类型,其语法为:value, ok:=i.(T),其中, i 是接口变量,T 是具体类型。如果接口变量 i 确实是类型 T,则返回具体值和 true;否则返回类型 T 的零值和 false。具体代码如下:

func main() {
	// 声明 p 变量
	var p Speaker
	// 初始化
	p = Person{Name: "张三", Age: 30, Gender: "男"}

	person, ok := p.(Person)
	if ok {
		fmt.Println("Person")
		fmt.Println(person.Speak())
	} else {
		fmt.Println("Other")
	}
}

9.5.2. 类型选择

当需要区分多种可能类型时,使用 switch 配合 . (type),具体代码如下:

func Describe(i interface{}) {
    switch v := i.(type) {
        case Person:
        fmt.Println("Person:", v.Name)
        case string:
        fmt.Println("String:", v)
        case int:
        fmt.Println("Int:", v)
        case nil:
        fmt.Println("它是 nil")
        default:
        fmt.Println("未知类型")
    }
}

9.6. 本章小结

在 Go 语言中,接口是一组方法的集合,当任何一个类型(如结构体)实现了接口中定义的所有方法时,就说该类型实现了此接口,相比其他语言来说,无需使用使用 implements 关键字,是一种隐式实现方式。掌握接口,才能更好理解面向对象中的多态。