第四章笔记总结

Kesa...大约 8 分钟golang

1. 函数调用

Go 语言函数调用:

  1. 函数参数按照从右至左的顺序入栈
  2. 函数返回值存储在

C 和 Go 在函数调用上的差异:

  1. C 使用寄存器传递参数,使用寄存器传递返回值,无法返回多个返回值
  2. Go 使用传递参数和返回值,可以返回多个返回值

1.2 参数传递

Golang 的参数传递采用值传递,即会对参数进行拷贝。

2. 接口

2.1 定义及其优点

在计算机科学中,接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息。

使用接口可以:

  1. 解耦有依赖关系的上下游
  2. 隐藏底层实现,减少关注点

2.2 隐式实现

Golang 的接口的实现是隐式的,实现接口定义的所有方法即可实现该接口,无需显式声明。

2.3 类型

Golang 的接口有两种:

  1. 空接口interface{},不含任何方法,运行时数据结构为 runtime.efaceopen in new window
  2. 接口,包含方法,运行时数据结构为 runtime.ifaceopen in new window

空接口不是任意类型,若将变量赋值给空接口将发生隐式的类型转换

2.4 接口和方法集

将变量经过类型转换成接口类型后,根据原变量类型的不同(值类型或指针类型)能调用的方法集不同。

原变量类型\方法接收者指针
不可
指针
  • 原变量类型为指针,转换成接口类型后,接口持有的是原变量的指针,可以调用接收者为类型(自动解引用)的方法和指针类型的方法
  • 原变量类型为,转换成接口类型后,接口持有的是原变量的值的拷贝,可以调用接收者为值类型的方法,不能调用指针类型的方法(已不可寻址)

根本原因在于,Golang 的函数参数使用值传递,当原变量类型是值类型时,接口变量持有的是一份拷贝,在调用指针方法时无法获取原始的变量地址。

golang-interface-method-receive
golang-interface-method-receive

2.5 数据结构

空接口

runtime.efaceopen in new window

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
  • _type :指向表示类型的结构
  • data:指向底层数据

接口

runtime.ifaceopen in new window

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
  • tab: 指向表示类型的结构
  • data:指向底层数据

runtime._type

runtime._typeopen in new window是Golang 类型的运行时表示:

type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}
  • size:类型占用的内存大小
  • hash:用于快速判断类型是否相同
  • equal:用于比较相同类型的变量是否相等

runtime.itab

runtime.itabopen in new window是接口类型的核心组成部分:

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32
	_     [4]byte
	fun   [1]uintptr
}
  • inter:指向表示接口类型的结构
  • _type:指向表示具体类型的结构
  • hash_type.hash的拷贝,在惊醒类型转换时用于快速判断目标类型和具体类型
  • fun:动态大小的数组,存储一组函数指针;声明类型为数组,但在使用时会根据指针获取数据,所以是动态的。

2.6 类型转换

指针 -> 接口

var c Duck = &Cat{Name: "draven"}
c.Quack()

以上述代码为例(Cat 实现了 Duck 接口,方法实现为指针方法)

将指针类型的变量转换成接口类型的流程:

  1. 结构体 Cat 在上初始化,并将指针存放在
  2. 进行类型转换,将 Cat 转换成 Duck 类型
  3. 调用接口方法

完成类型转换之后的栈内容如下:

golang-struct-pointer-to-iface
golang-struct-pointer-to-iface

值 -> 接口

var c Duck = Cat{Name: "draven"}
c.Quack()

以上述代码为例(Cat 实现了 Duck 接口,方法实现为值类型方法)

将值类型变量转换成接口类型的流程如下:

  1. 上初始化结构体 Cat
  2. 进行类型转换,调用函数runtime.convT2Iopen in new window,生成runtime.ifaceopen in new window结构体,其中iface.data会在上初始化,并将栈上的 Cat 的内容拷贝过去
  3. 调用接口方法

完成类型转换之后的栈内容如下:

golang-struct-to-iface
golang-struct-to-iface

2.7 类型断言

类型断言是将接口类型转换具体类型

非空接口

var c Duck = &Cat{Name: "draven"}
	switch c.(type) {
	case *Cat:
		cat := c.(*Cat)
		cat.Quack()
}

type-switch为例,类型断言并调用方法的流程如下:

  1. 获取目标类型的_type.hash
  2. 和接口中的iface.itab.hash比较
  3. 若相同:
    1. 获取iface.data,即*Cat指针
    2. 调用接口方法
  4. 若不同,则直接返回

空接口

var c interface{} = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
	cat := c.(*Cat)
	cat.Quack()
}

空接口和接口的流程类似,不过比较类型采用的是eface._type.hash

2.8 动态派发

动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程。

var c Duck = &Cat{Name: "draven"}
c.Quack()

以上述代码为例,调用c.Quack()的流程如下:

  1. 进行类型转换
  2. 调用接口方法Quack,此时会在iface.itab.fun中寻找对应的函数指针并调用

iface.itab.fun,即函数列表中查找需要的目标函数就是动态派发的过程。

性能

在关闭编译器优化的情况下,使用接口调用方法(动态派发)相较于直接调用,会有~18%左右的额外开销。

开启编译器优化之后,会降至~5%,此时和接口带来的好处相比往往可以忽略。

值类型和指针类型变量调用接口方法性能比较

func BenchmarkDirectCall(b *testing.B) {
	c := Cat{Name: "draven"}
	for n := 0; n < b.N; n++ {
		c.Quack()
	}
}

