七叶笔记 » golang编程 » Golang: Interface in Go(译2)

Golang: Interface in Go(译2)

南京紫金山—美龄宫

很多时候,我们想把一个接口类型的值转换成另外一个值。类型转换发生在编译期,并且在之前的文章中有讨论到。可以简单的举栗说明一下:

 type T1 struct {
    name string
}
type T2 struct {
    name string
}
func main() {
    vs := []interface{}{T2(T1{"foo"}), string(322), []byte("abł")}
    for _, v := range vs {
        fmt.Printf("%v %T\n", v, v)
    }
}  

输出:

 {foo} main.T2
ł string
[97 98 197 130] []uint8  

Go中的赋值语句允许在某些情况下赋给一个变量不同类型的值:

 type T struct {
    name string
}
func main() {
    v1 := struct{ name string }{"foo"}
    fmt.Printf("%T\n", v1) // struct { name string }
    var v2 T
    v2 = v1
    fmt.Printf("%T\n", v2) // main.T
}  

下面我们集中在接口类型的类型转换上,两种新的概念会引入进来:

  • Type Assertion(类型断言)
  • Type Switch

假设我们有两个接口类型值,并且我们希望将其中一个赋值给另外一个:

 type I1 interface {
    M1()
}
type I2 interface {
    M1()
}
type T struct{}
func (T) M1() {}
func main() {
    var v1 I1 = T{}
    var v2 I2 = v1
    _ = v2
}  

这段代码能够很好的运行,因为它满足第三种赋值场景:

T is an interface type and x implements T.

因为“v1”的类型实现了接口“I2”,不而在于这些类型具体是如何构成的。

 type I1 interface {
    M1()
    M2()
}
type I2 interface {
    M1()
    I3
}
type I3 interface {
    M2()
}
type T struct{}
func (T) M1() {}
func (T) M2() {}
func main() {
    var v1 I1 = T{}
    var v2 I2 = v1
    _ = v2
}  

即使“I2”嵌入了其他接口,而“I1”没有嵌入其他接口,它们依然相互满足接口。接口方法的没有顺序。值得注意的是,方法集不需要完全相同:

 type I1 interface {
    M1()
    M2()
}
type I2 interface {
    M1()
}
type T struct{}
func (T) M1() {}
func (T) M2() {}
func main() {
    var v1 I1 = T{}
    var v2 I2 = v1
    _ = v2
}  

基于第三种赋值场景,这样的代码可以很好的工作。类型“I2”的值实现了“I1”,因为它的方法集是“I1”方法集的子集。如果不是这种情况,编译器就立刻反映出来:

 type I1 interface {
    M1()
}
type I2 interface {
    M1()
    M2()
}
type T struct{}
func (T) M1() {}
func main() {
    var v1 I1 = T{}
    var v2 I2 = v1
    _ = v2
}  

上述无法编译通过,并且会报错:

 main.go:18: cannot use v1 (type I1) as type I2 in assignment:
I1 does not implement I2 (missing M2 method)  

目前为止,我们见到了涉及到两个接口类型的情况。当右边的值是具体类型时且它实现了一个接口,第三种赋值依然正确:

 type I1 interface {
    M1()
}
type T struct{}
func (T) M1() {}
func main() {
    var v1 I1 = T{}
    _ = v1
}  

那么,如果我们需要将一个接口类型变量赋值给一个具体类型变量呢?

 type I1 interface {
    M1()
}
type T struct{}
func (T) M1() {}
func main() {
    var v1 I1 = T{}
    var v2 T = v1
    _ = v2
}  

这段代码没办法工作,并且会抛出一个错误:“cannot use v1 (type I1) as type T in assignment”,这种情况,我们需要使用下面要讨论到的“Type assertion”(类型断言)……

编译器能检查转换的正确性。以下场景会发生编译期错误:

  1. 接口类型 -> 具体类型
 type I interface {
    M()
}
type T struct {}
func (T) M() {}
func main() {
    var v I = T{}
    fmt.Println(T(v))
}  

这个会报编译错误“cannot convert v(type I) to type T: need type assertion”。因为编译器没办法知道这种间接的转换是否正确,因为类型实现了接口“I”的值都可以赋给变量“v”。

