[Journey with golang] 4. Interface

接口是一个编程规约,也是一组方法签名的集合。golang的接口是非侵入式的设计,也就是说,一个具体类型实现接口不需要再语法上显式地声明,只要具体类型的方法集是接口方法集的超集,就代表该类型实现了该接口,编译器在编译时会进行方法集的校验。接口是没有具体实现逻辑的,也不能定义字段。

接口变量只有值和类型的概念,所以接口类型变量仍然称为接口变量,接口内部存放的具体类型变量被称为接口指向的“实例”。接口只有声明没有实现,所以定义一个新接口,通常又变成声明一个新接口,二者通用,意思相同。

最常使用的接口字面量类型就是空接口 interface{} ,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括未命名类型的实例。注意:未命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。

golang的接口分为接口字面量类型和接口命名类型,接口声明使用 interface 关键字。接口字面量类型声明语法如下所示:

1 interface{
2     MethodSignature_1
3     MethodSignature_2
4 }

接口命名类型使用 type 关键字声明,语法如下所示:

1 type InterfaceName interface{
2     MethodSignature_1
3     MethodSignature_2
4 }

使用接口字面量的场景很少,一般只有空接口interface{}类型变量的声明才会使用。

接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混淆。例如:

 1 // Reader ...
 2 type Reader interface {
 3     Read(p []byte) (n int, err error)
 4 }
 5 
 6 // Writer ...
 7 type Writer interface {
 8     Write(p []byte) (n int, err error)
 9 }
10 
11 type readAndWrite interface {
12     Reader
13     Writer
14 }
15 
16 type readAndWrite2 interface {
17     Read(p []byte) (n int, err error)
18     Writer
19 }

golang的函数没有“函数声明”,类型的方法本质上就是函数的一种特殊形式,但golang有“方法声明”,而不是使用“方法签名”。严格意义上的函数签名是函数的字面量类型,函数签名是不包括函数名的,而函数声明是指带上函数名的函数签名。同理,接口定义使用方法声明,而不是方法签名。可以说,方法声明=方法名+方法签名。golang编译器在做接口匹配判断时是严格校验方法名和方法签名的。

声明新接口类型的特点:

  1. 接口的命名一般以er结尾
  2. 接口定义的内部方法声明不需要用 func 来引导
  3. 在接口定义中,只有方法声明而没有方法实现

接口只有被初始化为具体的类型时才有意义。接口作为一个胶水层,起到抽象和适配的作用。没有初始化的接口变量,其默认值为nil。接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法:实例赋值接口和接口变量复制接口变量。

如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。

接口变量赋值给接口变量为:已经初始化的接口类型变量a直接复制给另一种接口变量b。这要求b的方法集是a的方法集的子集。此时golang编译器会在编译时进行方法集静态检查,这个过程也是接口初始化的一种方式,此时接口变量b绑定的具体实例是接口变量a绑定的具体实例的副本。

接口方法调用和普通的函数调用有区别。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。直接调用未初始化的接口变量的方法会引起panic,例如:

 1 package main
 2 
 3 type printer interface {
 4     print()
 5 }
 6 
 7 type s struct{}
 8 
 9 func (ss s) print() {
10     println("print successfully")
11 }
12 
13 func main() {
14     var i printer // this is an interface example
15     // the following code will cause a panic
16     // i.print()
17     i = s{}   // an interface example must be inited, or it will cause panic
18     i.print() // call function print via interface example
19     var a s
20     a.print() // call function print via struct example
21 }

接口分为动态类型接口和静态类型接口。接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以接口的动态类型是随着其绑定的不同类型实例而发生变化的。若接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。接口的静态类型在其定义时就被确定,静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以相互赋值。原因是golang编译器校验接口是否能赋值,是比较二者的方法集,而不是看具体类型接口类型名。

接口是一个抽象的类型,像一层胶水,可以灵活地解耦软件的每一个层次。有时我们需要直到已经初始化的接口变量绑定的具体实例是什么类型,以及这个具体实例是否还实现了其他接口,这就要用到接口类型断言和接口类型查询。