func BenchmarkDynamicDispatch(b *testing.B) {
	c := Duck(Cat{Name: "draven"})
	for n := 0; n < b.N; n++ {
		c.Quack()
	}
}
直接调用动态派发
指针~3.03ns~3.58ns
~3.09ns~6.98ns

值类型的接口方法调用要比指针类型额外消耗~125%的时间。

主要原因依然在于Golang 的参数传递是值传递,在值的(特别是大型结构体,数组等)的拷贝过程中可能会有大量的性能损耗。

3. 反射

反射包reflectopen in new window有两个主要的函数:

  1. reflect.TypeOfopen in new window 接口类型,用于获取类型信息
  2. reflect.ValueOfopen in new window 结构体类型,用于获取数据的运行时表示

3.1 反射的三大法则

反射可以作为元编程方式减少代码,但是过多的反射会使得程序逻辑难以理解并运行缓慢。

Golang 的反射由三大法则:

  1. interface{}变量可以反射出反射对象
  2. 从反射对象可以获取interface{}变量
  3. 要修改反射对象,其值必须可以设置

interface{}中可以反射出反射对象

reflect.TypeOfopen in new windowreflect.ValueOfopen in new window入参都是interface{}类型,在方法调用的过程中,会发生隐式的类型转换

golang-interface-to-reflection
golang-interface-to-reflection

从反射对象可以获取interface{}变量

reflect.Value.Interfaceopen in new window可以将反射变量转换成interface{}变量。

golang-reflection-to-interface
golang-reflection-to-interface
v := reflect.ValueOf(1)
v.Interface().(int)
golang-bidirectional-reflection
golang-bidirectional-reflection

要修改反射对象,其值必须可以设置

func main() {
	i := 1
	v := reflect.ValueOf(i)
	v.SetInt(10)
	fmt.Println(i)
}

$ go run reflect.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

因为 Golang 的参数都是值传递,那么得到的反射对象和原变量没有任何关系,无法修改原变量。

若想要修改原值:

func main() {
	i := 1
	v := reflect.ValueOf(&i)
	v.Elem().SetInt(10)
	fmt.Println(i)
}

$ go run reflect.go
10
  1. reflect.ValueOfopen in new window传入变量的指针
  2. reflect.Value.Elemopen in new window获取指针指向的变量
  3. reflect.Value.SetIntopen in new window更新变量的值

3.2 类型和值

interface{}在反射包中使用reflect.emptyInterfaceopen in new window表示:

type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}
  • typ:表示具体类型
  • word:表示底层结构

reflect.TypeOfopen in new window的流程:

  1. 接收参数为interface{},那么调用函数时会有隐式的类型转换
  2. interface转换成reflect.emptyInterfaceopen in new window
  3. 提取类型信息emptyInterface.typ并返回

reflect.ValueOfopen in new window的流程:

  1. 接收参数为interface{},那么调用函数时会有隐式的类型转换
  2. 使用reflect.escapesopen in new window将类型逃逸
  3. 使用reflect.unpackEfaceopen in new window将传入的值构建成reflect.Valueopen in new window结构体

3.3 更新变量

使用reflect.Value.Setopen in new window更新反射变量,流程如下:

  1. 使用reflect.flag.mustBeAssignableopen in new window,检查是否可被设置
  2. 使用reflect.flag.mustBeExportedopen in new window,检查是否是导出的
  3. 使用reflect.Value.assignToopen in new window,根据新的值和原对象构建新的反射对象

3.4 实现协议

使用reflect.rtype.Implementsopen in new window方法可用于判断特定类型是否实现了某个接口,流程如下:

  1. 检查入参,若入参为nil或不是接口类型,触发对应的panic
  2. 调用reflect.implementsopen in new window检查类型的实现关系,会比较所有的接口方法,时间复杂度为O(n)

使用示例:

type CustomError struct{}

func (*CustomError) Error() string {
	return ""
}

func main() {
	typeOfError := reflect.TypeOf((*error)(nil)).Elem()     // 获取接口类型
	customErrorPtr := reflect.TypeOf(&CustomError{})		// 获取具体类型
	customError := reflect.TypeOf(CustomError{})			// 获取具体类型

	fmt.Println(customErrorPtr.Implements(typeOfError)) // #=> true
	fmt.Println(customError.Implements(typeOfError)) // #=> false
}

3.5 方法调用

使用reflectopen in new window可以执行指定的函数,示例如下:

func Add(a, b int) int { return a + b }

func main() {
	v := reflect.ValueOf(Add)
	if v.Kind() != reflect.Func {
		return
	}
	t := v.Type()
	argv := make([]reflect.Value, t.NumIn())
	for i := range argv {
		if t.In(i).Kind() != reflect.Int {
			return
		}
		argv[i] = reflect.ValueOf(i)
	}
	result := v.Call(argv)
	if len(result) != 1 || result[0].Kind() != reflect.Int {
		return
	}
	fmt.Println(result[0].Int()) // #=> 1
}
  1. reflect.ValueOf(Add)reflect.ValueOfopen in new window获取函数的反射对象
  2. v.Type().NumIn()reflect.rtype.NumInopen in new window获取函数的入参个数
  3. reflect.ValueOfopen in new window设置入参
  4. reflect.Value.Callopen in new window调用函数,并传入函数列表
  5. 获取结果,验证结果和类型

Reference

  1. https://draveness.me/golangopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2