[golang] interface 和 reflection

309 阅读9分钟

interface

Go 语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来说明对象的行为:如果谁能搞定这件事,它就可以用在这儿。

接口定义了一组方法(方法集),接口里也不能包含变量。(按照约定,只包含一个方法的)接口的名字由方法名加 er 后缀组成,例如 PrinterReaderWriterLoggerConverter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

在 Go 语言中接口可以有值,一个接口类型的变量或一个接口值:var ai Namerai 是一个多字(multiword)数据结构,它的值是 nil。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。

image.png

类型(比如结构体)可以实现某个接口的方法集;这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口所有的方法集。实现了 Namer 接口的类型的变量可以赋值给 ai(即 receiver 的值),方法表指针(method table ptr)就指向了当前的方法实现。当另一个实现了 Namer 接口的类型的变量被赋给 ai,receiver 的值和方法表指针也会相应改变。

类型不需要显式声明它实现了某个接口:接口被隐式地实现。即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。

type Shaper interface {
    Area() float32
}

type Square struct {
    side float32
}

func (sq *Square) Area() float32 {
    return sq.side * sq.side
}

func main() {
    sq1 := new(Square)
    sq1.side = 5

    var areaIntf Shaper
    areaIntf = sq1
    // shorter, without separate declaration:
    // areaIntf := Shaper(sq1)
    // or even:
    // areaIntf := sq1
    fmt.Printf("The square has area: %f\n", areaIntf.Area())
}
The square has area: 25.000000

现在接口变量包含一个指向 Square 变量的引用,通过它可以调用 Square 上的方法 Area()。当然也可以直接在 Square 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。

这是 Go 版本的多态,多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上表现出不同的行为。

type valuable interface {
	getValue() float32
}

type stockPosition struct {
    ticker     string
    sharePrice float32
    count      float32
}

func (s stockPosition) getValue() float32 {
    return s.sharePrice * s.count
}

type car struct {
    make  string
    model string
    price float32
}

func (c car) getValue() float32 {
    return c.price
}

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", 66500}
    showValue(o)
}
Value of the asset is 2308.800049
Value of the asset is 66500.000000

永远不要使用一个指针指向一个接口类型,因为它已经是一个指针。

type nexter interface {
	next() byte
}

func nextFew1(n nexter, num int) []byte {
    var b []byte
    for i := 0; i < num; i++ {
        b[i] = n.next()
    }
    return b
}

func nextFew2(n *nexter, num int) []byte {
    var b []byte
    for i := 0; i < num; i++ {
        // 编译错误:n.next undefined (type *nexter has no field or method next)
        b[i] = n.next()
    }
    return b
}

接口嵌套接口

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock()
}

type File interface {
    ReadWrite
    Lock
    Close()
}

类型断言

if-else

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的动态类型,即运行时在变量中存储的值的实际类型。通常可以使用类型断言来测试在某个时刻 varI 是否包含类型 T 的值。

// varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)
v := varI.(T) // unchecked type assertion

// 更安全的方式是使用以下形式来进行类型断言
if v, ok := varI.(T); ok {  // checked type assertion
    Process(v)
    return
}
// varI is not of type T
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
}
The type of areaIntf is: *main.Square
areaIntf does not contain a variable of type Circle

如果忽略 areaIntf.(*Square) 中的 * 号,会导致编译错误:impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)

type-switch

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough

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

判断值是否实现了某个接口

Print 函数就是如此检测类型是否可以打印自身的。

type Stringer interface {
    String() string
}

if sv, ok := v.(Stringer); ok {
    fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}

总结

和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。

作用于变量上的方法实际上是不区分变量到底是指针还是值的,但是接口变量中存储的具体值是不可寻址的,如果使用不当,编译器会给出错误提示。

type Lener interface {
    Len() int
}

type Appender interface {
    Append(int)
}

type List []int

func (l List) Len() int {
    return len(l)
}

func (l *List) Append(val int) {
    *l = append(*l, val)
}

func CountInto(a Appender, start, end int) {
    for i := start; i <= end; i++ {
        a.Append(i)
    }
}

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

在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P 直接辨识的。

  • 指针方法可以通过指针调用。
  • 值方法可以通过值调用。
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用。
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址。

空接口

空接口或者最小接口不包含任何方法,它对实现不做任何要求。

type Any interface{}

var (
    i   = 5
    str = "ABC"
)

type Person struct {
    name string
    age  int
}

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 has the value: 5
val has the value: ABC
val has the value: &{Rob Pike 55}
Type pointer to Person *main.Person

每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。

复制数据切片至空接口切片

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice

可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment

首先,带有 []interface{} 类型的变量不是接口,它是一个元素类型恰好为 interface{} 的切片。带有 []interface{} 类型的变量在编译时就有一个特定的内存布局。每个 interface{} 使用两个字段表示,因此,长度为 N 且带有 []interface{} 类型的切片将由一个长度为 N * 2 个字段的数据块支持。

