1、类型断言(Type Assertion)
接口类型断言的语法形式如下:
i.(TypeName)
i必须是接口变量,如果是具体类型变量,则编译器会报non-interface type xxx on left,TypeName可以是接口类型名,也可以是具体类型名。
接口查询的两层语义
- 如果TypeNname是一个具体类型名,则类型断言用于判断接口变量i绑定的实例类型是否就是具体类TypeName。
- 如果TypeName是一个接口类型名,则类型断言用于判断接口变量i绑定的实例类型是否同时实现了TypeName接口。
接口断言的两种语法表现
直接赋值模式如下:
o := i.(TypeName)
语义分析:
- TypeName是具体类型名,此时如果接口i绑定的实例类型就是具体类型TypeName,则变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本(当然实例可能是
指针值,那就是指针值的副本)。 - TypeName是接口类型名,如果接口i绑定的实例类型满足接口类型TypeName,则变量o的类型就是接口类型TypeName,o底层绑定的具体类型实例是i绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。
- 如果上述两种情况都不满足,则程序抛出panic。
示例如下:
package main
import "fmt"
func main() {
st := &St{"andes"}
var i interface{} = st
//判断i绑定的实例是否实现了接口类型Inter
o := i.(Inter)
o.Ping()
o.Pang()
//如下语句会引发panic,因为i没有实现接口Anter
//p := i.(Anter)
//p.String()
//判断i绑定的实例是否就是具体类型St
s := i.(*St)
fmt.Printf("%s", s.Name)
}
type Inter interface {
Ping()
Pang()
}
type Anter interface {
Inter
String()
}
type St struct {
Name string
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
comma,ok表达式模式如下:
if o, ok := i.(TypeName); ok {
}
语义分析:
- TypeName是具体类型名,此时如果接口i绑定的实例类型就是具体类型TypeName,则ok为true,变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本(当然
实例可能是指针值,那就是指针值的副本)。 - TypeName是接口类型名,此时如果接口i绑定的实例的类型满足接口类型TypeName,则ok为true,变量o的类型就是接口类型TypeName,o底层绑定的具体类型实例是i绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。
- 如果上述两个都不满足,则ok为false,变量o是TypeName类型的“零值”,此种条件分支下程序逻辑不应该再去引用o,因为此时的·没有意义。
示例如下:
package main
import "fmt"
func main() {
st := &St{"andes"}
var i interface{} = st
//判断i绑定的示例是否实现了接口类型Inter
if o, ok := i.(Inter); ok {
o.Ping() //ping
o.Pang() //pang
}
if p, ok := i.(Anter); ok {
//i没有实现接口Anter,所以程序不会执行到这里
p.String()
}
//判断i绑定的实例是否就是具体类型St
if s, ok := i.(*St); ok {
fmt.Printf("%s", s.Name) //andes
}
}
type Inter interface {
Ping()
Pang()
}
type Anter interface {
Inter
String()
}
type St struct {
Name string
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
2、类型查询(Type Switches)
接口类型查询的语法格式如下:
switch v := i.(type) {
case type1:
xxx
case type2:
xxx
default:
xxx
}
语义分析:
接口查询有两层语义,一是查询一个接口变量底层绑定的底层变量的具体类型是什么,二是查询接口变量绑定的底层变量是否还实现了其他接口。
- i必须是接口类型。
具体类型实例的类型是静态的,在类型声明后就不再变化,所以具体类型的变量不存在类型查询,类型查询一定是对一个接口变量进行操作。也就是说,上文中的ⅰ必须是接口变量,如果i是未初始化接口变量,则v的值是nil。例如:
var i io.Reader
switch v := i.(type) { //此处i是为未初始化的接口变量,所以v为nil
case nil:
fmt.Printf("%T\n", v) //nil
default:
fmt.Printf("default")
}
- case字句后面可以跟非接口类型名,也可以跟接口类型名,匹配是按照case子句的顺序进行的。
- 如果case后面是一个接口类型名,且接口变量i绑定的实例类型实现了该接口类型的方法,则匹配成功,ⅴ的类型是接口类型,ⅴ底层绑定的实例是i绑定具体类型实例的副本。例如:
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
defer f.Close()
var i io.Reader = f
switch v := i.(type) {
//i的绑定的实例是*osFile类型,实现了io.ReadWriter接口,所以case匹配成功
case io.ReadWriter:
//v是io.ReadWriter接口类型,所以可以调用Write方法
v.Write([]byte("io.ReadWriter\n"))
//由于上一个case已经匹配,就算这个case也匹配,也不会走到这里
case *os.File:
v.Write([]byte("*os.File\n"))
v.Sync()
default:
return
- 如果case后面是一个具体类型名,且接口变量i绑定的示例类型和该具体类型相同,则匹配成功,此时v就是该具体类型变量,v的值是i绑定的实例值的副本。例如:
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
defer f.Close()
var i io.Reader = f
switch v := i.(type) {
//匹配成功,v的类型就是具体类型*os.File
case *os.File:
v.Write([]byte("*os.File\n"))
v.Sync()
//由于上一个case已经匹配,就算这个case也匹配,也不会走到这里
case io.ReadWriter:
v.Write([]byte("io.ReadWriter\n"))
default:
return
}
- 如果case后面跟着多个类型,使用逗号分隔,接口变量i绑定的实例类型只要和其中一个类型匹配,则直接使用o赋值给V,相当于ⅴ:=o。这个语法有点奇怪,按理说编译
器不应该允许这种操作,语言实现者可能想让type switch语句和普通的switch语句保持一样的语法规则,允许发生这种情况。例如:
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
defer f.Close()
var i io.Reader = f
switch v := i.(type) {
//多个类型,f满足其中任何一个就算匹配
case *os.File, io.ReadWriter:
//此时相当于执行v := i,v和i是等价的,使用v没有意义
if v == i {
fmt.Println(true) //true
}
default:
return
}
- 如果所有的case子句都不满足,则执行default语句,此时执行的仍然是 v:=o,最终v的值是o。此时使用v没有任何意义。
- fallthrough语句不能在Type Switch语句中使用。
注意:Go和很多标准库使用如下的格式:
switch i := i.(type) {
}
这种使用方式存在争议:首先在switch语句块内新声明局部变量i覆盖原有的同名变量i不是一种好的编程方式,其次如果类型匹配成功,则ⅰ的类型就发生了变化,如果没有匹配成功,则ⅰ还是原来的接口类型。除非使用者对这种模糊语义了如指掌,不然很容易出错,所以不建议使用这种方式。
推荐的方式是将i.(ype)赋值给一个新变量:
switch v := i.(type) {
}
类型查询和类型断言
- 类型查询和类型断言具有相同的语义,只是语法格式不同。二者都能判断接口变量绑定的实例的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型。
- 类型查询使用cse字句一次判断多个类型,类型断言一次只能判断一个类型,当然类型断言也可以使用if else if语句达到同样的效果。
3、接口优点和使用形式
接口优点
- 解耦:复杂系统进行垂直和水平的分割是常用的设计手段,在层与层之间使用接口进行抽象和解耦是一种好的编程策略。Go的非侵入式的接口使层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加了接口使用的自由度。
- 实现泛型:由于现阶段Go语言还不支持泛型,使用空接口作为函数或方法参数能够用在需要泛型的场景中。
接口使用形式
接口类型是"第一公民",可以用在任何使用变量的地方,使用灵活,方便解耦,主要使用在如下地方:
- 作为结构内嵌字段。
- 作为函数或方法的形参。
- 作为函数或方法的返回值。
- 作为其他接口定义的嵌入字段。