七叶笔记 » golang编程 » Golang: Interface in Go(译1)

Golang: Interface in Go(译1)

南京莫愁湖

在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)
}  

相关文章