Go语言核心编程第4章 “接口”
Go语言核心编程第4章 “接口”
接口是一个编程规约,也是一组方法签名的集合。
Go的接口是非侵入式的设计,一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法集是接口方法集的超集,就代表该类型实现了接口。
编译器在编译时会进行方法集的校验。
接口没有具体实现逻辑,不能定义字段。
"实例"代表具体类型的变量。
空接口方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。
注:非命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。
Go接口分为接口字面量和接口命名类型
接口字面量声明语法如下
interface{
MethodSignature1
MethodSignature2
}
接口命名类型使用type关键字声明
type InterfaceName interface{
MethodSignature1
MethodSignature2
}
接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是两种的混合。
接口支持嵌入匿名接口字段,编译器会自动进行展开处理
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
方法声明 = 方法名 + 方法签名
MethodName (InputTypeList)OutputTypeList
Go编译器做接口匹配判断时严格校验方法名称和方法签名
声明新接口类型的特点
1.接口的命名一般以"er"结尾
2.接口定义的内部方法声明不需要func引导
3.在接口定义中,只有方法声明没有方法实现
单纯声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。
接口作为一个胶水层或抽象层,起到抽象和适配的作用。
没有初始化的接口变量,其默认值是nil。
var i io.Reader
fmt.Printf("%T\n",i) //
接口初始化是指接口绑定具体类型的实例
两种实例化方法
第一种:实例赋值接口
如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会做静态的类型检查。接口被初始化后,调用接口就相当于调用绑定的具体类型的方法。
第二种:接口变量赋值接口变量
已经初始化的接口类型变量a直接赋值给另一个接口变量b,要求b的方法集是a的方法集的子集。此时go编译器会在编译时进行方法集静态检查。
file,_ := os.OpenFile(".txt", os.O_RDWR|os.O_CREATE,0755)
var rw io.ReadWriter = file
var w io.Writer = rw
接口方法调用和普通的函数调用有区别
接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。
接口方法调用有一定的运行时开销。
直接调用未被初始化的接口变量的方法会产生panic。
package main
type Printer interface {
Print()
}
type S struct{}
func (s S) Print() {
println("println")
}
func main() {
var i Printer
//i.Print()
//会产生panic
i = S{}
i.Print()
}
动态类型
接口绑定的具体实例的类型称为接口的动态类型。
接口可以绑定不同的实例,所以接口的动态类型是随着其绑定的不同类型的实例而发生变化的。
静态类型
接口被定义时,其类型就已经被确定,这个类型叫做接口的静态类型。
静态类型的本质特征是接口方法签名的集合。
两个接口如果方法签名集合相同,则这两个接口语义上完全等价,不需要强制类型转换就可以互相赋值。
a接口方法集合A,b接口方法集合B。如果B是A的子集,则a接口的变量可以直接赋值给B的接口变量。
需要判断已经初始化的接口变量绑定的具体实例是什么类型,以及这个具体实例是否还实现了其他接口。
接口类型断言的语法
i.(TypeName)
i必须是接口变量,如果是具体类型变量会报错。
TypeName可以是接口类型名,也可以是具体类型名。
接口查询的两层含义
1.如果TypeName是一个具体类型名,则判断接口变量i绑定的实例类型是否就是TypeName
2.如果TypeName是一个接口类型名,则判断接口变量i绑定的实例类型是否同时实现了TypeName接口
o := i.(TypeName)
如果TypeName是一个具体类型名,此时如果接口变量i绑定的实例类型就是TypeName,则o类型是TypeName,值是副本。
如果TypeName是一个接口类型名,此时如果接口变量i绑定的实例类型满足TypeName接口类型,则o类型是TypeName,值是i绑定的实例的副本。
不满足上述情况,报panic
package main
import "fmt"
type Inter interface {
Ping()
Pang()
}
type St struct {
Name string
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
type Anter interface {
Inter
string()
}
func main() {
st := &St{"andes"}
var i interface{} = st
判断i绑定的实例是否实现了接口类型Inter
o := i.(Inter)
o.Ping()
o.Pang()
抛出panic,因为没有实现Anter这个接口
p := i.(Anter)
p.string()
判断i绑定的实例是否就是具体类型St
s := i.(*St)
fmt.Println("%s", s.Name)
}
采用下面这种结构,判断是否满足上述两种语义之一,不满足不进行后续的操作
if o,ok := i.(TypeName);ok {
}
---------------------------------
package main
import "fmt"
type Inter interface {
Ping()
Pang()
}
type St struct {
Name string
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
type Anter interface {
Inter
string()
}
func main() {
st := &St{"andes"}
var i interface{} = st
//判断i绑定的实例是否实现了接口类型Inter
if o, ok := i.(Inter) ; ok {
o.Ping()
o.Pang()
}
//生成panic
if p, ok := i.(Anter) ; ok {
p.string()
}
//判断i绑定的实例是否就是具体类型St
if s, ok := i.(*St) ; ok {
fmt.Println("%s", s.Name)
}
}
接口类型查询语法格式如下:
switch v := i.(type)
case type1:
xxx
case type2:
xxx
default:
xxx
接口查询两层语义
一是查询一个接口变量底层绑定的底层变量的具体类型是什么
二是查询一个接口变量是否实现了其他接口
var i Reader
i是未被初始化的接口变量,所以v是nil
switch v := i.(type)
case nil:
fmt.Println("%T\n",v) //
default:
fmt.Println("%T\n","default")
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接口类型,可以调用Writer方法
v.Write([]byte("io.ReadWriter\n"))
由于上一个已经匹配,不会走到这里
case *os.File:
v.Write([]byte("os.File\n"))
v.Sync()
default:
return
}
情况2:如果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
}
情况3:如果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
}
情况4:如果所有的case子句都不满足,则执行default语句,此时执行的仍然是 v:=o,最终v的值是o。此时使用v没有任何意义。同时,fallthrough语句不能在Type Switch语句中使用。对于非Type Switch的Switch语句,拥有fallthrough的case的下一个case无需进行判断匹配即可执行,只能穿透一次
Go和很多标准库使用如下的格式:
switch i := i.(type) {
}
这种使用方式存在争议:
首先:在switch语句块内新声明局部变量i覆盖原有的同名变量i不是一种好的编程方式,
其次:如果类型匹配成功,则i的类型就发生了变化,如果没有匹配成功,则i还是原来的接口类型。
容易出错,所以不建议使用这种方式。
推荐的方式是将i.(ype)赋值给一个新变量:
switch v := i.(type) {
}
总结
类型查询和类型断言具有相同的语义,只是语法格式不同。二者都能判断接口变量绑定的实例的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型。类型查询使用case字句一次判断多个类型,类型断言一次只能判断一个类型,当然类型断言也可以使用if else if语句达到同样的效果。
接口优点:
1.解耦:复杂系统进行垂直和水平的分割是常用的设计手段。Go非侵入式的接口使得层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加量接口使用的自由度。
2.实现泛型:使用空接口作为函数或方法参数能够用在需要泛型的场景中。
接口使用方式:
1.作为结构内嵌字段
2.作为函数或方法的形参
3.作为函数或方法的返回值
4.作为其他接口定义的嵌入字段
没有任何方法的接口,interface{}。
系统中任何类型都符合空接口的要求,类似于Java中的Object。不同之处在于Go中的基本类型也符合空接口。
空接口和泛型
Go语言没有泛型,如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型
func Fprint(w io.Writer,a ...interface{}) (n int,err error)
空接口和反射
空接口是反射实现的基础,反射库就是将相关的具体类型转换并赋值给空接口后才去处理。
空接口不是真的空,接口有类型和值两个概念。
package main
import "fmt"
type Inter interface {
Ping()
Pang()
}
type St struct{}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
func main() {
var st *St = nil
var it Inter = st
fmt.Printf("%p\n",st) 0x0
fmt.Printf("%p\n",it) 0x0
if it != nil { //true
it.Pang()
//it.Ping()方法转换为函数调用,第一个参数是Str类型,由于*Str是空,无法获取指针所指的对象值。导致panic
//也就是说,对于Ping来说,传递指针时,会取*,再进行操作。现在传递的为空,所以报错
}
}
空接口有两个字段,一个是值,一个是绑定实例的指针。两个均为空,空接口才为空。
iface数据结构
src/runtime/runtime2.go
type iface struct {
tab *itab //itab存放类型及方法指针信息
data unsafe.Pointer
}
itab:用来存放接口自身类型和绑定的实例类型及实例相关的函数指针
数据指针data:指向接口绑定的实例的副本,接口的初始化也是一种值拷贝
src/runtime/runtime2.go
type itab struct {
inter是一个指向接口类型元信息的指针
inter *interfacetype
_type是一个指向接口存放的具体类型元信息的指针
_type *_type
hash是具体类型的哈希值,_type里面也有hash值,冗余存放为了接口断言或者接口查询时的快速使用
hash uint32
_ [4]byte
fun是一个指向方法表的指针数组,指向的是具体类型实现接口方法的地址。
fun [1]uintptr
}
itab是非空接口实现动态调用的基础,itab信息被编译器和链接器保存,存放在可执行文件的只读存储段(.rodata)。itab存放在静态分配的存储空间中,不受GC的限制,其内存不会被回收。
src/runtime/type.go
Go语言为每种类型维护一个类型的元信息,这个元信息在运行和反射时都会用到
Go语言的类型元信息通用结构是_type,其他类型都是以_type为内嵌字段封装而成的结构体
type _type struct {
size uintptr //大小
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 //类型Hash
tflag tflag //类型的特征标记
align uint8 //_type作为整体变量存放时的对齐字节数
fieldAlign uint8 //当前结构字段的对齐字节数
kind uint8 //基础类型枚举值和反射中的Kind一致,kind决定了如何解析该类型
alg *typeAlg //指向一个函数指针表,该表有两个函数,一个是计算类型的Hash函数,一个是比较两个类型是否相同的equal函数
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte // GC相关信息
str nameOff //str用来表示类型名称字符串在编译后二进制文件中某个section的偏移量,由链接器负责填充
ptrToThis typeOff //ptrToThis用来表示类型元信息的指针在编译后二进制文件中某个section的偏移量,由链接器负责填充
}
_type包含所有类型的共同元信息,编译器和运行时可以根据该元信息解析具体类型,类型名存放位置,类型的hash值等基本信息。
_type里面的nameOff和typeOff最终由链接器负责确定和填充,它们都是一个偏移量(offset),类型的名称和类型元信息实际上存放在连接后可执行文件的某个段(section)里,这两个值是相对于段内的偏移量,运行时提供两个转换查找函数。如下:
// 获取_type的name
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
//获取_type的副本
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
Go语言类型元信息最初由编译器负责构建,并以表的形式存放在编译后的对象文件中,再由链接器在链接时进行段合并、符号重定向(填充某些值)。这些类型信息在接口的动态调用和反射中被运行时引用。
描述接口的类型
type interfacetype struct {
typ _type //类型通用部分
pkgpath name //接口所属包的名字信息,name内存放名称和描述信息
mhdr []imethod //接口的方法
}
接口方法元信息
type imethod struct {
name nameOff
ityp typeOff
}
接口动态调用过程有两部分多余时耗:
一个是接口实例化的过程,即iface结构建立的过程,一旦实例化后,这个接口和具体类型的itab数据结构是可以复用的;
另一个是接口的方法调用,它是一个函数指针的间接调用。
同时需要考虑接口调用是一种动态的计算后的跳转调用,会导致CPU缓存失效和分支预测失败。
每次接口调用大约会比直接调用慢0.2ns
空接口只关心存放的具体类型是什么,具体类型的值是什么
type eface struct {
_type *_type
data unsafe.Pointer
}
从eface的数据结构上看,空接口不是真的为空,其保留了具体实例的类型和值拷贝,即便存放的具体类型是空的,空接口也不是空的。由于空接口自身没有方法集,所有空接口实例化后的真正用途不是接口方法的动态调用
空接口在Go语言中的真正意义是支持多态。