# 接口

接口(interface)是多个方法声明集合,代表一种调用契约。

只要目标类型方法集包含接口声明的全部方法,就视为实现该接口,无需显示声明。当然,单个目标类型可实现多个接口。在设计上,接口解除了显式类型依赖(DIP,依赖倒置),提供面向对象多态性。应该定义小型、灵活及组合性接口(ISP,接口隔离),减少可视方法,屏蔽对象内部结构和实现细节。

不能有字段。
只能声明方法,不能实现。
可嵌入其他接口。

通常以 er 作为名称后缀。
空接口( interface{} , any )没有任何方法声明。

接口实现的依据是方法集,所以要区分 T.set*T.set

type Xer interface {
	test()
	toString() string
}
type N struct{}
func (*N) test() {}
func (N) toString() string { return "" }
func main() {
	var n N
	// var t Xer = n // ~ N does not implement Xer //  test method has pointer receiver
	var t Xer = &n
	t.test()
	t.toString()
}

匿名接口可直接用于变量定义,或作为结构字段类型。

type N struct{}
func (N) test() {}
type node struct {
	value interface {
		test()
	}
}
func main() {
	var t interface {
		test()
	} = N{}
	n := node{ value: t }
	n.value.test()
}

空接口可被赋值任何对象。

type any = interface{}     // 别名
func main() {
	var i any = 123
	fmt.Println(i)
	i = "abc"
	fmt.Println(i)
}

注意,接口会复制目标对象,通常以指针替代原始值。

type Xer interface {
	toString() string
}
type N struct {
	x int
}
func (n N) toString() string { 
	return strconv.Itoa(n.x)
}
// -------------------------
func main() {
	n := N{ 100 }
	var x Xer = n   // copy
	n.x = 200
	println(n.toString())  // 200
	println(x.toString())  // 100
}

# 匿名嵌入

像匿名字段那样,嵌入其他接口。目标类型方法集中,必须包含嵌入接口在内的全部方法实现。

相当于导入(include)声明,非继承。
不能嵌入自身或循环嵌入。
不允许声明重载(overload)。
允许签名相同的声明(并集去重)。
鼓励小接口嵌入组合。

签名包括方法名、参数列表(数量、类型、排列顺序)和返回值,不包括参数名。

type Aer interface {
	Test()
}
type Ber interface {
	ToString(string) string
}
type Xer interface {
	Aer
	Ber                            // 嵌入接口有相同声明。
	ToString(s string) string      // 签名相同(并集去重)。
	// ToString () string           // 不允许重载(签名不同)。
	// ~~~~~~ duplicate method
	Print()
}
type N struct{}
func (N) ToString(string) string { return "" }
func (N) Test() {}
func (N) Print() {}
func main() {
	i := Xer(N{})
	t := reflect.TypeOf(i)
	for i := 0; i < t.NumMethod(); i++ {
		fmt.Println(t.Method(i))
	}
}
/*
   Print:  func(main.N)
    Test:  func(main.N)
ToString:  func(main.N, string) string
*/

# 类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。

在 Go 语言中类型断言的语法格式如下:

value, ok := x.(T)
func main() {
    var x interface{}
    x = 10
    value, ok := x.(int)
    fmt.Print(value, ",", ok)
}

# 接口类型转换

超集接口(即便非嵌入)可隐式转换为子集,反之不行。

超集包含子集的全部声明(含嵌入)。
和声明顺序无关。

type Aer interface {
	toString(string) string
}
type Xer interface {
	test()
	toString(string) string
}
type N struct{}
func (*N) test() {}
func (N) toString(s string) string { return s }
func main() {
	var x Xer = &N{}     // super
	var a Aer = x        // sub
	a.toString("abc")
	// var x2 Xer = a // ~ Aer does not implement Xer (missing test method)
	// x2 := Xer(a) //  ~ Aer does not implement Xer    
}

类型推断将接口还原为原始类型,或判断是否实现了某个更具体的接口类型。

