gocookbook icon indicating copy to clipboard operation
gocookbook copied to clipboard

函数的调用惯例和参数传递

Open kevinyan815 opened this issue 4 years ago • 0 comments

函数调用惯例

调用惯例是调用方和被调用方对于参数和返回值传递的约定。

C语言的调用惯例

当我们在 x86_64 的机器上使用 C 语言中调用函数时,参数都是通过寄存器和栈传递的,其中:

六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递; 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序依次存入栈中; 而函数的返回值是通过 eax 寄存器进行传递的,由于只使用一个寄存器存储返回值,所以 C 语言的函数不能同时返回多个值。

Go语言的调用惯例

Go 通过栈传递函数的参数和返回值,在调用函数之前会在栈上为返回值分配合适的内存空间,随后将入参从右到左按顺序压栈并拷贝参数,返回值会被存储到调用方预留好的栈空间上。

Go 语言的方式能够降低实现的复杂度并支持多返回值,但是牺牲了函数调用的性能;

  • 不需要考虑超过寄存器数量的参数应该如何传递;
  • 不需要考虑不同架构上的寄存器差异;
  • 函数入参和出参的内存空间需要在栈上进行分配;

参数传递

Go 语言在传递参数时是传值还是传引用也是一个有趣的问题,不同的选择会影响我们在函数中修改入参时是否会影响调用方看到的数据。

我们先来介绍一下传值和传引用两者的区别:

传值:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据; 传引用:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方。

不同语言会选择不同的方式传递参数,Go 语言选择了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝

可以通过下面的程序,论证Go语言的函数参数传递方式是传值:

package main

import (
	"fmt"
	"unsafe"
)

func MyFunction(i int, arr [2]int)  {
	i = 22
	arr[1] = 99
	fmt.Printf("in my_function - i=(%d, %p) arr=(%v, %p)\n", i, &i, arr, &arr)
}

type MyStruct struct {
	i int
}

func MyFunction2(a MyStruct, b *MyStruct) {
	a.i = 31;
	b.i = 41
	fmt.Printf("in my_function2 - a=(%d, %p) b=(%v, %p)\n", a, &a, b, &b)
}

func BaseTypeTest () {
	i := 30
	arr := [2]int{66, 77}
	fmt.Printf("before calling - i=(%d, %p) arr=(%v, %p)\n", i, &i, arr, &arr)
	MyFunction(i, arr)
	fmt.Printf("after calling - i=(%d, %p) arr=(%v, %p)\n", i, &i, arr, &arr)
}

func CompoundTypeTest () {
	a := MyStruct{i: 30}
	b := &MyStruct{i: 40}
	fmt.Printf("before calling - a=(%d, %p) b=(%v, %p)\n", a, &a, b, &b)
	MyFunction2(a, b)
	fmt.Printf("after calling - a=(%d, %p) b=(%v, %p)\n", a, &a, b, &b)
}



type MyStruct2 struct {
	i, j int
}

func MyFunction3 (ms *MyStruct2) {
	fmt.Printf("pointer adress in function: %p\n", &ms)
	ptr := unsafe.Pointer(ms)

	for i := 0; i < 2; i++ {
		c := (*int)(unsafe.Pointer(uintptr(ptr) + uintptr(8 * i)))
		*c = *c + i + 1
		fmt.Printf("[%p] %d\n", c, *c)
	}
}

// MyFunction3与3_1功能一样,3使用的直接修改内存地址的方式修改的指针指向结构体的字段值
func MyFunction3_1 (ms *MyStruct2) {
	fmt.Printf("pointer adress in function: %p\n", &ms)
	ms.i = 41
	ms.j = 52

}

func MyFunction4 (ms *MyStruct2) {
	fmt.Printf("pointer adress in function: %p\n", &ms)
	ms = &MyStruct2{
		i: 41,
		j: 52,
	}

	fmt.Printf("in my_function4 - [%p] %v\n", ms, ms)
}

//func MyFunction4 (ms *MyStruct2) {
//	ms
//}

func PointedDataUpdateTest()  {
	a := &MyStruct2{i: 40, j: 50}
	fmt.Printf("[%p] %v  pointer adress outside: %p\n", a, a, &a)
	MyFunction3(a)
	fmt.Printf("[%p] %v\n", a, a)
}

func UpdatePointerDirection() {
	a := &MyStruct2{i: 40, j: 50}
	fmt.Printf("before calling my_function4 - [%p] %v\n", a, a)
	MyFunction4(a)
	fmt.Printf("after calling my_function4 - [%p] %v\n", a, a)
}


func main () {
	BaseTypeTest()
	fmt.Println("=======================分割线===================")
	CompoundTypeTest()
	fmt.Println("=======================分割线===================")
	PointedDataUpdateTest()
	fmt.Println("=======================分割线===================")
	UpdatePointerDirection()
}

从上述程序运行的结果我们可以得出如下结论:

  • Go语言里函数的参数都是通过值拷贝的方式进行传递
  • 传递结构体时:会拷贝结构体中的全部内容;
  • 传递结构体指针时:会拷贝结构体指针;

总结

这里我们详细分析了 Go 语言的调用惯例,包括传递参数和返回值的过程和原理。Go 通过栈传递函数的参数和返回值,在调用函数之前会在栈上为返回值分配合适的内存空间,随后将入参从右到左按顺序压栈并拷贝参数,返回值会被存储到调用方预留好的栈空间上,我们可以简单总结出以下几条规则:

通过堆栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右; 函数返回值通过堆栈传递并由调用者预先分配内存空间; 调用函数时都是传值,接收方会对入参进行复制再计算;

部分内容摘录自:https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-function-call

kevinyan815 avatar Jan 08 '21 04:01 kevinyan815