接口
定义
在传统的面向对象的语言中,是会存在类和继承的概念的,但是Go并没有
那Go如何实现类似的方法呢?它提供了接口的概念,可以实现很多面向对象的特性
接口定义会实现一组方法集,但是这些方法不包含实现的代码,他们是抽象的概念,接口里也不能有变量
用如下的方式来定义接口
type Namer interface {Method1(param_list) return_typeMethod2(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 的值和方法表指针也会相应改变
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口
实现某个接口的类型(除了实现接口方法外)可以有其他的方法
一个类型可以实现多个接口
比如以下面的代码为定义
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 test1ShaperareaInterface = sqfmt.Println(areaInterface.Area())areaInterface = crfmt.Println(areaInterface.Area())
}
再看这个例子
package mainimport "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 valueq := &Square{5} // Area() of Square needs a pointer// shapes := []Shaper{Shaper(r), Shaper(q)}// or shortershapes := []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
:
type Reader interface {Read(p []byte) (n int, err error)
}
定义变量 r
: var r io.Reader
那么就可以写如下的代码:
var r io.Readerr = os.Stdin // see 12.1r = 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() 方法
type ReadWrite interface {Read(b Buffer) boolWrite(b Buffer) bool
}type Lock interface {Lock()Unlock()
}type File interface {ReadWriteLockClose()
}
类型断言:检测和转换接口变量的类型
一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值
比如可以是这样
package mainimport ("fmt""math"
)type Square struct {side float32
}type Circle struct {radius float32
}type Shaper interface {Area() float32
}func main() {var areaIntf Shapersq1 := new(Square)sq1.side = 5areaIntf = 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
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 使用方法集与接口
package mainimport ("fmt"
)type List []intfunc (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 valuevar 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 typefmt.Printf("- lst is long enough\n")}// A pointer valueplst := new(List)CountInto(plst, 1, 10) // VALID: Identical receiver typeif LongEnough(plst) {// VALID: a *List can be dereferenced for the receiverfmt.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) 方法
于是可以写出如下所示的代码
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中实际上就会声明了对应的这些方法
type Sorter interface {Len() intLess(i, j int) boolSwap(i, j int)
}
所以,这句意味着,假设此时我们要对于一个int类型的数组来进行排序,那么就意味着要在这个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)
相同的原理,可以实现其他类型数据的接口调用,这里我们假设自定义一个结构体,根据结构体中的相关字段来进行排序:
type test2S struct {name stringscore int
}type TsArray []test2Sfunc (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)
}
运行结果为
排序前: [{jack 80} {keven 90} {joe 70}]
排序后: [{joe 70} {jack 80} {keven 90}]
空接口
概念
不包含任何方法,对于实现没有任何要求
type Any interface {}
任何其他类型都实现了空接口,可以给一个空接口类型的变量 var val interface {} 赋任何类型的值
这就意味着,空接口支持可以接受任何类型的变量,这在实际的开发中是很有意义的,比如可以产生如下的代码
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
类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:
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
}
通用类型的节点数据结构
假设有现在的场景:
type node struct {next *nodeprev *nodedata 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 语言动态的一面
比如,给出下面的代码
type test5S struct {firstname stringlastname 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) 返回
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:
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
一样)
下面给出如下的示例代码
func test6() {var f float64v := reflect.ValueOf(f)fmt.Println(v)k := v.Kind()fmt.Println(k)fmt.Println(reflect.Float64)
}
通过反射修改(设置)值
先看这个代码
func test7() {var x float64 = 2.3v := reflect.ValueOf(x)fmt.Println("can be set?", v.CanSet())
}
这里表示,现在通过反射拿到了x的类型,现在如果想直接进行设置它的值,是不被允许的,原因在于:当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x
所以这里实际上需要的是,使用一个&类型,因此可以改造成这样
func test8() {var x float64 = 2.3v := reflect.ValueOf(&x)fmt.Println("can be set?", v.CanSet())
}
但是这样依旧不能设置,这是因为&x的值,相当于是一个float类型的指针,想要在代码中直接对于指针进行设置,很明显是不成功的,所以就要想办法来获取到指针对应的值
所以可以这样进行设置,使用一个Elem函数,这样就会自动来使用指针对应的值
func test9() {var x float64 = 2.3v := 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)
给出如下的示例代码
type test10S1 struct {s1, s2, s3 string
}type test10S2 struct {s1, s2 stringi1 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)
}
但是在这样的情景下,如果要进行修改值的操作,是不被允许的,比如
reflect.ValueOf(&ts).Elem().Field(0).SetString("hee")
这是因为,这个结构体当中的字段没有被导出,应该改成大写才能被修改,我们修改结构体为这样:
type test10S1 struct {S1, s2, s3 string
}type test10S2 struct {S1, s2 stringi1 int
}
此时再次运行,就好了