接口
定义
在传统的面向对象的语言中,是会存在类和继承的概念的,但是Go并没有
那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 开头
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namer,ai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
此处的方法指针表是通过运行时反射能力构建的。
类型(比如结构体)可以实现某个接口的方法集;这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口的方法集。实现了 Namer 接口的类型的变量可以赋值给 ai(即 receiver 的值),方法表指针(method table ptr)就指向了当前的方法实现。当另一个实现了 Namer 接口的类型的变量被赋给 ai,receiver 的值和方法表指针也会相应改变
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口
实现某个接口的类型(除了实现接口方法外)可以有其他的方法
一个类型可以实现多个接口
比如以下面的代码为定义
go
type test1Shaper interface {
Area() float32
}
type test1Square struct {
side float32
}
type test1Circle struct {
r float32
}
func (sq test1Square) Area() float32 {
return sq.side * sq.side
}
func (cr test1Circle) Area() float32 {
return 3.14 * cr.r * cr.r
}
func test1() {
sq := test1Square{10}
cr := test1Circle{5}
var areaInterface test1Shaper
areaInterface = sq
fmt.Println(areaInterface.Area())
areaInterface = cr
fmt.Println(areaInterface.Area())
}
再看这个例子
go
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 details: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area())
}
}
在调用 shapes[n].Area() 这个时,只知道 shapes[n] 是一个 Shaper 对象,最后它摇身一变成为了一个 Square 或 Rectangle 对象,并且表现出了相对应的行为
一个标准库的例子
io
包里有一个接口类型 Reader
:
go
type Reader interface {
Read(p []byte) (n int, err error)
}
定义变量 r
: var r io.Reader
那么就可以写如下的代码:
go
var r io.Reader
r = os.Stdin // see 12.1
r = bufio.NewReader(r)
r = new(bytes.Buffer)
f,_ := os.Open("test.txt")
r = bufio.NewReader(f)
上面 r
右边的类型都实现了 Read()
方法,并且有相同的方法签名,r
的静态类型是 io.Reader
。
接口嵌套接口
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法
go
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
类型断言:检测和转换接口变量的类型
一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值
比如可以是这样
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
// Is Square the type of areaIntf?
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of areaIntf is: %T\n", t)
}
if u, ok := areaIntf.(*Circle); ok {
fmt.Printf("The type of areaIntf is: %T\n", u)
} else {
fmt.Println("areaIntf 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
}
类型判断
接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch
go
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)
}
11.6 使用方法集与接口
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 CountInto:
// List does 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) // VALID: Identical receiver type
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
也是可以的,因为指针会被自动解引用。
总结
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P
直接辨识的:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
译注
Go 语言规范定义了接口方法集的调用规则:
- 类型
*T
的可调用方法集包含接受者为*T
或T
的所有方法集 - 类型
T
的可调用方法集包含接受者为T
的所有方法 - 类型
T
的可调用方法集不 包含接受者为*T
的方法
具体例子展示
来看下sort包当中对于接口部分的运用是怎样的:
要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的 Len() 方法、比较第 i 和 j 个元素的 Less(i, j) 方法以及交换第 i 和 j 个元素的 Swap(i, j) 方法
于是可以写出如下所示的代码
go
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)
}
}
}
}
而在这个实现中,在Sorter中实际上就会声明了对应的这些方法
go
type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
所以,这句意味着,假设此时我们要对于一个int类型的数组来进行排序,那么就意味着要在这个int类型的数组上实现对应的接口方法,这样才能让标准库在调用Sorter的时候可以找到对应的方法,例如下所示:
go
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] }
这样的,就可以写出如下的代码,来进行一个合理的接口调用的过程:
go
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)
相同的原理,可以实现其他类型数据的接口调用,这里我们假设自定义一个结构体,根据结构体中的相关字段来进行排序:
go
type test2S struct {
name string
score int
}
type TsArray []test2S
func (ts TsArray) Len() int {
return len(ts)
}
func (ts TsArray) Less(i, j int) bool {
return ts[i].score < ts[j].score
}
func (ts TsArray) Swap(i, j int) {
ts[i], ts[j] = ts[j], ts[i]
}
func test2() {
data := []test2S{{"jack", 80}, {"keven", 90}, {"joe", 70}}
fmt.Println("排序前: ", data)
//sort.Sort(data) 错误的调用,因为Sort的接收值是一个interface变量,所以要通过data创建出它对应的interface变量
sort.Sort(TsArray(data))
fmt.Println("排序后: ", data)
}
运行结果为
text
排序前: [{jack 80} {keven 90} {joe 70}]
排序后: [{joe 70} {jack 80} {keven 90}]
空接口
概念
不包含任何方法,对于实现没有任何要求
go
type Any interface {}
任何其他类型都实现了空接口,可以给一个空接口类型的变量 var val interface {} 赋任何类型的值
这就意味着,空接口支持可以接受任何类型的变量,这在实际的开发中是很有意义的,比如可以产生如下的代码
go
func test3() {
testFunc := func(any interface{}) {
switch v := any.(type) {
case bool:
fmt.Println("bool type", v)
case int:
fmt.Println("int type", v)
case string:
fmt.Println("string type", v)
default:
fmt.Println("other type", v)
}
}
testFunc(1)
testFunc(1.2)
testFunc("hello world")
}
11.9.3 复制数据切片至空接口切片
假设你有一个 myType
类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:
go
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice
可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment
。
原因是它们俩在内存中的布局是不一样的
必须使用 for-range
语句来一个一个显式地赋值:
go
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
通用类型的节点数据结构
假设有现在的场景:
go
type node struct {
next *node
prev *node
data interface{}
}
func test4() {
root := &node{nil, nil, "hello root"}
root.next = &node{nil, root, 10}
root.prev = &node{root, nil, 20}
fmt.Println(root.prev.data, root.data, root.next.data)
}
接口到接口
一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go 语言动态的一面
比如,给出下面的代码
go
type test5S struct {
firstname string
lastname string
}
func (ts *test5S) print2() {
fmt.Println(ts.firstname, ts.lastname)
}
type test5PrintInterface interface {
print1()
}
type test5MyInterface interface {
print2()
}
func t5func(x test5MyInterface) {
if p, ok := x.(test5PrintInterface); ok {
p.print1()
} else {
fmt.Println("error")
}
}
func test5() {
ts := &test5S{"bob", "joe"}
t5func(ts)
}
从这个就能看出问题,对于ts变量来说,他实现了test5MyInterface接口,但是实际上没有实现test5PrintInterface接口的内容,因此这里的转换是失败的,所以就要加一个类似于上面的检测的过程
反射包
来看看反射的概念:
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如:它的大小、它的方法以及它能"动态地"调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用
变量的最基本信息就是类型和值:反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口
两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64,reflect.ValueOf(x) 返回
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:
go
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
接口的值包含一个 type 和 value。
反射可以从接口值反射到对象,也可以从对象反射回接口值。
reflect.Type
和 reflect.Value
都有许多方法用于检查和操作它们。一个重要的例子是 Value
有一个 Type()
方法返回 reflect.Value
的 Type
类型。另一个是 Type
和 Value
都有 Kind()
方法返回一个常量来表示类型:Uint
、Float64
、Slice
等等。同样 Value
有叫做 Int()
和 Float()
的方法可以获取存储在内部的值(跟 int64
和 float64
一样)
下面给出如下的示例代码
go
func test6() {
var f float64
v := reflect.ValueOf(f)
fmt.Println(v)
k := v.Kind()
fmt.Println(k)
fmt.Println(reflect.Float64)
}
通过反射修改(设置)值
先看这个代码
go
func test7() {
var x float64 = 2.3
v := reflect.ValueOf(x)
fmt.Println("can be set?", v.CanSet())
}
这里表示,现在通过反射拿到了x的类型,现在如果想直接进行设置它的值,是不被允许的,原因在于:当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x
所以这里实际上需要的是,使用一个&类型,因此可以改造成这样
go
func test8() {
var x float64 = 2.3
v := reflect.ValueOf(&x)
fmt.Println("can be set?", v.CanSet())
}
但是这样依旧不能设置,这是因为&x的值,相当于是一个float类型的指针,想要在代码中直接对于指针进行设置,很明显是不成功的,所以就要想办法来获取到指针对应的值
所以可以这样进行设置,使用一个Elem函数,这样就会自动来使用指针对应的值
go
func test9() {
var x float64 = 2.3
v := reflect.ValueOf(&x)
v = v.Elem()
fmt.Println("can be set?", v.CanSet())
v.SetFloat(20.1)
fmt.Println(v)
fmt.Println(x)
fmt.Println(v.Interface())
}
反射结构
有些时候需要反射一个结构类型。NumField() 方法返回结构内的字段数量;通过一个 for 循环用索引取得每个字段的值 Field(i)
给出如下的示例代码
go
type test10S1 struct {
s1, s2, s3 string
}
type test10S2 struct {
s1, s2 string
i1 int
}
func (ts test10S1) String() string {
return ts.s1 + "->" + ts.s2 + "->" + ts.s3
}
func (ts test10S2) String() string {
return ts.s1 + "->" + ts.s2 + "->" + strconv.Itoa(ts.i1)
}
func test10Func(s1 interface{}) {
typ := reflect.TypeOf(s1)
val := reflect.ValueOf(s1)
knd := val.Kind()
fmt.Println(typ, val, knd)
for i := 0; i < val.NumField(); i++ {
fmt.Println(i, val.Field(i))
}
}
func test10() {
var s1 interface{} = test10S1{"hello", "go", "s1"}
var s2 interface{} = test10S2{"hello", "hello", 10}
test10Func(s1)
test10Func(s2)
}
但是在这样的情景下,如果要进行修改值的操作,是不被允许的,比如
go
reflect.ValueOf(&ts).Elem().Field(0).SetString("hee")
这是因为,这个结构体当中的字段没有被导出,应该改成大写才能被修改,我们修改结构体为这样:
go
type test10S1 struct {
S1, s2, s3 string
}
type test10S2 struct {
S1, s2 string
i1 int
}
此时再次运行,就好了