Skip to main content

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})
}

类型断言

将一个不明确的类型转换成一个明确的类型,语法为接口类型的变量.(断言类型)

断言主要是针对anyinterface{}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)
}

类型断言也可用于structmap、自定义等类型。

模拟泛型

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 的方法集?

  1. 如果有一个 *T,肯定能解引用成 T 值再调用值接收者方法。
  2. 对编译器来说调用无障碍,就把方法放进 *T 的方法集,免得开发者必须手动写。
  3. 如果值接收者方法不进入 *T 方法集,每次用指针调用值接收者方法都要显示解引用,非常繁琐。
  4. 方便接口满足性:给类型 T 写了一组只读方法后,无论拿 T 还是 *T 都能实现相同的接口。

指针 *T 接收者方法为什么不进 T 的方法集?

  1. T 不是可寻址的,就没法为它隐式取址去调用,自动取址并非在所有场合都做得到。
  2. 如果编译器仍强塞进方法集,会让不少代码貌似合法却运行时崩溃。
  3. 规范为了安全,选择一刀切,只有肯定能调用到的方法才加入对应方法集。
  4. 从源头避免潜在未定义行为和逃逸混乱。