我们从前面了解到,已经初始化的接口类型变量a直接复制给另一种接口变量b,要去b的方法集是a的方法集的子集。如果b的方法集不是a的方法集的子集,此时如果直接将a复制给接口变量(即b=a),编译器在静态检查时会报错。此种情况下要想确定接口变量指向的实例是否满足接口变量b,就需要检查运行时的接口类型。编程过程中有时需要确认已经初始化的接口变量指向实例的具体类型是什么,也需要检查运行时的接口类型。

接口类型断言(type assertion)的语法形式为 i.(TypeName) 。i必须为接口变量,如果是具体类型变量,则编译器会报non-interface type xxx on left。TypeName可以为接口类型名,也可以为具体类型名。

接口断言的两层语义:

  1. 如果TypeName是一个具体类型名,则类型断言用于判断接口变量i绑定的实例类型是否就是具体类型TypeName。
  2. 如果TypeName是一个接口类型名,则类型断言用于判断接口变量i绑定的实例类型是否同时实现了TypeName接口。

直接赋值模式为 o := i.(TypeName) 。语义分析如下:

  1. TypeName是具体类型名,此时如果接口i绑定的实例类型就是具体类型TypeName,则变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本(实例可能是指针值,那就是指针值的副本)。
  2. TypeName是接口类型名,如果接口i绑定的实例满足接口类型TypeName,则变量o的类型就是接口类型TypeName,o底层绑定的具体类型实例是i绑定的实例的副本(实例可能是指针值,那就是指针值的副本)。
  3. 若以上两种情况都不满足,程序抛出panic。

示例如下:

 1 package main
 2 
 3 import "fmt"
 4 
 5 // Inter ...
 6 type Inter interface {
 7     Ping()
 8     Pang()
 9 }
10 
11 // Anter ...
12 type Anter interface {
13     Inter
14     String()
15 }
16 
17 // St ...
18 type St struct {
19     Name string
20 }
21 
22 // Ping ...
23 func (St) Ping() {
24     println("ping")
25 }
26 
27 // Pang ...
28 func (*St) Pang() {
29     println("pang")
30 }
31 
32 func main() {
33     st := &St{"andes"}
34     var i interface{} = st
35     // 判断i绑定的实例是否实现了接口类型Inter
36     o := i.(Inter)
37     o.Ping()
38     o.Pang()
39     // 如下语句会引发panic,因为i没有实现接口Anter
40     // p := i.(Anter)
41     // p.String()
42     // 判断i绑定的实例是否就是具体类型St
43     s := i.(*St)
44     fmt.Printf("%s", s.Name)
45 }

comma,ok表达式模型如下:

1 if o, ok := i.(TypeName); ok {
2     // ....
3 }

语义分析:

  1. TypeName是具体类型名,此时如果接口i绑定的实例类型就是具体类型TypeName,则ok为true,变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本(实例可能是指针值,那就是指针值的副本)。
  2. TypeName是接口类型名,此时如果接口i绑定的实例类型满足接口类型TypeName,则ok为true,变量o的类型就是接口类型TypeName,o低层绑定的具体类型实例是i绑定的实例的副本(实例可能是指针值,那就是指针值的副本)。
  3. 如果上述两个条件都不满足,则ok为false,变量o是TypeName类型的“零值”,此种条件分支下程序逻辑不应该再去引用o,因为此时的o没有意义。

示例如下:

 1 package main
 2 
 3 import "fmt"
 4 
 5 // Inter ...
 6 type Inter interface {
 7     Ping()
 8     Pang()
 9 }
