Interface
接口
接口是一系列规范方法实现的标准,保存着每个需要实现方法的定义规则。
interface也是自定义类型:
type A interface {
funcA (a, b string) string
funcB () int
}
通过接口可创建鸭子类型(统一每个对象的类型,做到统一实现与方法的调用)。
空接口
空接口表示接口可以接收任意的数据类型,any的本质就是一个空接口。
type any = interface {}
泛型约束
接口可以描述泛型的类型约束:
package main
type PlusType interface {
// ~ 表示包含衍生类型
~int | string | float32 | float64 | uint8
}
// int的衍生类型(自定义类型)
type MyInt int
func plus[T PlusType](a, b T) T {
return a + b
}
func main() {
a1 := 1
b1 := 2
plus(a1, b1)
var a2 MyInt = 1
var b2 MyInt = 2
plus(a2, b2)
}
鸭子类型
Go语言中没有类似implements类似的机制,需要通过函数接收器的方式绑定方法。在真正使用时才会检查是否完全实现了interface中的接口规范。
package main
// 规范接口
type Duck interface {
walk()
shout()
}
type Bird struct {
legs int
}
type Dog struct {
legs int
}
// Bird绑定 walk 方法
func (bd *Bird) walk() {
fmt.Println("Bird is walking.")
}
// Bird绑定 shout 方法
func (bd *Bird) shout() {
fmt.Println("Bird is shouting.")
}
// Dog绑定 walk 方法
func (bd *Dog) walk() {
fmt.Println("Dog is walking.")
}
// Dog绑定 shout 方法
func (bd *Dog) shout() {
fmt.Println("Dog is shouting.")
}
func doSth(animal Duck) {
animal.walk()
animal.shout()
}
func main() {
doSth(&Bird{legs: 3})
doSth(&Dog{legs: 3})
}
类型断言
将一个不明确的类型转换成一个明确的类型,语法为接口类型的变量.(断言类型)。
断言主要是针对any或interface{},interface不支持类型转换:
package main
import "fmt"
func main() {
var a interface{} = 1
// invalid operation: a += 1 (mismatched types interface{} and int)
// a += 1
a = a.(int) + 1
fmt.Println(a)
}
类型断言也可用于struct、map、自定义等类型。
模拟泛型
Go 1.18版本之前不支持泛型,可借助类型断言来模拟泛型:
package main
import "fmt"
func plus(a, b interface{}) interface{} {
switch a.(type) {
case int:
_a, _ := a.(int)
_b, _ := b.(int)
return _a + _b
case float64:
_a, _ := a.(float64)
_b, _ := b.(float64)
return _a + _b
case string:
_a, _ := a.(string)
_b, _ := b.(string)
return _a + _b
default:
panic("类型不支持")
}
}
func main() {
res1 := plus(1, 2)
res2 := plus(1.1, 2.2)
res3 := plus("a", "b")
fmt.Println(res1, res2, res3)
}
接口继承
Go 语言中,可通过接口嵌套的方式实现接口的继承:
type Base struct{ A int }
func (b *Base) Foo(){}
type Child struct{ Base }
编译器会把 Base.Foo 提升到 Child 的方法集,所以 c.Foo() 合法。
方法集规则
编译器在做 T 是否实现某接口的检查时,只看方法签名能否匹配到对应的方法集。
Go 规范对类型 T 与指针类型 *T 的方法集做了严格区分。
| 声明的接收者 | 属于 T 的方法集 | 属于 *T 的方法集 |
|---|---|---|
| func (v T) F() | ✅ 包含 F | ✅ 包含 F(解引用调用) |
| func (p *T) G() | ❌ 不包含 G | ✅ 包含 G |
type I interface { F() }
type J interface { F(); G() }
type T struct{}
func (T) F() {} // 值接收者
func (*T) G() {} // 指针接收者
var _ I = T{} // ok:T 方法集中有 F
var _ I = &T{} // ok:*T 方法集中也有 F(值方法自动提升)
// var _ J = T{} // 编译错误:T 方法集没 G
var _ J = &T{} // ok:*T 既有 F 又有 G
值 T 接收者的方法会出现在 *T 的方法集?
- 如果有一个
*T,肯定能解引用成T值再调用值接收者方法。 - 对编译器来说调用无障碍,就把方法放进
*T的方法集,免得开发者必须手动写。 - 如果值接收者方法不进入
*T方法集,每次用指针调用值接收者方法都要显示解引用,非常繁琐。 - 方便接口满足性:给类型
T写了一组只读方法后,无论拿T还是*T都能实现相同的接口。
指针 *T 接收者方法为什么不进 T 的方法集?
- 若
T不是可寻址的,就没法为它隐式取址去调用,自动取址并非在所有场合都做得到。 - 如果编译器仍强塞进方法集,会让不少代码貌似合法却运行时崩溃。
- 规范为了安全,选择一刀切,只有肯定能调用到的方法才加入对应方法集。
- 从源头避免潜在未定义行为和逃逸混乱。