func main() {
	var x Xer = &N{}     // super
	var a Aer = x        // sub
    // 原始类型。
	n, ok := a.(*N)
	fmt.Println(n, ok)   // true
    // 接口。
	x2, ok := a.(Xer)
	fmt.Println(x2, ok)  // true
}
func main() {
    var e any = (*int)(nil)
	_, ok := e.(*int)
	println(ok)  // true
}

还可用 switch 语句在多种类型间做出推断匹配,如此空接口就有更多发挥空间。

未使用变量视为错误。
不支持 allthrough

func main() {
	var i any = &N{}
	switch v := i.(type) {
	case nil: 
	case *int:
	case func()string:
	case *N: fmt.Println(v)
	case Xer: fmt.Println(v)
	default:
	}
}
func main() {
	var i any = &N{}
	switch v := i.(type) {   // v declared but not used
	case *N: fallthrough     // fallthrough statement out of place
	default:
	}
}

# 接口比较

如实现接口的动态类型支持,可做相等( ==!= )运算。

func main() {
    println(any(nil) == any(nil))
    println(any(100) == any(100))
    // println(any(map[string]int{}) == any(map[string]int{})) // ~ comparing uncomparable type map[string]int
}

允许方式:

接口类型相同。
接口类型不同,但声明完全相同。
接口类型是超集或子集。

type Aer interface {
	test()
}
type Ber interface {
	test()
}
type Cer interface {
	toString() string
}
type Xer interface {
	test()
	toString() string
}
type N struct { x int }
func (N) test() {}
func (N) toString() string { return "" }
func main() {
	a, b := N{ 100 }, N{ 100 }
	println(a == b)               // true
	// 相同类型接口。
	println(Aer(a)  == Aer(b))    // true
	println(Aer(&a) == Aer(&b))   // false
	// 不同接口类型,但声明相同(或超集)。
	println(Aer(a) == Ber(b))     // true
	println(Aer(a) == Xer(b))     // true
	// 不同接口类型,声明不同。
	// println(Aer(a) == Cer(b)) // ~ mismatched types Aer and Cer
	// 对象和它实现的接口类型(b impl Aer)。
	println(Aer(a) == b)          // true
}

接口内部由两个字段组成: itabdata
只有两字段都为 nil 时,接口才等于 nil 。可利用反射完善判断结果。
不能直接以指针判断,因为编译器可能将其指向 runtime.zerobasezeroVal 全局变量。

func main() {
    var n *N
    var x Xer = n      // type != nil
    
    println(x == nil)  // false
    println(x == nil || reflect.ValueOf(x).IsNil()) // true
}
func main() {
	var t1 any                  // type == nil
	var t2 any = ([]int)(nil)   // type != nil
	println(t1 == nil)  // true
	println(t2 == nil)  // false
	println(t1 == nil || reflect.ValueOf(t1).IsNil())  // true
	println(t2 == nil || reflect.ValueOf(t2).IsNil())  // true
}

# 接口实现

接口本身一样是静态类型,内部使用 itab 结构存储运行期所需的相关类型信息。

// runtime/runtime2.go
type iface struct {
	tab  *itab            // 类型和方法表。
	data unsafe.Pointer   // 目标对象指针。
}
type itab struct {
	inter *interfacetype  // 接口类型。  
	_type *_type          // 目标类型。
	hash  uint32
	_     [4]byte
    fun   [1]uintptr      // 方法表。 offset: 24, 0x18
}

内部实现

输出编译结果,查看接口结构存储的具体内容。

type Mer interface {
	A()	
}
type Ner interface {
	Mer
	B(int)
	C(string) string
}
type N int
func  (N) A() {}
func (*N) B(int) {}
func (*N) C(string) string { return "" }
func (*N) D() {}
func main() {
	var n N
	var t Ner = &n
	t.A()
}
$ go build -gcflags "-N -l -S"

"".main STEXT
  LEAQ	go.itab.*"".N,"".Ner(SB), SI

