Go语言核心编程第4章 “接口”

学习目标:

Go语言核心编程第4章 “接口”

学习内容:

Go语言核心编程第4章 “接口”

第4章 接口

4.1 基本概念

接口是一个编程规约,也是一组方法签名的集合。
Go的接口是非侵入式的设计,一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法集是接口方法集的超集,就代表该类型实现了接口
编译器在编译时会进行方法集的校验。
接口没有具体实现逻辑,不能定义字段。
"实例"代表具体类型的变量。
空接口方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。
注:非命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。

4.1.1 接口声明

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.在接口定义中,只有方法声明没有方法实现

4.1.2 接口初始化

单纯声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。
接口作为一个胶水层或抽象层,起到抽象和适配的作用。
没有初始化的接口变量,其默认值是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
4.1.3 接口方法调用

接口方法调用和普通的函数调用有区别
接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。
接口方法调用有一定的运行时开销。
直接调用未被初始化的接口变量的方法会产生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()
}
4.1.4 接口的动态类型和静态类型

动态类型
接口绑定的具体实例的类型称为接口的动态类型。
接口可以绑定不同的实例,所以接口的动态类型是随着其绑定的不同类型的实例而发生变化的。
静态类型
接口被定义时,其类型就已经被确定,这个类型叫做接口的静态类型。
静态类型的本质特征是接口方法签名的集合。
两个接口如果方法签名集合相同,则这两个接口语义上完全等价,不需要强制类型转换就可以互相赋值。
a接口方法集合A,b接口方法集合B。如果B是A的子集,则a接口的变量可以直接赋值给B的接口变量。

4.2 接口运算

需要判断已经初始化的接口变量绑定的具体实例是什么类型,以及这个具体实例是否还实现了其他接口。

4.2.1 类型断言
接口类型断言的语法
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)
	}
}
4.2.2 类型查询

接口类型查询语法格式如下

switch v := i.(type)
	case type1:
		xxx
	case type2:
		xxx
	default:
		xxx

接口查询两层语义
一是查询一个接口变量底层绑定的底层变量的具体类型是什么
二是查询一个接口变量是否实现了其他接口

  • 注意点1i必须是接口类型(因为具体类型的实例是静态的,声明后不变化,所以具体类型不存在类型查询)。
var i Reader
i是未被初始化的接口变量,所以v是nil
switch v := i.(type)
case nil:
	fmt.Println("%T\n",v) //
default:
	fmt.Println("%T\n","default")
  • 注意点2case后面可以跟非接口类型名,也可以跟接口类型名,匹配按照case子句的顺序进行。
    情况1:如果case后面跟着接口类型名,且接口变量i绑定的实例实现了该接口类型,匹配成功。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) {
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语句达到同样的效果。

4.2.3 接口优点和使用方式

接口优点:
1.解耦:复杂系统进行垂直和水平的分割是常用的设计手段。Go非侵入式的接口使得层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加量接口使用的自由度。
2.实现泛型:使用空接口作为函数或方法参数能够用在需要泛型的场景中。
接口使用方式
1.作为结构内嵌字段
2.作为函数或方法的形参
3.作为函数或方法的返回值
4.作为其他接口定义的嵌入字段

4.3 空接口

4.3.1 基本概念

没有任何方法的接口,interface{}。
系统中任何类型都符合空接口的要求,类似于Java中的Object。不同之处在于Go中的基本类型也符合空接口。

4.3.2 空接口的用途

空接口和泛型
Go语言没有泛型,如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型

func Fprint(w io.Writer,a ...interface{}) (n int,err error)

空接口和反射
空接口是反射实现的基础,反射库就是将相关的具体类型转换并赋值给空接口后才去处理。

4.3.3 空接口和nil

空接口不是真的空,接口有类型和值两个概念。

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来说,传递指针时,会取*,再进行操作。现在传递的为空,所以报错
	}
}

空接口有两个字段,一个是值,一个是绑定实例的指针。两个均为空,空接口才为空。

4.4 接口内部实现

4.4.1 非空接口数据结构
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
}
4.3.2 接口调用代价

接口动态调用过程有两部分多余时耗:
一个是接口实例化的过程,即iface结构建立的过程,一旦实例化后,这个接口和具体类型的itab数据结构是可以复用的;
另一个是接口的方法调用,它是一个函数指针的间接调用。
同时需要考虑接口调用是一种动态的计算后的跳转调用,会导致CPU缓存失效和分支预测失败。
每次接口调用大约会比直接调用慢0.2ns

4.3.3 空接口的数据结构
空接口只关心存放的具体类型是什么,具体类型的值是什么
type eface struct {
	_type *_type
	data  unsafe.Pointer
}

从eface的数据结构上看,空接口不是真的为空,其保留了具体实例的类型和值拷贝,即便存放的具体类型是空的,空接口也不是空的。由于空接口自身没有方法集,所有空接口实例化后的真正用途不是接口方法的动态调用
空接口在Go语言中的真正意义是支持多态。

你可能感兴趣的:(golang,开发语言,后端)