第四章笔记总结
1. 函数调用
Go 语言函数调用:
- 函数参数按照从右至左的顺序入栈
- 函数返回值存储在栈上
C 和 Go 在函数调用上的差异:
- C 使用寄存器和栈传递参数,使用寄存器传递返回值,无法返回多个返回值
- Go 使用栈传递参数和返回值,可以返回多个返回值
1.2 参数传递
Golang 的参数传递采用值传递,即会对参数进行拷贝。
2. 接口
2.1 定义及其优点
在计算机科学中,接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息。
使用接口可以:
- 解耦有依赖关系的上下游
- 隐藏底层实现,减少关注点
2.2 隐式实现
Golang 的接口的实现是隐式的,实现接口定义的所有方法即可实现该接口,无需显式声明。
2.3 类型
Golang 的接口有两种:
- 空接口,
interface{}
,不含任何方法,运行时数据结构为runtime.eface
- 接口,包含方法,运行时数据结构为
runtime.iface
空接口不是任意类型,若将变量赋值给空接口将发生隐式的类型转换。
2.4 接口和方法集
将变量经过类型转换成接口类型后,根据原变量类型的不同(值类型或指针类型)能调用的方法集不同。
原变量类型\方法接收者 | 值 | 指针 |
---|---|---|
值 | 可 | 不可 |
指针 | 可 | 可 |
- 原变量类型为指针,转换成接口类型后,接口持有的是原变量的指针,可以调用接收者为值类型(自动解引用)的方法和指针类型的方法
- 原变量类型为值,转换成接口类型后,接口持有的是原变量的值的拷贝,可以调用接收者为值类型的方法,不能调用指针类型的方法(已不可寻址)
根本原因在于,Golang 的函数参数使用值传递,当原变量类型是值类型时,接口变量持有的是一份拷贝,在调用指针方法时无法获取原始的变量地址。
2.5 数据结构
空接口
type eface struct {
_type *_type
data unsafe.Pointer
}
_type
:指向表示类型的结构data
:指向底层数据
接口
type iface struct {
tab *itab
data unsafe.Pointer
}
tab
: 指向表示类型的结构data
:指向底层数据
runtime._type
runtime._type
是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.itab
是接口类型的核心组成部分:
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 接口,方法实现为指针方法)
将指针类型的变量转换成接口类型的流程:
- 结构体 Cat 在堆上初始化,并将指针存放在栈上
- 进行类型转换,将 Cat 转换成 Duck 类型
- 调用接口方法
完成类型转换之后的栈内容如下:
值 -> 接口
var c Duck = Cat{Name: "draven"}
c.Quack()
以上述代码为例(Cat 实现了 Duck 接口,方法实现为值类型方法)
将值类型变量转换成接口类型的流程如下:
- 在栈上初始化结构体 Cat
- 进行类型转换,调用函数
runtime.convT2I
,生成runtime.iface
结构体,其中iface.data
会在堆上初始化,并将栈上的 Cat 的内容拷贝过去 - 调用接口方法
完成类型转换之后的栈内容如下:
2.7 类型断言
类型断言是将接口类型转换成具体类型。
非空接口
var c Duck = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
以type-switch
为例,类型断言并调用方法的流程如下:
- 获取目标类型的
_type.hash
- 和接口中的
iface.itab.hash
比较 - 若相同:
- 获取
iface.data
,即*Cat
指针 - 调用接口方法
- 获取
- 若不同,则直接返回
空接口
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()
的流程如下:
- 进行类型转换
- 调用接口方法
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. 反射
反射包reflect
有两个主要的函数:
reflect.TypeOf
接口类型,用于获取类型信息reflect.ValueOf
结构体类型,用于获取数据的运行时表示
3.1 反射的三大法则
反射可以作为元编程方式减少代码,但是过多的反射会使得程序逻辑难以理解并运行缓慢。
Golang 的反射由三大法则:
- 从
interface{}
变量可以反射出反射对象 - 从反射对象可以获取
interface{}
变量 - 要修改反射对象,其值必须可以设置
interface{}
中可以反射出反射对象
从reflect.TypeOf
和reflect.ValueOf
的入参都是interface{}
类型,在方法调用的过程中,会发生隐式的类型转换。
interface{}
变量
从反射对象可以获取reflect.Value.Interface
可以将反射变量转换成interface{}
变量。
v := reflect.ValueOf(1)
v.Interface().(int)
要修改反射对象,其值必须可以设置
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
reflect.ValueOf
传入变量的指针reflect.Value.Elem
获取指针指向的变量reflect.Value.SetInt
更新变量的值
3.2 类型和值
interface{}
在反射包中使用reflect.emptyInterface
表示:
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
typ
:表示具体类型word
:表示底层结构
reflect.TypeOf
的流程:
- 接收参数为
interface{}
,那么调用函数时会有隐式的类型转换 - 将
interface
转换成reflect.emptyInterface
- 提取类型信息
emptyInterface.typ
并返回
reflect.ValueOf
的流程:
- 接收参数为
interface{}
,那么调用函数时会有隐式的类型转换 - 使用
reflect.escapes
将类型逃逸到堆上 - 使用
reflect.unpackEface
将传入的值构建成reflect.Value
结构体
3.3 更新变量
使用reflect.Value.Set
可更新反射变量,流程如下:
- 使用
reflect.flag.mustBeAssignable
,检查是否可被设置 - 使用
reflect.flag.mustBeExported
,检查是否是导出的 - 使用
reflect.Value.assignTo
,根据新的值和原对象构建新的反射对象
3.4 实现协议
使用reflect.rtype.Implements
方法可用于判断特定类型是否实现了某个接口,流程如下:
- 检查入参,若入参为
nil
或不是接口类型,触发对应的panic - 调用
reflect.implements
检查类型的实现关系,会比较所有的接口方法,时间复杂度为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 方法调用
使用reflect
可以执行指定的函数,示例如下:
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
}
reflect.ValueOf(Add)
:reflect.ValueOf
获取函数的反射对象v.Type().NumIn()
:reflect.rtype.NumIn
获取函数的入参个数reflect.ValueOf
设置入参reflect.Value.Call
调用函数,并传入函数列表- 获取结果,验证结果和类型