go.itab.*"".N,"".Ner SRODATA
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
	0x0010 57 9a 14 c4 00 00 00 00 00 00 00 00 00 00 00 00
	0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
	rel  0+8 t=1       type."".Ner+0
	rel  8+8 t=1       type.*"".N+0
	rel 24+8 t=-32767  "".(*N).A+0
	rel 32+8 t=-32767  "".(*N).B+0
	rel 40+8 t=-32767  "".(*N).C+0
# 不含接口未声明的 *N.D 

编译器将接口和目标类型组合,生成实例( go.itab.*"".N,"".Ner )。
其中有接口和目标类型引用,并在方法表(24, 0x18)静态填入具体方法地址。

前文提及,接口有个重要特征:赋值给接口时,会复制目标对象。

func main() {
	var n N = 100
	
	var t1 Mer = n   // copy n
	t1.A()
	var t2 Ner = &n  // copy ptr
	t2.B(1)
}
(gdb) info locals

t1 = {
  tab = 0x477d38 <N,main.Mer>,
  data = 0xc000032730            // data = &n_copy
}

t2 = {
  tab = 0x477d78 <N,main.Ner>,
  data = 0xc000032728            // data = &n
}

n = 100

(gdb) p/x &n
$1 = 0xc000032728

(gdb) x/1xg 0xc000032730        ; t1.data。
0xc000032730:   0x0000000000000064

因不可寻址(unaddressable),故无法修改接口内部存储的复制品。

func main() {
	var n N = 100
	// var t1 Mer = n
	// p := &(t1.(N)) // ~ cannot take address
	var t2 Ner = &n    // 接口存储的是指针。
    *(t2.(*N)) = 200   // 透过指针修改目标对象。
	println(n) // 200
}
}

动态调用

以接口调用方法,需通过方法表动态完成。

//go:noinline
func test(n Ner) {
	n.B(9)
}
func main() {
	var n N = 100
	var i Ner = &n
	test(i)
}
# 相比动态调用,内存逃逸才是接口导致的最大性能问题。
$ go build -gcflags "-m"
    moved to heap: n
$ go tool objdump -S -s "main\.main" ./test
func main() {
        var n N = 100
  0x455294    LEAQ 0x5d45(IP), AX
  0x45529b    NOPL 0(AX)(AX*1)
  0x4552a0    CALL runtime.newobject(SB)  ; heap alloc
  0x4552a5    MOVQ $0x64, 0(AX)
  
        test(i)
  0x4552ac    MOVQ AX, BX                 ; .data
  0x4552af    LEAQ go.itab.*main.N,main.Ner(SB), AX
  0x4552b6    CALL main.test(SB)
}
$ go tool objdump -S -s "main\.test" ./test
func test(n Ner) {
        n.B(9)
  0x45523e    MOVQ 0x20(AX), CX  ; .itab + 32 -> B()
  0x455242    MOVQ BX, AX        ; .data
  0x455245    MOVL $0x9, BX      ; argument
  0x45524a    CALL CX            ; B(.data, 0x9)
}

编译器尝试以内联等方式优化,消除(或减少)因动态调用和内存逃逸导致的性能问题。

func test(n Ner) {
    n.B(9)
}
func main() {
    var n N = 100
    var i Ner = &n
    test(i)
}

优化后的代码,完全抹掉了接口的痕迹(内存逃逸和动态调用)。

$ go build -gcflags "-m"
    can inline test
    can inline main
    inlining call to test
$ go tool objdump -S -s "main\.main" ./test
func main() {
        var n N = 100
  0x455234    MOVQ $0x64, 0x10(SP)
        n.B(9)
  0x45523e    LEAQ 0x10(SP), AX
  0x455243    MOVL $0x9, BX
  0x455248    CALL main.(*N).B(SB)
}

# 接口技巧

让编译器检查,确保类型实现了指定接口。

type X int
var _ fmt.Stringer = X(0) // ~ X does not implement fmt.Stringer (missing String method)

定义函数类型,包装函数,使其实现特定接口。

type FuncString func() string
func (f FuncString) String() string {
	return f()
}
func main() {
	f := func() string {
		return "hello, world!"
	}
	var t fmt.Stringer = FuncString(f)
	fmt.Println(t)
}
更新于
-->