10 
11 // Anter ...
12 type Anter interface {
13     Inter
14     String()
15 }
16 
17 // St ...
18 type St struct {
19     Name string
20 }
21 
22 // Ping ...
23 func (St) Ping() {
24     println("ping")
25 }
26 
27 // Pang ...
28 func (*St) Pang() {
29     println("pang")
30 }
31 
32 func main() {
33     st := &St{"andes"}
34     var i interface{} = st
35     // 判断i绑定的实例是否实现了接口类型Inter
36     if o, ok := i.(Inter); ok {
37         o.Ping()
38         o.Pang()
39     }
40 
41     if p, ok := i.(Anter); ok {
42         // 由于i没有实现接口Anter,所以程序不会执行到这里
43         p.String()
44     }
45 
46     // 判断i绑定的实例是否就是具体类型St
47     if s, ok := i.(*St); ok {
48         fmt.Printf("%s", s.Name)
49     }
50 }

接口类型查询的语法格式如下:

1 switch v := i.(type){
2 case type1:
3     xxxx
4 case type2:
5     xxxx
6 default:
7     xxxx
8 }

接口查询有两层语义,一是查询一个接口变量低层绑定的底层变量的具体类型是什么,二是查询接口变量绑定的底层变量是否还实现了其他接口。

  1. i必须是接口类型。
    •   具体类型实例的类型是静态的,在类型声明后就不再变化,所以具体类型的变量不存在类型查询,类型查询一定是对一个接口变量进行操作。如果i是未初始化接口变量,则v的值为nil。例如:
       1 package main
       2 
       3 import (
       4     "fmt"
       5     "io"
       6 )
       7 
       8 func main() {
       9     var i io.Reader
      10     switch v := i.(type) {
      11     case nil:
      12         fmt.Printf("%T
      ", v)
      13     default:
      14         fmt.Printf("default")
      15     }
      16 }
  2. case字句后面可以跟非接口类型名,也可以跟接口类型名,匹配是按照case子句的顺序进行的。
    •   如果case后面是一个接口类型名,且接口变量i绑定的实例类型实现了该接口类型的方法,则匹配成功,v的类型是接口类型,v底层绑定的实例是i绑定具体类型实例的副本。例如:
       1 package main
       2 
       3 import (
       4     "io"
       5     "log"
       6     "os"
       7 )
       8 
       9 func main() {
      10     f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
      11     if err != nil {
      12         log.Fatal(err)
      13     }
      14     defer f.Close()
      15     var i io.Reader = f
      16     switch v := i.(type) {
      17     case io.ReadWriter:
      18         v.Write([]byte("io.ReadWriter
      "))
      19     case *os.File:
      20         v.Write([]byte("*os.File
      "))
      21         v.Sync()
      22     default:
      23         return
      24     }
      25 }
    • 如果case后面是一个具体类型名,且接口变量i绑定的实例类型和该具体类型相同,则匹配成功,此时v就是该具体类型变量,v的值是i绑定的实例值的副本。例如:
       1 package main
       2 
       3 import (
       4     "io"
       5     "log"
       6     "os"
       7 )
       8 
       9 func main() {
      10     f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
      11     if err != nil {
      12         log.Fatal(err)
      13     }
      14     defer f.Close()
      15     var i io.Reader = f
      16     switch v := i.(type) {
      17     case *os.File:
      18         v.Write([]byte("*os.File
      "))
      19         v.Sync()
      20     case io.ReadWriter:
      21         v.Write([]byte("io.ReadWriter
      "))
      22     default:
      23         return
      24     }
      25 }
    • 如果case后面跟着多个类型,使用逗号分隔,接口变量i绑定的实例类型只要和其中一个类型匹配,则直接使用o赋值给v,相当于 v := o 。这个语法有点奇怪,按理说编译器不应该允许这个操作,语言实现者可能想让type switch语句和普通的switch语句保持一样的语法规则,允许发生这种情况。例如:
       1 package main
       2 
       3 import (
       4     "fmt"
       5     "io"
       6     "log"
       7     "os"
       8 )
       9 
      10 func main() {
      11     f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
      12     if err != nil {
      13         log.Fatal(err)
      14     }
      15     defer f.Close()
      16     var i io.Reader = f
      17     switch v := i.(type) {
      18     case *os.File, io.ReadWriter:
      19         if v == i {
      20             fmt.Println(true)
      21         }
      22     default:
      23         return
      24     }
      25 }
    • 如果所有的case字句都不满足,则执行default语句,此时执行的仍然是 v := o ,最终v的值为o。此时使用v没有任何意义。
    • fallthrough语句不能再type switch语句中使用。

注意,golang和很多标准库使用如下格式:

1 switch i := i(type){
2     ....
3 }

这种使用方式存在争议:首先在switch语句块内声明局部变量i覆盖原有的同名变量i不是一种好的编程方式,其次如果类型匹配成功,则i的类型就发生了变化,如果没有匹配成功,则i还是原来的接口类型。除非使用者对这种模糊语义了如指掌,不然很容易出错,所以不建议使用这种方式。推荐的方式是把i.(type)赋值给一个新变量v。

类型查询和类型断言具有相同的语义,只是语法格式不桶。二者都能判断接口变量绑定的实例的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型;类型查询使用case字句以此判断多个类型,类型断言一次只能判断一个类型。

接口的优点有两个:

  1. 解耦:复杂系统进行垂直和水平的分割是常用的设计手段,在层与层之间使用接口进行抽象和解耦是一种良好的编程策略。Go的非侵入式的接口使层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加了接口使用的自由度。
  2. 实现泛型:由于现阶段Go语言还不支持泛型,使用空接口作为函数或方法参数能够用在需要泛型的场景中。

接口类型是“第一公民”,可以用在任何使用变量的地方,使用灵活,方便解耦,主要使用在如下地方:

  1. 作为结构内嵌字段
  2. 作为函数或方法的形参
  3. 作为函数或方法的返回值
  4. 作为其他接口定义的嵌入字段

没有任何方法的接口称为空接口,表示为 interface{} 。系统中任何类型都符合空接口的要求,空接口有点类似于Java语言中的Object。不同之处在于,Go中的基本类型int、float和string也符合空接口。Go的类型系统里面没有类的概念,所有的类型都是一样的身份,没有Java里面对基本类型的开箱和装箱操作,所有的类型都是统一的。Go语言的空接口有点像C语言中的void*,只不过后者是指针,而前者内部封装了指针而已。

Golang没有泛型,如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型,这是弥补没有泛型的一种手段。空接口是反射实现的基础,反射库就是将相关具体类型的类型转换并复制给空接口后才去处理。

空接口不是真的为空,接口有类型和值两个概念。举一个简单的例子:

 1 package main
 2 
 3 import "fmt"
 4 
 5 // Inter ...
 6 type Inter interface {
 7     Ping()
 8     Pang()
 9 }
10 
11 // St ...
12 type St struct{}
13 
14 // Ping ...
15 func (St) Ping() {
16     println("ping")
17 }
18 
19 // Pang ...
20 func (*St) Pang() {
21     println("pang")
22 }
23 
24 func main() {
25     var st *St = nil
26     var it Inter = st
27     fmt.Printf("%p
", st)
28     fmt.Printf("%p
", it)
29     if it != nil {
30         it.Pang()
31         // the next centence will lead a panic
32         // it.Ping()
33     }
34 }

这个程序暴露出golang的一些瑕疵,fmt.Printf("%p ",it)的结果是0x0,但it!=nil的判断结果却是true。空接口有两个字段,一个是实例类型,另一个是指向绑定实例的指针,当只有两个都为nil时,空接口才为nil。

接下来的部分讲解接口的底层实现。接口变量必须初始化才有意义,没有初始化的接口变量默认值为nil,没有任何意义。具体类型实例传递给接口称为接口的实例化。在接口实例化的过程中,编译器通过特定的数据结构表述这个过程。非空接口的底层数据结构是iface,代码位于 src/runtime/runtime2.go 中。

非空接口初始化的过程就是初始化一个iface类型的结构,示例如下:

type iface struct {
    tab *itab
    data unsafe.Pointer
}

可以看到iface结构很简单,有两个指针类型字段。

  • itab:用来存放接口资深类型和绑定的实例类型及实例相关的函数指针。
  • 数据指针data:指向接口绑定的实例的副本,接口的初始化也是一种值拷贝。

data指向具体的示例数据,如果传递给接口的是值类型,则data指向的是实例的副本,如果传递给接口的是指针类型,则data指向指针的副本。总而言之,无论接口的转换,还是函数调用,golang遵循一样的规则——值传递。

itab数据结构是接口内部实现的核心和基础,定义如下:

1 type itab struct {
2     inter *interfacetype
3     _type *_type
4     hash  uint32 // copy of _type.hash. Used for type switches.
5     _     [4]byte
6     fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
7 }

itab有5个字段:

  • inner是指向接口类型元信息的指针。
  • _type是指向接口存放的具体类型元信息的指针,iface里的data指针指向的是该类型的值,一个数类型信息,另一个是类型的值。
  • hash是具体类型的Hash值,_type里面也有hash,这里冗余存放主要是为了接口断言或类型查询时快速访问。
  • fun是一个函数指针,可以理解为C++对象模型里面的虚拟函数指针,这里虽然只有一个元素,实际上指针数组的大学是可变的,编译器负责填充,运行时使用底层指针进行访问,不会受struct类型越界检查的约束,这些指针指向的是具体类型的方法。

itab这个数据结构是非空接口实现动态调用的基础,itab的信息被编译器和链接器保存了下来,存放在可执行文件的只读存储段(.rodata)中。itab存放在静态分配的存储空间中,不受GC的限制,其内存不会被回收。

golang作为一种强类型语言,编译器在编译时会做严格的类型校验,所以golang必然为每种类型维护一个类型的元信息,这个元信息在运行和反射时都会用到,golang的类型元信息的通用结构是_type,其他类型都是以_type为内嵌字段封装而成的结构体。

 1 // Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
 2 // ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
 3 // ../reflect/type.go:/^type.rtype.
 4 type _type struct {
 5     size       uintptr  // 大小
 6     ptrdata    uintptr  // size of memory prefix holding all pointers
 7     hash       uint32   // 类型hash
 8     tflag      tflag    // 类型的特征标记
 9     align      uint8    // _type作为整体变量存放时的对齐字节数
10     fieldalign uint8    // 当前结构字段的对齐字节数
11     kind       uint8    // 基础类型枚举值和反射中的kind一致,kind决定了如何解析该类型
12     alg        *typeAlg // 指向一个函数指针表,该表有两个函数,一个是计算类型hash函数,另一个是比较两个类型是否相同的equal函数
13     // gcdata stores the GC type data for the garbage collector.
14     // If the KindGCProg bit is set in kind, gcdata is a GC program.
15     // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
16     gcdata    *byte   // GC相关信息
17     str       nameOff // str用来表示类型名称字符串在编译后二进制文件中某个section的偏移量
18     ptrToThis typeOff // ptrToThis用来表示类型元信息的指针在编译后二进制文件中某个section的偏移量,由链接器负责填充
19 }

_type包含所有类型的共同元信息,编译器和运行时可以根据该元信息解析具体类型、类型名存放位置、类型的hash值等基本信息。_type里面的nameOff和typeOff最终是由链接器负责确定和填充的,他们都是一个偏移量(offset),类型的名称和类型元信息实际上存放在连接后可执行文件的某个段(section)里,这两个值是相对于段内的偏移量,运行时提供两个转换查找函数,例如:

 1 func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {
 2     if off == 0 {
 3         return name{}
 4     }
 5     base := uintptr(ptrInModule)
 6     for md := &firstmoduledata; md != nil; md = md.next {
 7         if base >= md.types && base < md.etypes {
 8             res := md.types + uintptr(off)
 9             if res > md.etypes {
10                 println("runtime: nameOff", hex(off), "out of range", hex(md.types), "-", hex(md.etypes))
11                 throw("runtime: name offset out of range")
12             }
13             return name{(*byte)(unsafe.Pointer(res))}
14         }
15     }
16 
17     // No module found. see if it is a run time name.
18     reflectOffsLock()
19     res, found := reflectOffs.m[int32(off)]
20     reflectOffsUnlock()
21     if !found {
22         println("runtime: nameOff", hex(off), "base", hex(base), "not in ranges:")
23         for next := &firstmoduledata; next != nil; next = next.next {
24             println("	types", hex(next.types), "etypes", hex(next.etypes))
25         }
26         throw("runtime: name offset base pointer out of range")
27     }
28     return name{(*byte)(res)}
29 }
30 
31 func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {
32     if off == 0 {
33         return nil
34     }
35     base := uintptr(ptrInModule)
36     var md *moduledata
37     for next := &firstmoduledata; next != nil; next = next.next {
38         if base >= next.types && base < next.etypes {
39             md = next
40             break
41         }
42     }
43     if md == nil {
44         reflectOffsLock()
45         res := reflectOffs.m[int32(off)]
46         reflectOffsUnlock()
47         if res == nil {
48             println("runtime: typeOff", hex(off), "base", hex(base), "not in ranges:")
49             for next := &firstmoduledata; next != nil; next = next.next {
50                 println("	types", hex(next.types), "etypes", hex(next.etypes))
51             }
52             throw("runtime: type offset base pointer out of range")
53         }
54         return (*_type)(res)
55     }
56     if t := md.typemap[off]; t != nil {
57         return t
58     }
59     res := md.types + uintptr(off)
60     if res > md.etypes {
61         println("runtime: typeOff", hex(off), "out of range", hex(md.types), "-", hex(md.etypes))
62         throw("runtime: type offset out of range")
63     }
64     return (*_type)(unsafe.Pointer(res))
65 }

golang语言类型元信息最初由编译器负责构建,并以表的形式存放在编译后的对象文件中,再由链接器在链接时进行段合并、符号重定向(填充某些值),这些类型信息在接口的动态调用和反射中被运行时引用。

接口类型元信息数据结构如下:

 1 type imethod struct {
 2     name nameOff  // 方法名在编译后的section里面的偏移量
 3     ityp typeOff  /// 方法类型在编译后的section里面的偏移量
 4 }
 5  
 6 type interfacetype struct {
 7     typ     _type // 类型通用部分
 8     pkgpath name // 接口所属包的名字信息,name内存放的不仅有名称,还有描述信息
 9     mhdr    []imethod // 接口的方法
10 }

接口动态调用有两部分多余消耗,一个是接口实例化的过程,也就是iface结构建立的过程。一旦实例化后,这个接口和具体类型的itab数据结构是可以复用的;另一个是接口的方法调用,它是一个函数指针的间接调用。同事我们应考虑到接口调用是一种动态的计算后跳转调用,这对现代的计算机CPU的执行很不友好,会导致CPU缓存失效和分支预测失效,这也有一部分的性能损失。

空接口由于其没有任何方法集,所以空接口内部不需要维护和动态内存分配相关的数据结构itab。空接口只关心存放的具体类型是什么,具体类型的值是什么,所以空接口的底层数据结构很简单:

1 type eface struct {
2     _type *_type
3     data  unsafe.Pointer
4 }

由此看出,空接口不是真的为空,其保留了具体实例的类型和值拷贝,即便存放的具体类型为空,空接口也不是空的。由于空接口自身没有方法集,所以空接口变量实例化后真正用途不是接口方法的动态调用。空接口在golang中真正意义在于支持多态。有如下几种方式使用了空接口:

  • 通过接口类型断言
  • 通过接口类型查询
  • 通过反射

至此,接口部分结束。

原文地址:https://www.cnblogs.com/JHSeng/p/12179272.html