接口
go 的接口是一种类型,他本身不干活,但可以束缚类型的方法,要求不同类型实现一套在调用方看来完全一样的方法,可以称它们是鸭子类型。
- “鸭子类型”(Duck Typing):“如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。”
这种设计叫多态,虽然底层类型、方法内部的逻辑都不同,但其方法的名称、参数和返回值类型都是统一的。
这样可提升代码的可维护性、复用性:接口由使用者定义,实现由提供者完成。
go
package main
import "fmt"
// 1. 定义接口
type Speaker interface {
Speak() string
}
// 2. 定义结构体(自定义类型)
type Person struct {
Name string
}
type Cat struct {
Name string
}
// 3. 给Person实现Speak方法(完全匹配接口的方法签名)
func (p Person) Speak() string {
return "你好,我是" + p.Name
}
// 4. 给Cat实现Speak方法
func (c Cat) Speak() string {
return "喵~ 我是" + c.Name
}
func main() {
// 接口类型变量,可以接收所有实现了该接口的类型实例
var s Speaker
s = Person{Name: "张三"}
fmt.Println(s.Speak()) // 输出:你好,我是张三
s = Cat{Name: "橘猫"}
fmt.Println(s.Speak()) // 输出:喵~ 我是橘猫
}上面的代码里,Person和Cat都实现了Speaker接口,这就是 Go 的多态:同一接口类型,不同的底层类型,调用同一个方法,执行不同的逻辑。
偷懒/灵活的隐式实现
哪怕函数/方法没有明确声明实现了接口,只要名称、参数、返回值都符合,那就没问题(一个类型只要实现了接口中定义的所有方法,就隐式地实现了该接口)。
好处:
- 虽然只能为每个方法节省显式声明的那一句代码量,但你就说有没有减少吧。
接口值
接口是一个类型,可以被变量初始化,接口值是一个元组,里面的数据是这样存放的:(value, type),前者是存放数据的底层类型的具体值,后者为该值的接口类型(非原本的接口类型)。
go
package main
import (
"fmt"
"math"
)
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}
type F float64
func (f F) M() {
fmt.Println(f)
}
func main() {
var i I
i = &T{"Hello"}
describe(i)
i.M()
i = F(math.Pi)
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}上面的代码中,其实 describe(i I) 才是主角,它规定传入的参数必须实现 I 接口,也就是参数要有个 M() 方法。它不管参数本身是啥类型,它只要参数具备 M() 方法。
实现接口的几个法子总结
- 直接实现接口要求的所有方法,接口的灵活性大多来源于此。
- 通过给接口值赋值(隐式实现,赋值时会触发编译器的接口类型检查)。
以我的简单粗俗的性子,写自己的项目时,大概只会用第一种吧。
类型断言
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
go
t, ok := i.(T)若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生 panic。
请注意这种语法和读取一个映射时的相同之处。
go
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // hello
s, ok := i.(string)
fmt.Println(s, ok) // hello true
f, ok := i.(float64)
fmt.Println(f, ok) // 0 false 有 ok 就不会 panic
f = i.(float64) // panic: interface conversion: interface {} is string, not float64
fmt.Println(f)
}