函数调用
调用惯例
调用惯例:调用方和被调用方对于参数和返回值传递的约定。各种语言的调用函数的语法大致相似,但是调用惯例可能会有很大不同。
C 语言
在 x86_64 机器上使用 C 语言中的函数调用时,参数都是通过寄存器和栈传递的,其中:
● 六个即六个以下参数会按照顺序分别使用 edi, esi, edx, esx, r8d, r9d 这六个寄存器。
● 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序存入堆中。
而函数的返回值是通过 eax 寄存器进行传递的,由于只能使用一个寄存器传返回值,所以 C 语言只能有一个返回值。
Go 语言
使用栈传递参数和接收返回值,所以只需要在栈上多分配一些内存就能返回多个值。
Go 和 C 对比
C 语言同时使用栈和寄存器传递参数,使用 eax 寄存器传递返回值,Go 语言使用栈传递参数和返回值。
C 语言:极大地减少函数调用的额外开销,但是增加了实现的复杂度。
● CPU 访问栈的开销比访问寄存器高十倍。
● 需要单独处理函数参数过多的情况。
● 返回值只能有一个。
Go 语言:降低实现的复杂度并支持多返回值,但是牺牲了函数的性能。
● 寄存器数量和参数的数量无关。
● 不需要考虑不同架构上寄存器的差异。
● 函数的入参和出参的内存空间需要在栈上分配。
● 编译器更加简洁,易于维护。
参数传递
传值和传引用的区别:
● 传值:函数调用时会复制参数,被调用方和调用方含有两份不相关的数据。
● 传引用:函数调用时会传递参数的指针,被调用方和调用方持有相同的数据,任意一方做出的修改都会影响另一方。
不同语言有不同的方式传递参数,Go 语言使用值传递,无论是传递基本类型,结构体,指针都会对传递的参数进行复制。
整型和数组
在调用时复制内容,获取另一份拷贝为函数使用,注意数组过大时会产生较大开销。
结构体和指针
传递结构体时会复制结构体中的全部内容,传递结构体指针时,会复制结构体指针。将指针传递给函数时,函数也会复制指针,也就是同时有两个指针指向同一个数据。
小结
● 通过堆栈传递参数,入栈的顺序是从右到左,参数的计算是从左到右。
● 函数返回值通过堆栈传递,并由调用方预先分配内存空间。
● 调用函数时都是传值,接收方会对入参进行复制再计算。
接口
概述
接口(interface)计算机系统中多个组件共享的边界,不同组件能够在边界上交换信息。接口本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层模块不在依赖下层的具体模块,只需要依赖一个约定好的接口。
隐式接口
接口隐式实现,而且在类型检查时,编译器只在需要时才检查类型。
类型
interface
分为两种,一种是 runtime.iface
表示带有一组方法的接口,一种是 runtime.eface
表示不带任何方法的接口 interface{}
。
interface{}
并不是任意类型,如果将类型转为 interface{}
类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}
。
指针和接口
实现接口的方法可以是结构体来实现,也可以是结构体指针来实现,同一方法不能被其结构体和结构体指针同时实现,会报错声明函数已经被声明。
结构体实现接口 | 结构体指针实现接口 | |
---|---|---|
结构体初始化变量 | 可 | 不可 |
结构体指针初始化变量 | 可 | 可 |
通过表格可见,当使用结构体指针初始化变量时,无论接口方法是以哪一种方式实现,最后都可以通过,而当使用结构体初始化时,如果接口方法中存在由结构体指针实现的方法,则不能通过。
编译器可以通过指针隐式的对变量解引用,即获得结构体的指针,就可以隐式的获得结构体本身,所以使用结构体实现方法,结构体指针实现接口是可行的。
nil 与 non-nil
func test(v interface{}) bool {
return v == nil
}
type ms struct{}
func main(){
var ms *ms
fmt.Println(ms==nil) //true
fmt.Println(test(ms)) //false
}
调用函数 test
时发生了隐式类型转换,除向方法传入参数外,变量的赋值也会触发隐式的类型转换,*ms
类型转换成了 interface{}
类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 ms
,所以转换后的变量与 nil
不相等。
数据结构
概述
已知 Go 语言的结构体分为两类:runtime.iface
, runtime.eface
type eface struct {
_type *_type
data unsafe.Pointer
}
type iface struct {
tab *itab //表示接口和结构体关系
data unsafe.Pointer
}
类型结构体
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
判断当前类型的多个对象是否相等。
itab 结构体
type itab struct { //32字节
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
●inter
和 _type
表示类型
● hash
是对 type.hash
的复制,当要将 interface
转换为具体类型时,可以用该字段快速判断目标类型和具体类型 runtime._type
是否一致。
● fun
是一个动态大小的数组,是一个用于动态派发的虚拟表,存储了一组函数指针,虽然被声明为固定大小的数组,但是在使用时会使用原始指针获取其中的数据,所以 fun
数组中元素数量是不确定的。
动态派发
概述
动态派发:是在运行期间选择具体多彩操作(方法或函数)执行的过程,是面向对象语言中常见的特性。Go 接口的引入为它带来了动态派发特性。
性能开销对比
直接调用 | 动态派发 | |
---|---|---|
指针 | t | 1.05t |
结构体 | t | 2.25t |
动态派发在结构体上的表现非常差,主要原因是 Go 语言在函数调用时是传值的,动态派发放大了参数调用的影响。所以结构体实现接口方法最好使用结构体指针。
嵌套
结构体嵌套接口
package main
import "fmt"
type T struct {
Interface
}
type Interface interface {
M1()
M2()
}
func (T) M1() {
fmt.Println("T M1")
}
type S struct{}
func (S) M1() {
fmt.Println("S M1")
}
func (S) M2() {
fmt.Println("S M2")
}
func main() {
t := T{
Interface: S{},
}
t.M1()
t.M2()
}
//T M1
//S M2
● 优先选择结构体自身实现的方法。
● 如果结构体自身并未实现,那么将查找结构体中的嵌入接口类型的方法集合中是否有该方法,如果有,则提升(promoted)为结构体的方法。
T 实现了方法 M1,所以调用时首先调用自身的方法,没有自身没有实现的 M2,将 S 实现的方法提升为自身结构体的方法。
结构体嵌套结构体
package main
import "fmt"
type T struct {
S
}
type Interface interface {
M1()
M2()
}
func (T) M1() {
fmt.Println("T M1")
}
type S struct{}
func (S) M1() {
fmt.Println("S M1")
}
func (S) M2() {
fmt.Println("S M2")
}
func main() {
t := T{
S: S{},
}
t.M1()
t.M2()
}
//T M1
//S M2
与嵌套接口类似。
package main
import (
"fmt"
"reflect"
)
func DumpMethodSet(i interface{}) {
v := reflect.TypeOf(i)
elemType := v.Elem()
n := elemType.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty! \n", elemType)
return
}
fmt.Printf("%s's method set: \n", elemType)
for i := 0; i < n; i++ {
fmt.Println("-", elemType.Method(i).Name)
}
fmt.Print("\n")
}
type T1 struct{}
func (T1) T1M1() { fmt.Println("T1's M1") }
func (T1) T1M2() { fmt.Println("T1's M2") }
func (*T1) PT1M3() { fmt.Println("PT1's M3") }
type T2 struct{}
func (T2) T2M1() { fmt.Println("T2's M1") }
func (T2) T2M2() { fmt.Println("T2's M2") }
func (*T2) PT2M3() { fmt.Println("PT2's M3") }
type T struct {
T1
*T2
}
func main() {
t := T{
T1: T1{},
T2: &T2{},
}
println("\ncall method through t:")
t.T1M1()
t.T1M2()
t.PT1M3()
t.T2M1()
t.T2M2()
t.PT2M3()
println("\ncall methdo through pt:")
pt := &t
pt.T1M1()
pt.T1M2()
pt.PT1M3()
pt.T2M1()
pt.T2M2()
pt.PT2M3()
var t1 T1
var pt1 *T1
DumpMethodSet(&t1)
DumpMethodSet(&pt1)
var t2 T2
var pt2 *T2
DumpMethodSet(&t2)
DumpMethodSet(&pt2)
DumpMethodSet(&t)
DumpMethodSet(&pt)
}
call method through t:
T1's M1
T1's M2
PT1's M3
T2's M1
T2's M2
PT2's M3
call methdo through pt:
T1's M1
T1's M2
PT1's M3
T2's M1
T2's M2
PT2's M3
main.T1's method set:
- T1M1
- T1M2
*main.T1's method set:
- T1M2
- T2M1
- T2M2
*main.T's method set:
- PT1M3
- PT2M3
- T1M1
- T1M2
- T2M1
- T2M2
PS F:\Golang> go run main.go
call method through t:
T1's M1
T1's M2
PT1's M3
T2's M1
T2's M2
PT2's M3
call methdo through pt:
T1's M1
T1's M2
PT1's M3
T2's M1
T2's M2
PT2's M3
main.T1's method set:
- T1M1
- T1M2
*main.T1's method set:
- PT1M3
- T1M1
- T1M2
main.T2's method set:
- T2M1
- T2M2
*main.T2's method set:
- PT2M3
- T2M1
- T2M2
main.T's method set:
- PT2M3
- T1M1
- T1M2
- T2M1
- T2M2
*main.T's method set:
- PT1M3
- PT2M3
- T1M1
- T1M2
- T2M1
- T2M2
通过输出结果可以看出,虽然无论通过 T 类型变量实例还是 T 类型变量实例都可以调用所有“继承”的方法(这也是Go语法糖),但是 T 和 T 类型的方法集合是有差别的:
T 类型的方法集合 = T1 的方法集合 + *T2 的方法集合;
T 类型的方法集合 = T1 的方法集合 + *T2 的方法集合
反射
概述
反射是在运行的时候来自我检查,并对内部成员进行操作。就是说这个变量的类型可以动态的改变,在运行的时候确定它的作用。reflect 实现了运行时的反射能力,能够让程序操作不同的对象。
Go 语言反射的三大法则:
● interface{}
变量可以转换成反射对象
● 从反射对象可以获取 interface{}
变量
● 要修改反射对象,其值必须可以设置
三大法则
第一法则
调用函数 valueof
,typeof
时会进行类型转换,这两个函数都是接收 interface{}
类型,所以将传进来的参数进行了类型转换。
第二法则
从反射对象获取 interface{}
类型变量,
从接口到反射对象,从反射对象到接口都需要进行两次转换
● 从接口到反射对象
▶ 从基本类型到接口类型的类型转换
▶ 从接口类型到反射对象的转换
● 从反射对象到接口
▶ 反射对象转换成接口类型
▶ 通过显式类型转换转换成原始类型
第三法则
要修改反射值,其值必须可修改。
func main() {
var a int = 10
var d dog
d.legs = 9
v := reflect.ValueOf(&a)
v.Elem().SetInt(998)
fmt.Println(a)
v = reflect.ValueOf(&d.legs)
v.Elem().SetInt(4)
fmt.Println(d)
}
type dog struct {
legs int
}
代码核心逻辑如下:
● 调用 reflect.ValueOf
获取变量的指针
● 调用 reflect.Value.Elem
获取指针指向的变量
● 调用 v.Elem().SetInt()
更新变量的值
由于函数调用都是值传递的,所以直接修改变量的值得到的反射对象与原变量没有任何关系,而且会导致程序崩溃,所以使用这种迂回方式,通过指针修改变量。
类型和值
interface{}
类型在 go 语言中都是通过 reflect.emptyInterface
结构体表示的。
type emptyInterface struct {
typ *rtype //表示变量类型
word unsafe.Pointer //指向内部封装的数据
}
用于获取变量类型的函数 reflect.TypeOf
函数将传入的变量转换为 reflect.emptyInterface
类型,并获取其中的类型信息。
用于获取接口值得 reflect.ValueOf
函数实现过程先是调用 reflect.escapes
保证当前值逃逸到堆中,然后通过 reflect.unpaceEface
从中获取具体的结构体。
实现协议
判断一个结构体是否实现一个接口,首先判断接口是否含有方法,如果没有方法,则任何类型都可以实现该接口,如果不是空类型,通过遍历接口和结构体中的方法签名(两者的函数签名都是按照字母的顺序排列的)来判断。
变长参数
模拟函数的可选参数和默认参数
package main
import "fmt"
type record struct {
name string
gender string
age uint16
city string
country string
}
func enroll(args ...interface{}) (*record, error) {
if len(args) > 5 || len(args) < 3 {
return nil, fmt.Errorf("the number of arguments passed is wrong")
}
r := &record{
city: "Beijing",
country: "China",
}
for i, v := range args {
switch i {
case 0:
name, ok := v.(string)
if !ok {
return nil, fmt.Errorf("name is not passed as string")
}
r.name = name
case 1:
gender, ok := v.(string)
if !ok {
return nil, fmt.Errorf("gender is not passed as string")
}
r.gender = gender
case 2:
age, ok := v.(int)
if !ok {
return nil, fmt.Errorf("age is not passed as int")
}
r.age = uint16(age)
case 3:
city, ok := v.(string)
if !ok {
return nil, fmt.Errorf("city is not passed as string")
}
r.city = city
case 4:
county, ok := v.(string)
if !ok {
return nil, fmt.Errorf("county is not passed as string")
}
r.country = county
default:
return nil, fmt.Errorf("unknow argument passed")
}
}
return r, nil
}
func main() {
r, _ := enroll("小明", "male", 23)
fmt.Printf("%+v\n", *r)
r, _ = enroll("小红", "female", 13, "Hangzhou")
fmt.Printf("%+v\n", *r)
r, _ = enroll("Leo Messi", "male", 33, "Barcelona", "Spain")
fmt.Printf("%+v\n", *r)
_, err := enroll("小吴", 21, "Suzhou")
if err != nil {
fmt.Println(err)
return
}
}
//{name:小明 gender:male age:23 city:Beijing country:China}
//{name:小红 gender:female age:13 city:Hangzhou country:China}
//{name:Leo Messi gender:male age:33 city:Barcelona country:Spain}
//gender is not passed as string
可见上述代码实现了默认值,但是该实现是由局限性的,调用者只能从右侧的参数开始逐一进行省略传递的处理,比如:可以省略 country,可以省略 country、city,但不能省略 city 而不省略 country 的传递。