
南京莫愁湖
在Go中,接口可以使我们的代码更灵活更可扩展,除此之外,它可帮助我们达到实现“多态”的目的。与其他语言需要一种特殊类型不同的是,Go中的接口允许我们仅指定一些需要的行为,而这些行为是通过定义一系列相关的方法达成。
type I interface {
f1(name string)
f2(name string) (error, float32)
f3() int64
}
接口本身无需特别的实现。如果一个类型的方法满足接口中所有的方法签名,就认为这个类型实现(满足)了这个接口。
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”作为其参数的函数。
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()”可以在各种类型中正常执行到,只要这些类型的方法实现了接口”I”。
这种接口的隐式实现正式Go的一大特色。
程序猿(媛)无需显式地指定类型”T”实现了接口”I”。这项工作编译器默认帮我们做好了。这种隐式特性非常方便地做到让接口很自然被已有类型实现,而不用对现有代码做改动。
事实上,这种灵活性让一个类型可以实现多个接口:
type I1 interface {
M1()
}
type I2 interface {
M2()
}
type T struct{}
func (T) M1() { fmt.Println("T.M1") }
func (T) M2() { fmt.Println("T.M2") }
func f1(i I1) { i.M1() }
func f2(i I2) { i.M2() }
func main() {
t := T{}
f1(t) // "T.M1"
f2(t) // "T.M2"
}
或同一个接口被多个类型实现:
type I interface {
M()
}
type T1 struct{}
func (T1) M() { fmt.Println("T1.M") }
type T2 struct{}
func (T2) M() { fmt.Println("T2.M") }
func f(i I) { i.M() }
func main() {
f(T1{}) // "T1.M"
f(T2{}) // "T2.M"
}
在Go中,有两个关于接口的概念:
- 接口——待实现方法的集合。它通过关键字“interface”定义
- 接口类型——接口类型变量,它包含任意实现这个接口的值
我们在以下段落中讨论。
定义接口
声明接口类型,同时指定属于它的方法。方法通过它的名字和签名(输入参数和返回值)定义。
type I interface {
m1()
m2(int)
m3(int) int
m4() int
}
除了接口内部直接定义方法;嵌入其他接口也是允许的,不论嵌入同一个包内的的接口,还是通过引入的方式,被嵌入的方法都会被增加到嵌入它的接口中:
import "fmt"
type I interface {
m1()
}
type J interface {
m2()
I
fmt.Stringer
}
接口”J”的方法集包括:
- m1() (嵌入接口”I”包含)
- m2()
- String() string (嵌入接口Stringer)
接口的方法没有顺序关系,方法的交替指定并无影响。
被嵌入接口中的导出方法(方法名由大写字母开头)和非导出方法(方法名由小写字母开头)都会被增加到包含它的接口中。
如果”I”嵌入接口”J”,”J”嵌入”K”,那么所有”K”中的方法也会被添加到”I”:
type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
}
接口”I”的方法集包含i(),j()和k()。
接口不允许循环嵌套,并且这一点会在编译期间就检测出来:
type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
I
}
编译器会抛出错误:“interface type loop involving I”。
接口方法名称不允许同名:
type I interface {
J
i()
}
type J interface {
j()
i(int)
}
否则编译器错误:“duplicate method i”会被抛出。
我们可以在标准库中找到复合接口的例子,比如:”io.ReadWriter”:
type ReadWriter interface {
Reader
Writer
}
现在我们知道如何创建接口。接下来一起学习下接口类型的值……
接口类型的值
接口类型”I”的值可以包含任意实现”I”的值:
type I interface {
method1()
}
type T struct{}
func (T) method1() {}
func main() {
var i I = T{}
fmt.Println(i)
}
例子中接口类型”I”的变量”i”。
静态类型 vs 动态类型
变量类型在编译期中被识别出来。它在声明语句中被指明出来,这种从不改变类型的变量我们称之为静态类型(或者说 类型 )。而接口类型的变量除了拥有静态类型外,它们同时也拥有动态类型,这些类型是赋值时传递给接口类型变量的:
type I interface {
M()
}
type T1 struct {}
func (T1) M() {}
type T2 struct {}
func (T2) M() {}
func main() {
var i I = T1{}
i = T2{}
_ = i
}
变量”i”的静态类型是”I”。这点是不会变的。另一方面动态类型是动态的……在第一次赋值之后”i”的动态类型是”T1″,在第二次赋值后,它的动态类型变成了”T2″。当接口类型的值是”nil”时(接口类型变量的zero value是”nil”),那么它的动态类型为未设置。
如何获取接口类型值得动态类型
“reflect”包可以帮助我们达到此目标:
fmt.Println(reflect.TypeOf(i).PkgPath(), reflect.TypeOf(i).Name())
fmt.Println(reflect.TypeOf(i).String())
同时“fmt”包也可以通过格式化输出“%T”达到此目的:
fmt.Printf("%T\n", i)
在它底层也是通过“reflect”包的方法实现的,这种方法对”i”是“nil”的时候同样适用。
接口值“nil”
接下来我们以下例展开:
type I interface {
M()
}
type T struct {}
func (T) M() {}
func main() {
var t *T
if t == nil {
fmt.Println("t is nil")
} else {
fmt.Println("t is not nil")
}
var i I = t
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is not nil")
}
}
输出:
t is nil
i is not nil
起初可能会觉得很意外,我们赋给变量”i”的值是“nil”,但是”i”并不等于“nil”。接口变量由两部分组成:
- 动态类型
- 动态值
动态类型在前面已经讨论过了。
动态值是赋值语句中的那个实际变量值。在之前讨论的代码片段中:
var i I = t
i的动态值是“nil”但动态类型是”*T”。随后的函数调用:
fmt.Printf("%T\n", i)
会输出:
*main.T
接口类型值是“nil”当且仅当动态值和动态类型同时为“nil”。事实上即使接口类型值包含的是“nil”指针,这个接口值也不是“nil”。更为广泛了解的一个错误是从函数中返回未初始化、无接口类型值的接口类型:
type I interface {}
type T struct {}
func F() I {
var t *T
if false { // not reachable but it actually sets value
t = &T{}
}
return t
}
func main() {
fmt.Printf("F() = %v\n", F())
fmt.Printf("F() is nil: %v\n", F() == nil)
fmt.Printf("type of F(): %T", F())
}
它会输出:
F() = <nil>
F() is nil: false
type of F(): *main.T
正因为从函数中返回的接口类型值包含动态类型集(*main.T),它不等于“nil”。
空接口
接口的方法集不是必须包含至少一个成员的。它可以是一个彻彻底底的空接口:
type I interface {}
type T struct {}
func (T) M() {}
func main() {
var i I = T{}
_ = i
}
空接口可以自动满足任意类型——所以任意类型的值可以赋值给这样的接口类型变量。在空接口上的应用动态或者静态类型的行为和非空接口一致。最显著的空接口使用场景是变参数函数”fmt.Println”。
满足一个接口
我们无需像Java语言中利用关键字“implements”关键字来实现一个接口。任意满足接口方法的类型会自动满足这样的接口。它会自动被Go编译器检测,这确实是Go语言本身中很强的一个功能:
import (
"fmt"
"regexp"
)
type I interface {
Find(b []byte) []byte
}
func f(i I) {
fmt.Printf("%s\n", i.Find([]byte("abc")))
}
func main() {
var re = regexp.MustCompile(`b`)
f(re)
}
我们在这里定义了一个接口,可以无缝地被”regexp.Regexp”类型实现,而无需修改任何代码。
行为抽象
接口类型值仅可通过接口类型定义的方法访问。它隐藏了所有的内部实现细节,比如”struct”,”array”,”scalar”等:
type I interface {
M1()
}
type T int64
func (T) M1() {}
func (T) M2() {}
func main() {
var i I = T(10)
i.M1()
i.M2() // i.M2 undefined (type I has no field or method M2)
}