深入理解Golang中的interface(一)

原文地址:https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c

要点:

  • 接口是系列方法的集合
  • 单个类型可以实现多个方法
  • 同一个接口可以被多种类型实现
  • 接口声明可内嵌其他接口,导入内嵌接口的所有方法(可导出方法+不可导出方法),多层内嵌接口也将全部被导入到接口声明中
  • 禁止接口的循环内嵌
  • 接口内方法名必须唯一:自定义方法和内嵌接口包含方法,名称必须唯一
  • 接口变量可以保存所有实现了此接口的所有类型的值:抽象的理论实现
  • 静态类型VS动态类型:接口类型的变量可以被实现了其接口的类型间相互赋值,动态类型
  • 接口类型变量:动态类型、动态值,只有二者均为零值nil时此接口类型的变量才为nil
  • 空接口:可以承载任何类型的变量,也可以说任何类型都实现(满足)了空接口
  • 接口实现:类型定义了包含某接口声明的所有方法(方法名+签名一致)
  • 接口类型值只能访问接口自身定义的方法,原类型的其他变量无法访问:行为的抽象,联想对比Java中子类赋值给父类时多态特性

关键词:接口(interface),类型(type),方法(method),函数(function),方法签名(signature),可导出方法(exported method),满足(satisfy),实现(implement),

接口(Interface)使得代码更具灵活性、扩展性,是Golang中多态的实现方式。接口允许指定只有一些行为是需要的,而不需要指定一种特定类型。
行为的定义就是一系列方法的集合,如下代码片段一:

1
2
3
4
5
type I interface {
f1(name string)
f2(name string) (error, float32)
f3() int64
}

并不需要强制的接口实现。我们说类型(type)实现或满足接口(interface),只需要它定义了接口期望的方法名和签名(输入和返回参数),如下代码段二:

1
2
3
4
5
6
7
8
9
10
type T int64
func (T) f1(name string) {
fmt.Println(name)
}
func (T) f2(name string) (error, float32) {
return nil, 10.2
}
func (T) f3() int64 {
return 10
}

这个例子中类型T满足代码段一中定义的接口I,类型T的值可以作为参数传递给任何将接口I作为接收参数的方法。
如下代码段三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type I interface {
M() string
}
type T struct {
name string
}
func (t T) M() string {
return t.name
}
func Hello(i I) {
fmt.Printf("Hi, my name is %s\n", i.M())
}
func main() {
Hello(T{name: "Michał"}) // "Hi, my name is Michał"
}

函数Hello中,方法调用i.M()通过特定的方式实现:当方法是类型满足的接口的实现时,不同类型的方法可以被调用。
Golang的重要特征:接口是隐式实现的,程序员不需要显示声明类型T实现了接口I。这项工作是由Go编译器自动完成的(永远不要让人去做机器应该做的事情)。这种行为的优雅实现使得如下这种方式成为可能:定义一个接口,这个接口被已经写好的类型自动实现(不需要对之前已完成的类型做修改)。
译者注:这种语言级的特性,可以在新增接口时,既有类型不修改,则自动实现了新的接口。这种多态的方式具有很好的灵活性。


这一特性为接口提供了很大灵活性:单个类型可以实现多个接口示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type I1 interface {
M1()
}
type I2 interface {
M2()
}
type T struct{}
func (T) M1() { fmt.Println("T.M1") } // 类型T实现了接口I1
func (T) M2() { fmt.Println("T.M2") } // 类型T实现了接口I2
func f1(i I1) { i.M1() }
func f2(i I2) { i.M2() }
func main() {
t := T{}
f1(t) // "T.M1"
f2(t) // "T.M2"
}

或者同一个接口可以被多种类型实现示例代码

1
2
3
4
5
6
7
8
9
10
11
12
type I interface {
M()
}
type T1 struct{}
func (T1) M() { fmt.Println("T1.M") } // 类型T1实现了接口I
type T2 struct{}
func (T2) M() { fmt.Println("T2.M") } // 类型T2实现了接口I
func f(i I) { i.M() }
func main() {
f(T1{}) // "T1.M"
f(T2{}) // "T2.M"
}

除了一个或多个接口要求的方法,类型可以自由实现其他不同的方法。

在Golang中,关于接口的两个概念:

  1. 接口(Interface):实现此接口必须的系列方法,通过关键词interface定义。
  2. 接口类型(Interface type):接口类型变量可以保存实现了任何特定接口的类型值。
    下面分别展开讨论这两个概念。

    定义一个接口

    接口定义:方法、内嵌其他接口

    接口的声明指定了属于此接口的方法,方法定义则通过其名称和签名(输入和返回参数)完成,如下:
    1
    2
    3
    4
    5
    6
    type I interface { // 定义了一个接口I,它包含四个方法
    m1()
    m2(int)
    m3(int) int
    m4() int
    }

接口中除了包含方法,也允许内嵌(embedded)其他接口-同一个包内定义或已被导入,此时接口将内嵌接口的所有方法导入到自己的定义中,如下示例:

1
2
3
4
5
6
7
8
9
import "fmt"
type I interface {
m1()
}
type J interface {
m2()
I
fmt.Stringer
}

接口J包含的方法集为:

  • m1() - 来自内嵌的接口I
  • m2() - 自定义方法
  • String() - 来自fmt.Stringer接口的String()方法(此接口中只有这一个方法)
    接口内方法顺序不关紧要,所以接口内可能出现方法和内嵌接口的交错出现的情况。

    接口将导入内嵌接口的所有方法,包含可导出方法(首字母大写的方法)和非导出方法(首字母小写)。

内嵌接口多层内嵌时,导入所有包含接口的方法

如果接口I嵌入了接口J,而接口又内嵌了其他接口K,则接口K的所有方法也将被加入到接口I的声明中,如下:

1
2
3
4
5
6
7
8
9
10
11
type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
}

接口I包含的方法为:i(), j(), k()

禁止接口的循环内嵌

Circular embedding of interfaces is disallowed and will be detected while compilation (source code):
接口的循环内嵌是被禁止的,正在编译阶段将被检测出错误,如下代码将产生error错误interface type loop involving I

1
2
3
4
5
6
7
8
9
10
11
12
type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
I
}

接口内的方法名必须唯一

如下代码中自定义方法和内嵌接口包含方法存在命名冲突,则将会抛出编译时错误error:duplicate method i

1
2
3
4
5
6
7
8
type I interface {
J
i()
}
type J interface {
j()
i(int)
}

这种接口的组装形式贯穿标准库的各种定义,一个io.ReaderWriter的例子:

1
2
3
4
type ReadWriter interface {
Reader
Writer
}

至此,我们已经知道了怎么新建一个接口,接下来介绍接口类型的变量(values of interface types)。

接口类型的变量

接口I类型的变量可以保存任何实现了接口I的值,示例如下

1
2
3
4
5
6
7
8
9
type I interface {
method1()
}
type T struct{}
func (T) method1() {}
func main() {
var i I = T{} // 变量i是接口类型I的变量, T实现了接口I, 则i可以保存类型为T的变量值
fmt.Println(i)
}

静态类型VS动态类型

变量已有的类型在编译阶段被明确,在声明时指定变量类型,且永不改变,这种情况称为静态类型(static type)或直接简称类型
接口类型的变量也有一种静态的类型,就是接口自身;他们额外还拥有动态类型(dynamic type),这种类型是可赋值的类型。
示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type I interface {
M()
}
type T1 struct {}
func (T1) M() {} // 类型T1实现了接口I
type T2 struct {}
func (T2) M() {} // 类型T2实现了接口I
func main() {
var i I = T1{} // 接口类型变量i,可以赋值为T1,也可以被赋值为类型T2
fmt.Printf("%T\n", i) // 输出main.T1
i = T2{}
fmt.Printf("%T\n", i) // 输出main.T2
_ = i
}

变量i的静态类型是接口I,这是不会改变的。
另一方面,动态类型也就是动态变化的,第一次赋值后,i的动态类型为类型T1,然而这并不是固化的,第二次赋值将i的动态类型修改为类型T2。
当接口类型的变量值为空值nil时(接口的零值为nil),则动态类型未被设置。

