函数调用

调用惯例

调用惯例:调用方和被调用方对于参数和返回值传递的约定。各种语言的调用函数的语法大致相似,但是调用惯例可能会有很大不同。

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

  1. func test(v interface{}) bool {
  2. return v == nil
  3. }
  4. type ms struct{}
  5. func main(){
  6. var ms *ms
  7. fmt.Println(ms==nil) //true
  8. fmt.Println(test(ms)) //false
  9. }

调用函数 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{} 变量

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

三大法则

第一法则

调用函数 valueoftypeof 时会进行类型转换,这两个函数都是接收 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 的传递。