这与支持类型为 []MyType 且相同长度的切片的数据块不同,其数据块的长度为 MyType 的大小 * N。

因此,必须使用 for-range 语句一个一个显式地赋值.

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
    interfaceSlice[i] = d
}

接口到接口

一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误。

假定:

var ai AbsInterface // declares method Abs()
type SqrInterface interface {
    Sqr() float32
}
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 doesn’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() // type assertion to myPrintInterface
}

x 转换成 myPrintInterface 类型是完全动态的:只要 x 的底层类型(动态类型)定义了 print 方法这个调用就可以正常运行。译注:若 x 的底层类型未定义 print 方法,此处类型断言会导致 panic,最佳实践应该为 if mpi, ok := x.(myPrintInterface); ok { mpi.print() }

reflection

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式,反射可以在运行时检查类型和变量,例如它的大小、方法和动态的调用这些方法。这对于没有源代码的包尤其有用,这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。

通过反射获取类型和值

变量的最基本信息就是类型和值:两个简单的函数,reflect.TypeOfreflect.ValueOf,返回被检查对象的类型和值。例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64reflect.ValueOf(x) 返回 <float64 Value>

var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value:", v)
fmt.Println("type:", v.Type())
fmt.Println("kind:", v.Kind())
fmt.Println("value:", v.Float())
// panic: reflect: call of reflect.Value.Int on float64 Value
// fmt.Println("int-value:", v.Int())
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)

fmt.Println()

type MyInt int32
var m MyInt = 5
v := reflect.ValueOf(m)
fmt.Println(v.Kind()) // int32
fmt.Println(v.Type()) // main.MyInt
type: float64
value: 3.4
type: float64
kind: float64
value: 3.4
3.4
value is 3.40e+00
3.4

int32
main.MyInt

通过反射修改值

假设要把 x 的值改为 3.1415,但是必须小心使用:v.SetFloat(3.1415)。这将产生一个错误:reflect.Value.SetFloat using unaddressable value。因为 v 不是可设置的(这里并不是说值不可寻址)。是否可设置是 Value 的一个属性,并且不是所有的反射值都有这个属性,可以使用 CanSet() 方法测试是否可设置。

v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x)

通过 Type() 看到 v 现在的类型是 *float64,并且仍然是不可设置的。要想让其可设置,需要使用 Elem() 函数,这间接的使用指针 v = v.Elem(),现在 v.CanSet() 返回 true

var x float64 = 3.4
v := reflect.ValueOf(x)
// setting a value:
// v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable value
fmt.Println("settability of v:", v.CanSet())
v = reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of v:", v.Type())
fmt.Println("settability of v:", v.CanSet())
v = v.Elem()
fmt.Println("The Elem of v is: ", v)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(3.1415) // this works!
fmt.Println(v.Interface())
fmt.Println(v)
fmt.Println(x)
settability of v: false
type of v: *float64
settability of v: false
The Elem of v is:  3.4
settability of v: true
3.1415
3.1415
3.1415

反射结构

type NotknownType struct {
    s1, s2, s3 string
}

func (n NotknownType) String() string {
    return n.s1 + " - " + n.s2 + " - " + n.s3
}

// variable to investigate:
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}

func main() {
    value := reflect.ValueOf(secret) // <main.NotknownType Value>
    typ := reflect.TypeOf(secret)    // main.NotknownType
    // alternative:
    // typ := value.Type()  // main.NotknownType
    fmt.Println(typ)
    knd := value.Kind() // struct
    fmt.Println(knd)

    // iterate through the fields of the struct:
    for i := 0; i < value.NumField(); i++ {
        fmt.Printf("Field %d: %v\n", i, value.Field(i))
        // error: panic: reflect.Value.SetString using value obtained using unexported field
        // value.Field(i).SetString("C#")
    }

    // call the first method, which is String():
    results := value.Method(0).Call(nil)
    fmt.Println(results) // [Ada - Go - Oberon]
}
main.NotknownType
struct
Field 0: Ada
Field 1: Go
Field 2: Oberon
[Ada - Go - Oberon]
type T struct {
    A int
    B string
}

func main() {
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }
    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)
}
0: A int = 23
1: B string = skidoo
t is now {77 Sunset Strip}

总结

Go 没有类,而是松耦合的类型、方法对接口的实现。

OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中是怎样表现的呢?

  • 封装(数据隐藏):类型只拥有自己所在包中定义的方法,和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:
    1. 包范围内的:通过标识符首字母小写,对象只在它所在的包内可见。
    2. 可导出的:通过标识符首字母大写,对象对所在包以外也可见。
  • 继承:用组合实现,内嵌一个(或多个)包含想要的行为(字段和方法。
  • 多态:用接口实现,某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。