如何获取接口类型变量的动态类型

  1. reflect包提供获取动态类型的方法,示例代码:
    当变量为零值nil时,reflect包将报错runtime error。

    1
    2
    fmt.Println(reflect.TypeOf(i).PkgPath(), reflect.TypeOf(i).Name())
    fmt.Println(reflect.TypeOf(i).String())
  2. fmt包通过格式化动词%T也可以获取变量的动态类型:
    虽然fmt也是使用reflect来实现的,但当变量i为零值nil时也可以支持。

    1
    fmt.Printf("%T\n", i)

接口类型空值nil

看如下的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type I interface {
M()
}
type T struct {}
func (T) M() {} // 类型T实现了接口I
func main() {
var t *T // 变量t必然是空值nil
if t == nil {
fmt.Println("t is nil") // 输出这里
} else {
fmt.Println("t is not nil")
}
var i I = t // t是空值,但i呢?
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is not nil")
}
}

输出结果:

1
2
t is nil
i is not nil

这个输出结果显然有些吃惊,赋值给变量i的值是nil,但i并不等于nil,接口类型变量包含两个部分

  • 动态类型(dynamic type)
  • 动态值(dynamic value)
    动态类型在前面章节[动态类型VS静态类型]已介绍。
    动态值是实际变量实际被赋值的值,在上面的例子中var i I = t,变量i的动态值是nil,但i的动态类型是*T
    通过fmt.Printf("%T\n", i)输出赋值后的变量i的动态类型为*main.T接口类型变量为nil当且仅当动态类型和动态值均为nil
    这种情况下,即使接口类型的变量保存了一个空值指针(nil pointer),但这个接口变量并不是nil。
    已知的错误是从应该返回接口类型的函数返回未初始化、非接口类型的变量值,示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type I interface {}
    type T struct {}
    func F() I { // 函数F应该返回接口类型I,在此例中接口类型的返回值=返回类型为*T,值为nil
    var t *T
    if false { // not reachable but it actually sets value
    t = &T{}
    }
    return t // 这里返回的变量t是空值nil
    }
    func main() {
    fmt.Printf("F() = %v\n", F()) // 返回参数的动态值为nil
    fmt.Printf("F() is nil: %v\n", F() == nil) // 返回参数为接口类型,此接口类型的值并不为nil
    fmt.Printf("type of F(): %T", F()) // 返回参数类型为类型T
    }

打印输出:

1
2
3
F() = <nil>
F() is nil: false
type of F(): *main.T

just because interface type value returned from function has dynamic value set (*main.T), it isn’t equal to nil.

就是因为函数返回的接口类型值的动态类型为*main.T,所有它不等于空值nil

空的接口

接口的方法集可以完全为空,示例代码:

1
2
3
4
5
6
7
type I interface {}
type T struct {}
func (T) M() {}
func main() {
var i I = T{}
_ = i
}

空接口自动被任何类型实现,所以任何类型都可以赋值给这种空接口类型的变量。
空接口的动态或静态类型的行为和非空接口一致。
fmt.Println函数中可变参数中空接口广泛使用。
TODO: 可变参数的实现原理?

实现接口

实现方法所有方法的任何类型都自动满足(实现)这个接口。不需要想Java中那样显示声明类型实现了哪个接口。
Go编译器自动检测类型对接口的实现,这是Golang语言级的强大特性。
示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"fmt"
"regexp"
)
type I interface {
Find(b []byte) []byte // 接口I包含方法Find,而Regexp实现包含此方法实现(方法名+签名),则Regexp实现了接口I
}
func f(i I) {
fmt.Printf("%s\n", i.Find([]byte("abc")))
}
func main() {
var re = regexp.MustCompile(`b`) // 返回类型为*Regexp
f(re)
}

这里我们定义了一个接口I,在没有修改内置的regexp模块的情况下,使得:regexp.Regexp类型实现了接口I。

  • 一个类型可以实现多个接口,一个接口可以被多个类型实现
  • 一个接口实现某接口,赋值给接口类型后,只能访问接口自身定义的方法

接口类型行为的抽象

接口类型值只能访问此接口类型的方法,其隐藏原类型中包含的其他值,比如结构体、数组、scalar等等。
示例代码

1
2
3
4
5
6
7
8
9
10
11
type I interface {
M1()
}
type T int64
func (T) M1() {}
func (T) M2() {}
func main() {
var i I = T(10) // 接口类型值i只能访问M1()方法
i.M1()
i.M2() // i.M2 undefined (type I has no field or method M2)
}

更多阅读