原文地址: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
5type I interface {
f1(name string)
f2(name string) (error, float32)
f3() int64
}
并不需要强制的接口实现。我们说类型(type)实现或满足接口(interface),只需要它定义了接口期望的方法名和签名(输入和返回参数),如下代码段二:1
2
3
4
5
6
7
8
9
10type 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
15type 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
16type 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
12type 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中,关于接口的两个概念:
- 接口(Interface):实现此接口必须的系列方法,通过关键词
interface
定义。 - 接口类型(Interface type):接口类型变量可以保存实现了任何特定接口的类型值。
下面分别展开讨论这两个概念。定义一个接口
接口定义:方法、内嵌其他接口
接口的声明指定了属于此接口的方法,方法定义则通过其名称和签名(输入和返回参数)完成,如下:1
2
3
4
5
6type I interface { // 定义了一个接口I,它包含四个方法
m1()
m2(int)
m3(int) int
m4() int
}
接口中除了包含方法,也允许内嵌(embedded)其他接口-同一个包内定义或已被导入,此时接口将内嵌接口的所有方法导入到自己的定义中,如下示例:1
2
3
4
5
6
7
8
9import "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
11type 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
12type 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
8type I interface {
J
i()
}
type J interface {
j()
i(int)
}
这种接口的组装形式贯穿标准库的各种定义,一个io.ReaderWriter的例子:1
2
3
4type ReadWriter interface {
Reader
Writer
}
至此,我们已经知道了怎么新建一个接口,接下来介绍接口类型的变量(values of interface types)。
接口类型的变量
接口I类型的变量可以保存任何实现了接口I的值,示例如下:1
2
3
4
5
6
7
8
9type 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
14type 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),则动态类型未被设置。
如何获取接口类型变量的动态类型
reflect包提供获取动态类型的方法,示例代码:
当变量为零值nil时,reflect包将报错runtime error。1
2fmt.Println(reflect.TypeOf(i).PkgPath(), reflect.TypeOf(i).Name())
fmt.Println(reflect.TypeOf(i).String())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
19type 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
2t 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
14type 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
3F() = <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
7type 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
14import (
"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
11type 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)
}