函数的传递参数方式:
- 传递结构体时:会拷贝结构体中的全部内容;
- 传递结构体指针时:会拷贝结构体指针;
- 将指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值
Go 语言的整型和数组类型都是值传递的
也就是在调用函数时会对内容进行拷贝。需要注意的是如果当前数组的大小非常的大,这种传值的方式会对性能造成比较大的
所以在使用函数的时候 传递数组或者内存占用非常大的结构体时,我们应该尽量使用指针作为参数类型来避免发生数据拷贝进而影响性能
golang的函数
- 通过堆栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右;
- 函数返回值通过堆栈传递并由调用者预先分配内存空间;
- 调用函数时都是传值,接收方会对入参进行复制再计算;
Go 语言选择了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝
数组:
数组和字符串的一些简单越界错误都会在编译期间发现,例如:直接使用整数或者常量访问数组;但是如果使用变量去访问数组或者字符串时,编译器就无法提前发现错误,我们需要 Go 语言运行时阻止不合法的访问:数组和字符串的一些简单越界错误都会在编译期间发现,例如:直接使用整数或者常量访问数组;但是如果使用变量去访问数组或者字符串时,编译器就无法提前发现错误,我们需要 Go 语言运行时阻止不合法的访问:
Go 语言运行时在发现数组、切片和字符串的越界操作会由运行时的 runtime.panicIndex
和 runtime.goPanicIndex
触发程序的运行时错误并导致崩溃退出:
slice:
[]int []interface{}
从切片的定义我们能推测出,切片在编译期间的生成的类型只会包含切片中的元素类型,即 int
或者 interface{}
等
在运行时切片可以由如下的 reflect.SliceHeader
结构体表示,其中:
Data
是指向数组的指针;Len
是当前切片的长度;Cap
是当前切片的容量,即Data
数组的大小
type SliceHeader struct { Data uintptr Len int Cap int }
Go 语言中包含三种初始化切片的方式:
- 通过下标的方式获得数组或者切片的一部分;
- 使用字面量初始化新的切片;
- 使用关键字
make
创建切片:
arr[0:3] or slice[0:3] slice := []int{1, 2, 3} slice := make([]int, 10)
runtime.makeslice
在最后调用的 runtime.mallocgc
是用于申请内存的函数,这个函数的实现还是比较复杂,如果遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化,我们会在后面的章节中详细介绍 Go 语言的内存分配器
使用 append
关键字向切片中追加元素也是常见的切片操作
/internal/gc.state.append
方法会根据返回值是否会覆盖原变量,选择进入两种流程,如果 append
返回的新切片不需要赋值回原有的变量,就会进入如下的处理流程:
/ append(slice, 1, 2, 3) ptr, len, cap := slice newlen := len + 3 if newlen > cap { ptr, len, cap = growslice(slice, newlen) newlen = len + 3 } *(ptr+len) = 1 *(ptr+len+1) = 2 *(ptr+len+2) = 3
先解析切片结构体获取它的数组指针、大小和容量,如果在追加元素后切片的大小大于容量,那么就会调用 runtime.growslice
对切片进行扩容并将新的元素依次加入切片
·如果使用 slice = append(slice, 1, 2, 3)
语句,那么 append
后的切片会覆盖原切片,这时compile/internal/gc.state.append
方法会使用另一种方式展开关键字:
// slice = append(slice, 1, 2, 3) a := &slice ptr, len, cap := slice newlen := len + 3 if uint(newlen) > uint(cap) { newptr, len, newcap = growslice(slice, newlen) vardef(a) *a.cap = newcap *a.ptr = newptr } newlen = len + 3 *a.len = newlen *(ptr+len) = 1 *(ptr+len+1) = 2 *(ptr+len+2) = 3
是否覆盖原变量的逻辑其实差不多,最大的区别在于得到的新切片是否会赋值回原变量。
如果我们选择覆盖原有的变量,就不需要担心切片发生拷贝影响性能,因为 Go 语言编译器已经对这种常见的情况做出了优化。
拷贝切片
切片拷贝方式都会通过 runtime.memmove
将整块内存的内容拷贝到目标的内存区域中:
切片的很多功能都是由运行时实现的,无论是初始化切片,还是对切片进行追加或扩容都需要运行时的支持,需要注意的是在遇到大切片扩容或者复制时可能会发生大规模的内存拷贝,一定要减少类似操作避免影响程序的性能。
相关文档: