接口
1.定义
Go不是一种“传统”的面向对象编程语言:里面没有类和继承的概念。
但是Go语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来说明对象的行为:如果谁能搞定这件事,它就可以用在这。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
定义接口,:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的Namer是一个接口类型。
接口的名字由方法名加 er 后缀组成,例如Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。
Go语言中的接口都狠简短,通常它们会包含0个、最多3个方法。
接口可以有值,一个接口类型的变量或一个接口值:var ai Namer,ai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
类型(比如结构体)可以实现某个接口里方法集;这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口的方法集。实现了Namer接口的类型的变量可以赋值给ai(即receiver的值),方法表指针(method table ptr)就指向了当前的方法实现。当另一个实现了 Namer 接口的类型的变量被赋给 ai,receiver 的值和方法表指针也会相应改变。
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以包含一个实例的引用,该实例的类型实现了此接口(接口是动态类型)。
即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口的方法,它就实现了此接口。
package main
import (
"fmt"
)
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func main() {
sql := new(Square)
sql.side = 5
// var areaIntf Shaper
// areaIntf = sql
areaIntf := sql
fmt.Printf("The Square has area: %f\n", areaIntf.Area())
}
上面的程序定义了一个结构体 Square 和一个接口 Shaper,接口有一个方法 Area()。
在 main() 方法中创建了一个 Square 的实例。在主程序外边定义了一个接收者类型是 Square 方法的 Area(),用来计算正方形的面积:结构体 Square 实现了接口 Shaper 。
所以可以将一个 Square 类型的变量赋值给一个接口类型的变量:areaIntf = sq1 。
现在接口变量包含一个指向 Square 变量的引用,通过它可以调用 Square 上的方法 Area()。当然也可以直接在 Square 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。
这是 多态 的Go版本,多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。
如果 Square 没有实现 Area() 方法,编译器将会给出清晰的错误信息:
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)
如果 Shaper 有另外一个方法 Perimeter(),但是Square 没有实现它,即使没有人在 Square 实例上调用这个方法,编译器也会给出上面同样的错误。
扩展一下上面的例子,类型 Rectangle 也实现了 Shaper 接口。接着创建一个 Shaper 类型的数组,迭代它的每一个元素并在上面调用 Area() 方法,以此来展示多态行为:
package main
import (
"fmt"
)
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3} // Area() of Rectangle needs a value
q := &Square{5} // Area() of Square needs a pointer
// shapes := []Shaper{Shaper(r), Shaper{q}}
// or shorter
shapes := []Shaper{r, q}
fmt.Println("Looping through shapes for area ...")
for n, _ := range shapes {
fmt.Println("Shape detail: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area())
}
}
在调用 shapes[n].Area() 这个时,只知道 shapes[n] 是一个 Shaper 对象,最后它摇身一变成为了一个 Square 或 Rectangle 对象,并且表现出了相对应的行为。通过接口如何产生 更干净、更简单 及 更具有扩展性 的代码。
下面是一个更具体的例子:有两个类型 stockPosition 和 car,它们都有一个 getValue() 方法,可以定义一个具有此方法的接口 valuable。接着定义一个使用 valuable 类型作为参数的函数 showValue(),所有实现了 valuable 接口的类型都可以用这个函数。
package main
import (
"fmt"
)
type stockPosition struct {
thicker string
sharePrice float32
count float32
}
/* method to determine the value of a stock position */
func (s stockPosition) getValue() float32 {
return s.sharePrice * s.count
}
type car struct {
make string
model string
price float32
}
/* method to determine the value of a car */
func (c car) getValue() float32 {
return c.price
}
type valuable interface {
getValue() float32
}
func showValue(asset valuable) {
fmt.Printf("Value of the asset is %f\n", asset.getValue())
}
func main() {
var o valuable = stockPosition{"GOOG", 577.20, 4}
showValue(o)
o = car{"BMW", "M3", 123456}
showValue(o)
}
一个标准库的例子:
io 包里有一个接口类型Reader:
type Reader interface {
Read(p []byte) (n int, err error)
}
那么就可以写入下的代码:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
f,_ := os.Open("test.txt")
r = bufio.NewReader(f)
上面 r 右边的类型都实现了 Read() 方法,并且有相同的方法签名,r 的静态类型是 io.Reader。
2.接口嵌套接口
一个接口可以包含一个多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
3.类型断言
一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的动态类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常可以使用类型断言来测试在某个时刻 varI 是否包含类型T的值:
v := varI.(T) // unchecked type assertion
varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)
。
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能遇见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
if v, ok := varI.(T); os {
Process(v)
return
}
// varI is not of type T
如果转换合法,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。
应该总是使用上面的方式来进行类型断言。
多数情况下,可能只想在if中测试下ok的值,使用以下的方法会是最方便的:
if _, ok := varI.(T); ok {
//...
}
type_interfaces.go:
package main
import (
"fmt"
"math"
)
type Square struct {
side float32
}
type Circle struct {
radius float32
}
type Shaper interface {
Area() float32
}
func main() {
var areaIntf Shaper
sq1 := new(Square)
sq1.side = 5
areaIntf = sq1
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of ereaIntf is: %T\n", t)
}
if u, ok := areaIntf.(*Circle); ok {
fmt.Printf("The type of ereainft is: %T\n", u)
} else {
fmt.Printf("ereaIntf does not contain a variable of type Circle")
}
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func (ci *Circle) Area() float32 {
return ci.radius * ci.radius * math.Pi
}
程序中定义了一个新类型Circle,它也实现了Shaper接口。if t, ok := areaIntf.(*Square); ok
测试 areaIntf 里是否有一个包含 _Square 类型的便利,结果是确定的;然后我们测试它是否包含一个 _Circle 类型的变量,结果是否定的。
备注: 如果忽略 areaIntf.(*Square)
中的 * 号,会导致编译错误:impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)
。
接口变量的类型也可以使用一种特殊形式的switch来检测:
switch t := areaIntf.(type) {
case *Square:
fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
fmt.Printf("nil value: nothing to check?\n")
default:
fmt.Printf("Unexpected type %T\n", t)
}
4.使用方法集与接口
作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。
methodset2.go:
package main
import (
"fmt"
)
type List []int
func (l List) Len() int {
return len(l)
}
func (l *List) Append(val int) {
*l = append(*l, val)
}
type Appender interface {
Append(int)
}
func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}
type Lener interface {
Len() int
}
func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}
func main() {
// A bare value
var lst List
// compiler error
// cannot use lst(type List) as type Appender in argument to CountInfo
// List dose not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID: Identical receiver type
fmt.Printf("- lst is long enough\n")
}
// A pointer value
plst := new(List)
CountInto(plst, 1, 10)
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
在lst上调用CountInto时会导致一个编译器错误,因为CountInto需要一个Appender,而它的方法Append只定义在指针上。在lst上调用LongEnough是可以的,因为Len定义在值上。
在plst上调用CountInto是可以的,因为CountInto需要一个Appender,并且它的方法Append定义在指针上。在plst上调用LongEnough也是可以的,因为指针会被自动解引用。
5.总结
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者可以从具体类型P直接可以辨识的:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
Go语言规范定义了接口方法集的调用规则:
- 类型T的可调用方法集包含接受者为*T或T的所有方法集
- 类型T的可调用方法集包含接受者为T的所有方法
- 类型*T的可调用方法集不包含接受者为T的方法
6.例子:使用Sorter接口排序
要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的len()方法、比较第i和j个元素的Less(i,j)方法以及交换第i和j个元素的Swap(i,j)方法。
排序函数的算法只会使用到这三个方法(可以使用任何排序算法来实现,此处使用冒泡排序):
func Sort(data Sorter) {
for pass := 1; pass < data.Len(); pass++ {
for i := 0; i < data.Len() - pass; i++ {
if data.Less(i+1, i) {
data.Swap(i, i + 1)
}
}
}
}
Sort函数接收一个接口类型的参数:Sorter,它声明了这些方法:
type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
参数中的int是待排序序列长度的类型,而不是说要排序的对象一定要是一组int。i和j表示元素的整型索引,长度也是整型的。
如果想对一个int数组进行排序,所有必须做的事情就是:为数组定一个类型并在它上面实现Sorter接口的方法:
type IntArray []int
func (p IntArray) Len() int { return len(p) }
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }
func (p IntArray) Swap(i, j int) { p[i],p[j] = p[j],p[i] }
调用排序函数的一个具体例子:
data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
a := sort.IntArray(data) //conversion to type IntArray from package sort
sort.Sort(a)
sort.go
package sort
type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
func Sort(data Sorter) {
for pass := 1; pass < data.Len(); pass++ {
for i := 0; i < data.Len()-pass; i++ {
if data.Less(i+1, i) {
data.Swap(i, i+1)
}
}
}
}
func IsSorted(data Sorter) bool {
n := data.Len()
for i := n - 1; i > 0; i-- {
if data.Less(i, i-1) {
return false
}
}
return true
}
type IntArray []int
func (p IntArray) Len() int { return len(p) }
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }
func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
type StringArray []string
func (p StringArray) Len() int { return len(p) }
func (p StringArray) Less(i, j int) bool { return p[i] < p[j] }
func (p StringArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func SortInts(a []int) { Sort(IntArray(a)) }
func SortStrings(a []string) { Sort(StringArray(a)) }
func IntsAreSorted(a []int) bool { return IsSorted(IntArray(a)) }
func StringsAreSorted(a []string) bool { return IsSorted(StringArray(a)) }
sortmain.go
package main
import (
"fmt"
"sort_test/sort"
)
func ints() {
data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
a := sort.IntArray(data)
sort.Sort(a)
if !sort.IsSorted(a) {
panic("fails")
}
fmt.Printf("The sorted array is: %v\n", a)
}
func strings() {
data := []string{"monday", "friday", "tuesday", "wednesday", "sunday", "thursday", "", "saturday"}
a := sort.StringArray(data)
sort.Sort(a)
if !sort.IsSorted(a) {
panic("fail")
}
fmt.Printf("The sorted array is: %v\n", a)
}
type day struct {
num int
shortName string
longName string
}
type dayArray struct {
data []*day
}
func (p *dayArray) Len() int { return len(p.data) }
func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }
func days() {
Sunday := day{0, "SUN", "Sunday"}
Monday := day{1, "MON", "Monday"}
Tuesday := day{2, "TUE", "Tuesday"}
Wednesday := day{3, "WED", "Wednesday"}
Thursday := day{4, "THU", "Thursday"}
Friday := day{5, "FRI", "Friday"}
Saturday := day{6, "SAT", "Saturday"}
data := []*day{&Thursday, &Tuesday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday}
a := dayArray{data}
sort.Sort(&a)
if !sort.IsSorted(&a) {
panic("fail")
}
for _, d := range data {
fmt.Printf("%s ", d.longName)
}
fmt.Printf("\n")
}
func main() {
ints()
strings()
days()
}
7.例子:读和写
读和写诗软件中很普遍的行为,提起它们会立即想到读写文件、缓存(比如字节或字符串切片)、标准输入输出、标准错误以及网络连接、管道等等。为了让代码尽可能通用,Go采取了一致的方式来读写数据。
io包提供了用于读和写的接口io.Reader和io.Writer:
type Reader interface {
Read(p []byte)(n int,err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
只要类型实现了读写接口,提供 Read 和 Write 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 io.Reader 接口,这个接口只有一个签名是 Read(p []byte) (n int, err error) 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 error 对象,如果没有错误发生返回 nil,如果已经到达输入的尾端,会返回 io.EOF(“EOF”),如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 io.Writer 接口,这个接口也只有一个签名是 Write(p []byte) (n int, err error) 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数和一个 error 对象(如果没有错误发生就是 nil)。
8.空接口
8.1 概念
空接口或者最小接口不包含任何方法,它对实现不做任何要求:
type Any interface {}
其他任何类型都实现了空接口,any或Any是空接口一个很好的别名或缩写。
可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。
package main
import "fmt"
var i = 5
var str = "ABC"
type Person struct {
name string
age int
}
type Any interface{}
func main() {
var val Any
val = 5
fmt.Printf("val has the value: %v\n", val)
val = str
fmt.Printf("val has the value: %v\n", val)
pers1 := new(Person)
pers1.name = "Rob Pike"
pers1.age = 55
val = pers1
fmt.Printf("val has the value: %v\n", val)
switch t := val.(type) {
case int:
fmt.Printf("Type int %T\n", t)
case string:
fmt.Printf("Type string %T\n", t)
case bool:
fmt.Printf("Type boolean %T\n", t)
case *Person:
fmt.Printf("Type pointer to Person %T\n", t)
default:
fmt.Printf("Unexpected type %T", t)
}
}
接口变量val被依次赋予一个int,string和Person实例的值,然后使用type-switch来测试它的实际类型。每个interface{}变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。
示例emptyint_switch.go说明了空接口在type-switch中联合lambda函数的用法:
package main
import "fmt"
type specialString string
var whatIsThis specialString = "hello"
func TypeSwitch() {
testFunc := func(any interface{}) {
switch v := any.(type) {
case bool:
fmt.Printf("any %v is a bool type", v)
case int:
fmt.Printf("any %v is an int type", v)
case float32:
fmt.Printf("any %v is a float32 type", v)
case string:
fmt.Printf("any %v is a string type", v)
case specialString:
fmt.Printf("any %v is a special String!", v)
default:
fmt.Println("unknown type!")
}
}
testFunc(whatIsThis)
}
func main() {
TypeSwitch()
}
8.2 构建通用类型或包含不同类型变量的数组
能被搜索和排序的int数组、float数组以及string数组,那么对于其他类型的数组,可以通过使用空接口,让我们给空接口定一个别名类型 Element:type Element interface{}
然后定义一个容器类型的结构体 Vector,它包含一个Element类型元素的切片:
type Vector struct {
a []Element
}
Vector里能放任何类型的变量,因为任何类型都实现了空接口,实际上Vector里放的每个元素可以是不同类型的变量。我们为它定义一个At()方法用于返回第i个元素:
func (p *Vector) At(i int) Element {
return p.a[i]
}
在定一个Set()方法用于设置第i个元素的值:
func (p *Vector) Set(i int, e Element) {
p.a[i] = e
}
Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。
8.3 复制数据切片至空接口切片
假设你有一个myType类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice
可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType as type []interface{ } in assignment
。原因是它俩在内存中的布局不一样。
必须使用for-range语句来一个一个显式地赋值:
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
8.4 通用类型的节点数据结构
列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的NewNode方法及设置数据的SetData方法
node_structrues.go:
package main
import "fmt"
type Node struct {
le *Node
data interface{}
ri *Node
}
func NewNode(left, right *Node) *Node {
return &Node{left, nil, right}
}
func (n *Node) SetData(data interface{}) {
n.data = data
}
func main() {
root := NewNode(nil, nil)
root.SetData("root node")
// make child (leaf) nodes
a := NewNode(nil, nil)
a.SetData("left node")
b := NewNode(nil, nil)
b.SetData("right node")
root.le = a
root.ri = b
fmt.Printf("%v\n %v\n %v\n", a, b, root)
}
8.5 接口到接口
一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败回导致一个运行时错误:
假定:
var ai AbsInterface //declares method Abs()
type SqrInterface interface {
Sqr() float
}
var si SqrInterface
pp := new(Point) //say *Point implements Abs, Sqr
var empty interface{}
那么下面的语句和类型断言是合法的:
empty = pp //everything satisfies empty
ai = empty.(AbsInterface) //underlying value pp implements Abs()
// (runtime failure otherwise)
si = ai.(SqrInterface) //*Point has Sqr() even though AbsInterface dosen't
empty = si //*Point implements empty set
// Note: statically checkable so type assertion not necessary
函数调用的一个例子:
type myPrintInterface interface {
print()
}
func f3(x myInterface) {
x.(myPrintInterface).print()
}
x转换为myPrintInterface类型事完全动态的,只要x的底层类型(动态类型)定义了print方法这个调用就可以正常运行(若x的底层类型未定义print方法,此处类型断言会导致panic,最佳实践应该为if mp1, ok := x.(myPrintInterface); ok {mpi.print() })。
9.反射包
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和动态的调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真的有必要,否则应当避免使用或小心使用。
变量的最基本信息就是类型和值:反射包的Type用来表示一个Go类型,反射包的value为Go提供了反射接口。
两个简单的函数,reflect.TypeOf 和 reflect.ValueOf, 返回被检查对象的类型和值。例如,x被定义为:var x float64 = 3.4,那么reflect.TypeOf(x)返回float64,reflect.ValueOf(x)返回 <\float64 Value>
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
接口的值包含一个type和value,反射可以从接口值反射到对象,也可以对象反射接口值。
reflect1.go:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type: ", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value: ", x)
fmt.Println("type: ", v.Type())
fmt.Println("kind: ", v.Kind())
fmt.Println("value: ", v.Float())
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)
}
10.Go的动态类型
Go没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系;Go的中接口跟Java/c#类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。
Go是结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型的变量。实现了某个接口的类型可以被传给任何以此接口为参数的函数。
这在实例duck_dance.go中得以阐明,函数DuckDance接受一个IDuck接口类型变量。仅当DuckDance被实现了IDuck接口的类型调用时程序才能编译通过。
duck_dance.go:
package main
import "fmt"
type IDuck interface {
Quack()
Walk()
}
func DuckDance(duck IDuck) {
for i := 1; i <= 3; i++ {
duck.Quack()
duck.Walk()
}
}
type Bird struct {
// ..
}
func (b *Bird) Quack() {
fmt.Println("I am quacking!")
}
func (b *Bird) Walk() {
fmt.Println("I am walking")
}
func main() {
b := new(Bird)
DuckDance(b)
}
如果Bird没有实现Walk(),就会得到一个编译错误:
MaGedu-Go/local_code/interface/duck_dance.go:31:11: cannot use b (type *Bird) as type IDuck in argument to DuckDance:
*Bird does not implement IDuck (missing Walk method)
10.1 动态方法调用
像Python、Ruby这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后再运行时才解析(它们很可能有像responds_to这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)。
Go的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像interface{}这样的”泛型”上,你可以通过类型断言来检查变量是否实现了相应接口。
例如,你用不同的类型表示XML输出流中的不同实体。然后为XML定义一个如下的”写”接口(甚至可以把它定义为私有接口):
type xmlWriter interface {
WriteXML(w io.Writer) error
}
现在可以实现适用于该流类型的任何变量的streamXML函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的encodeToXML来完成相应的工作:
// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It's an xmlWriter, use method of asserted type
return xw.WriterXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}
// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}
Go在这里用了和gob相同的机制:定义了两个接口 GobEncoder 和 GobDecoder。这样就允许类型自己实现从流编解码的具体方式;如果没有实现就使用标准的反射方式。
因此Go提供了动态语言的优点,却没有其他动态语言在运行时可能发生错的缺点。
对于动态语言非常重要的单元测试来说,这样既可以减少单元测试的部分需求,又可以发挥相当大的作用。
Go的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用Go接口还能实现依赖注入模式。
10.2 接口的提取
提取接口是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
Go接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
假设需要一个新的接口TopologicalGenus,用来给shape排序。需要做的是给想要满足接口的类型实现Rank()方法:
package main
import "fmt"
type Shaper interface {
Area() float32
}
type TopologicalGenus interface {
Rank() int
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func (sq *Square) Rank() int {
return 1
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func (r Rectangle) Rank() int {
return 2
}
func main() {
r := Rectangle{5, 3} // Rectangle needs a value
q := &Square{5} // Square needs a pointer
shapes := []Shaper{r, q}
fmt.Println("Looping through shapes for area ....")
for n, _ := range shapes {
fmt.Println("Share details: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area())
}
topgen := []TopologicalGenus{r, q}
fmt.Println("Looping through topgen for area ....")
for n, _ := range topgen {
fmt.Println("Share details: ", topgen[n])
fmt.Println("Topologic of this shape is: ", topgen[n].Rank())
}
}
所以不用提前设计出所有的接口:整个设计可以持续演进,而不用废弃之前的决定。类型要实现某个接口,它本身不用改变,只需要在这个类型上实现新的方法。
10.3 显式地指明类型实现了某个接口
可以向接口的方法集中添加一个具有描述性名字的方法,大部分代码并不是用这样的约束,因为它限制了接口的实用性。但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。
type Fooer interface {
Foo()
ImplementsFooer()
}
类型Bar必须实现ImplementsFooer方法来满足Fooer接口,以清楚地记录这个事实。
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
10.4 接口的继承
当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。
如:
type Task struct {
Command string
*log.Logger
}
func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}
当log.Logger实现了Log()方法后,Task的实例task就可以调用该方法:task.Log()
类型可以通过继承多个接口来提供像多重继承一样的特性:
type ReaderWriter struct {
*io.Reader
*io.Writer
}
11.总结:Go中的面向对象
Go没有类,而是松耦合的类型、方法对接口的实现。
封装,继承和多态:
封装(数据隐藏):
- 包范围内的:通过标识符首字母小写,对象只在它所在的包内可见
- 可导出的:通过标识符首字母大写,对象对所在包以外也可见
- 继承:用组合实现,内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现;
- 多态:用接口实现,某个类型的实例可以赋给它所实现的接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go接口不是Java和C#接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
12.结构体、集合和高阶函数
通常在应用中定义了一个结构体,可能需要这个结构体的(指针)对象集合,比如:
type Any interface{}
type Car struct {
Model string
Manufacturer string
BuildYear int
// ...
}
type Cars []*Car
然后就可以使用高阶函数,实际上也就是把函数作为定义所需方法(其它函数)的参数,例如:
- 定义一个通用的Process()函数,它接收一个作用于每一辆car的f函数作参数:```go
func (cs Cars) Process(f func(car *Car)) {
for _, c := range cs {
} } ```f(c)
- 在上面的基础上,实现一个查找函数来获取子集合,并在Process()中传入一个闭包执行(这样就可以访问局部切片cars):```go
func (cs Cars) FindAll(f func(car Car) bool) Cars {
cars := make([]Car, 0)
cs.Process(func(c *Car) {
}) } ```if f(c) {
cars = append(cars, c)
}
- 实现Map功能,产出除car对象以外的东西:```go
func (cs Cars) Map(f func(car Car) Any) []Any {
result := make([]Any, 0)
ix := 0
cs.Process(func(c Car) {
}) return result } ```result[ix] = f(c)
ix++
- 可以定义下面这样的具体查询:
go allNewBMWs := allCars.FindAll(func(car *Car) bool { return (car.Manufacturer == "BMW") && (car.BuildYear > 2010) })
也可以根据参数返回不同的函数。也许我们想根据不同的厂商添加汽车到不同的集合,但是这(这种映射关系)可能会是会改变的。所以我们可以定义一个函数来产生特定的添加函数和 map 集:```go func MakeSortedAppender(manufacturers []string) (func(car *Car), map[string]Cars) { sortedCars := make(map[string]Cars)
for _, m := range manufacturers {
sortedCars[m] = make([]*Car, 0)
}
sortedCars[“Default”] = make([]*Car, 0)
appender := func(c *Car) {
if _, ok := sortedCars[c.Manufacturer]; ok {
sortedCars[c.Manufacturer] = append(sortedCars[c.Manufacturer], c)
} else {
sortedCars["Default"] = append(sortedCars["Default"], c)
}
} return appender, sortedCars } ```