2. 接口类型 -> 接口类型,当右值的方法集不是左值方法集的子集

 type I1 interface {
    M()
}
type I2 interface {
    M()
    N()
}
func main() {
    var v I1
    fmt.Println(I2(v))
}  

编译器报错:

 main.go:16: cannot convert v (type I1) to type I2:
I1 does not implement I2 (missing N method)  

原因很简单,如果“I2”的方法集是“I1”方法集的子集,那么编译器可以在编译阶了解到。不同的事,这样的转换只能在运行期识别和发生。

不存在严格的转换,但是“Type assertion”和“Type switch”允许检查和获取接口类型值的动态值,甚至转换接口类型值到另一个接口类型的值。

Type Assertion

类型断言表达式语法如下:

 v.(T)  

其中“v”是接口类型,“T”即可以是抽象类型也可以是具体类型。

具体类型

首先我们看看非接口类型:

 type I interface {
    M()
}
type T struct{}
func (T) M() {}
func main() {
    var v1 I = T{}
    v2 := v1.(T)
    fmt.Printf("%T\n", v2) // main.T
}  

在类型断言中指定的类型必须实现“v1”的类型——“I”。编译器会在编译期检查它的正确与否:

 type I interface {
    M()
}
type T1 struct{}
func (T1) M() {}
type T2 struct{}
func main() {
    var v1 I = T1{}
    v2 := v1.(T2)
    fmt.Printf("%T\n", v2)
}  

上述代码无法通过编译期,因为变量“v1”没有持有类型“T2”的任何内容,“T2”没有满足接口“I”并且变量“v1”只能存储实现了“I”接口的类型值。

编译器在程序运行时并不知道变量“v1”内部存储的值。类型断言是一种获取接口类型值的方法,但是如果“v1”的动态类型不匹配“T”呢?

 type I interface {
    M()
}
type T1 struct{}
func (T1) M() {}
type T2 struct{}
func (T2) M() {}
func main() {
    var v1 I = T1{}
    v2 := v1.(T2)
    fmt.Printf("%T\n", v2)
}  

这种情况程序会“panic”:

 panic: interface conversion: main.I is main.T1, not main.T2  

多值转化

类型断言有一种多值转化的语法形式,其中第二个值是一个代表表达式是否成功的布尔值,如果不成功,第一个值将会是类型“T”的零值:

 type I interface {
    M()
}
type T1 struct{}
func (T1) M() {}
type T2 struct{}
func (T2) M() {}
func main() {
    var v1 I = T1{}
    v2, ok := v1.(T2)
    if !ok {
        fmt.Printf("ok: %v\n", ok) // ok: false
        fmt.Printf("%v,  %T\n", v2, v2) // {},  main.T2
    }
}  

这种形式不会发生“panic“,并且第二个布尔值可以用来检查转化是否成功。

接口类型

上述所有例子中用到的都是具体类型。接口类型同样也是被支持的。它检查动态值是否满足具体的接口并返回这个接口类型值。不同于普通的类型转换,传递给类型断言的接口方法集不必是类型“v”的子集:

 type I1 interface {
    M()
}
type I2 interface {
    I1
    N()
}
type T struct{
    name string
}
func (T) M() {}
func (T) N() {}
func main() {
    var v1 I1 = T{"foo"}
    var v2 I2
    v2, ok := v1.(I2)
    fmt.Printf("%T %v %v\n", v2, v2, ok) // main.T {foo} true
}  

如果接口不满足,那么接口的零值会被返回:

 type I1 interface {
    M()
}
type I2 interface {
    N()
}
type T struct {}
func (T) M() {}
func main() {
    var v1 I1 = T{}
    var v2 I2
    v2, ok := v1.(I2)
    fmt.Printf("%T %v %v\n", v2, v2, ok) // <nil> <nil> false
}  

接口类型的类型断言单值表达式也是被支持的。

nil

当“v”是nil时,那类型断言总是会失败,无论“T”是接口类型还是具体类型。

 type I interface {
    M()
}
type T struct{}
func (T) M() {}
func main() {
    var v1 I
    v2 := v1.(T)
    fmt.Printf("%T\n", v2)
}  

上述代码会“panic”:

 panic: interface conversion: main.I is nil, not main.T  

