接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。
Go语言中接口类型的独特之处在于它是满足隐式实现的。 也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就 足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定 义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
接口约定
接口类型是一种抽象的类型。它不会暴露出它所代表的 对象的内部值的结构和这个对象支持的基础操作的集合;它们只会展示出它们自己的方法。也就是说当 你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
接口类型
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
有些新的接口类型通过组合已经有的接口来定义。
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
实现接口的条件
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close metho
在T类型的参数 上调用一个T的方法是合法的,只要这个参数是一个变量;编译器隐式的获取了它的地址。但这仅仅是一 个语法糖:T类型的值不拥有所有*T指针的方法,那这样它就可能只实现更少的接口。例如:
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
但是我们可以在一个IntSet值上调用这个方法:
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method
由于只有IntSet类型有String方法,所有也只有IntSet类型实现了fmt.Stringer接口:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method
接口值
概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口 的动态类型和动态值。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
一个接口值基于它的动态类型被描述为空或非空,所以这是一个空的接口值。你可以通过使用w==nil或 者w!=nil来判读接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
w.Write([]byte("hello")) // panic: nil pointer dereference
w = os.Stdout
w.Write([]byte("hello")) // "hello"
这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设 为*os.Stdout指针的类型描述符,它的动态值持有os.Stdout的拷贝;
通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。 这个调用的接收者是一个接口动态值的拷贝,os.Stdout。
接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并 且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者 作为switch语句的操作数。
然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比 较就会失败并且panic:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
一个包含nil指针的接口不是nil接口
一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容 易绊倒每个Go程序员的陷阱。
总结
- 通过满足接口定义的方法,类型就可以成为此接口的类型
- 接口的实现依托于动态分配具体类型。编译器把代码生成在类型描述符的方法上。
- 接口值可以使用 ==、!=来进行比较。然而接口值如果动态类型是不可比较的话,那么进行比较会失败,并且panic(不建议使用)
网友评论