这里如果使用多值表达式可以防止程序“panic”。

Type switch

类型断言是一种检查一个接口类型值是否实现了具体接口或是否这个接口类型指等同于传递的具体类型的方法。如果我们需要对一个变量进行一系列这样的测试,Go提供了一个比做一系列类型断言更加紧凑的语法结构——“switch”:

 type I1 interface {
    M1()
}
type T1 struct{}
func (T1) M1() {}
type I2 interface {
    I1
    M2()
}
type T2 struct{}
func (T2) M1() {}
func (T2) M2() {}
func main() {
    var v I1
    switch v.(type) {
    case T1:
            fmt.Println("T1")
    case T2:
            fmt.Println("T2")
    case nil:
            fmt.Println("nil")
    default:
            fmt.Println("default")
    }
}  

它的语法很像类型断言,但这里了用到了关键字“type”。因为接口类型值是“nil”,这段代码的输出是“nil”,那么如果我们给“v”赋值呢:

 var v I1 = T2{}  

程序会输出“T2”。“Type switch”对接口类型依然有效:

 var v I1 = T2{}
switch v.(type) {
case I2:
        fmt.Println("I2")
case T1:
        fmt.Println("T1")
case T2:
        fmt.Println("T2")
case nil:
        fmt.Println("nil")
default:
        fmt.Println("default")
}  

程序会打印“I2”。如果多个匹配的情况都是符合的,那么第一个将会是真正的匹配值;如果没有匹配项,那么什么也不会发生:

 type I interface {
    M()
}
func main() {
    var v I
    switch v.(type) {
    }
}  

这个程序不会“panic”——程序会成功执行到底。

一个匹配项有多个类型

一个“switch”匹配项可以指定多个用逗号分隔的类型。这样如果存在可合并的分支,可以在一定程度减少重复代码。

 type I1 interface {
    M1()
}
type T1 struct{}
func (T1) M1() {}
type T2 struct{}
func (T2) M1() {}
func main() {
    var v I1 = T2{}
    switch v.(type) {
    case nil:
            fmt.Println("nil")
    case T1, T2:
            fmt.Println("T1 or T2")
    }
}  

这里输出“T1 or T2”。(“and good since the dynamic type of v at the moment when guard is evaluated is T2 ”)

默认分支

默认分支很像其他语言中的“switch”,它会在没有任何匹配时作用到:

 var v I
switch v.(type) {
default:
        fmt.Println("fallback")
}  

短变量声明

目前为止我们看到了“Type switch”,其中用到了“guard”的语法:

 v.(type)  

“v”是一个像变量标识的表达式,除此之外,短变量声明也可以在这里使用:

 var p *T2
var v I1 = p
switch t := v.(type) {
case nil:
         fmt.Println("nil")
case *T1:
         fmt.Printf("%T is nil: %v\n", t, t == nil)
case *T2:
         fmt.Printf("%T is nil: %v\n", t, t == nil)
}  

它输出“*main.T2 is nil: true”,所以“t’”的类型就是匹配到的分支上的类型。如果一个分支上有多个匹配值,那么“t”的类型和“v”一致:

 var p *T2
var v I1 = p
switch t := v.(type) {
case nil:
         fmt.Println("nil")
case *T1, *T2:
         fmt.Printf("%T is nil: %v\n", t, t == nil)
}  

这段代码输出“*main.T2 is nil: false”。变量“t”是接口类型,因为它不是“nil”而是一个指定“nil”的指针。

重复

匹配分支中的类型必须唯一:

 switch v.(type) {
case nil:
    fmt.Println("nil")
case T1, T2:
    fmt.Println("T1 or T2")
case T1:
    fmt.Println("T1")
}  

编译这段代码会报错:“duplicate case T1 in type switch”。

可选的单一声明

“Guard”可以发生在像短变量声明这样的单一声明之前:

 var v I1 = T1{}
switch aux := 1; v.(type) {
case nil:
    fmt.Println("nil")
case T1:
    fmt.Println("T1", aux)
case T2:
    fmt.Println("T2", aux)
}  

这段代码输出“T1 1”。另外单一声明可以被使用到无论“guard”是否在短变量声明的形式中。

相关文章