Golang 函数传参使用切片而不使用数组为什么?
阅读目录
数组与切片的区别
Go里面的数组是值类型,切片是引用类型。
值类型的对象在做为实参传给函数时,形参是实参的另外拷贝的一份数据,对形参的修改不会影响函数外实参的值。
因此在如下例子中两次打印的指针地址是不一样的。
数组是值类型
package main
import "fmt"
func arrayTest(x [2]int) {
fmt.Printf("%p \n", &x)
// 0xc00000e0d0
}
func main() {
arrayA := [2]int{1, 2}
fmt.Printf("%p \n", &arrayA)
// 0xc00000e100
arrayTest(arrayA)
}
假想每次传参都用数组,那么每次数组都要被复制一遍。如果数组大小有 100万,在64位机器上就需要花费大约 800W 字节,即 8MB 内存。这样会消耗掉大量的内存。
切片是引用类型
引用类型,则没有这个拷贝的过程,实参与形参指向的是同一块内存地址。
package main
import "fmt"
func sliceTest(x []int) {
fmt.Printf("%p \n", x)
// 0x3c9580
}
func main() {
sliceA := make([]int, 0)
fmt.Printf("%p \n", sliceA)
// 0x3c9580
sliceTest(sliceA)
}
由此我们可以得出结论:
把第一个大数组传递给函数会消耗很多内存,采用切片的方式传参可以避免上述问题。切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。
那么你肯定要问了,数组指针也是引用类型啊,也不一定要用切片吧?
确实,传递数组指针是可以避免对值进行拷贝的内存浪费。
切片函数传递是引用传值示例
package main
import "fmt"
func modify(arr []int) {
arr[0] = 999
}
func main() {
arr := []int{1, 2, 3, 4, 5}
modify(arr)
fmt.Println(arr)
}
PS E:\TEXT\test_go\test> go run .\main.go
[999 2 3 4 5]
PS E:\TEXT\test_go\test>
package main
import "fmt"
func sliceTest(x []int) {
x[1] = 888
}
func main() {
sliceA := make([]int, 0)
sliceA = append(sliceA, 1, 2, 3)
sliceTest(sliceA)
fmt.Println(sliceA)
}
PS E:\TEXT\test_go\test> go run .\main.go
[1 888 3]
PS E:\TEXT\test_go\test>
数组对比切片有哪些优势?
1 编译检查越界
由于数组在声明后,长度就是固定的,因此在编译的时候编译器可以检查在索引取值的时候,是否有越界。
func main() {
array := [2]int{}
array[2] = 2
//invalid array index 2 (out of bounds for 2-element array)
}
而切片的长度只有运行时才能知晓,编译器无法检查。
2 长度是类型的一部分
在声明一个数组的类型时,需要指明两点:
- 元素的类型
- 元素的个数
var array [2]int
因此长度是数组类型的一部分,两个元素类型相同,但可包含的元素个数不同的数组,属于两个类型。
func main() {
var array1 [2]int
var array2 [2]int
var array3 [3]int
fmt.Println(reflect.TypeOf(array1) == reflect.TypeOf(array2)) // true
fmt.Println(reflect.TypeOf(array1) == reflect.TypeOf(array3)) // false
}
基于这个特点,可以用它来达到一些合法性校验的目的,例如 IPv4 的地址可以声明为 [4]byte,符合该类型的数组就是合法的 ip,反之则不合法。
3 数组可以比较
类型相同的两个数组可以进行比较。
func main() {
array1 := [2]int{1,2}
array2 := [2]int{1,2}
array3 := [2]int{2,1}
fmt.Println(array1 == array2) // true
fmt.Println(array1 == array3) // false
}
类型不同(长度不同)的数组 和 切片均不行。
可比较这一特性,决定了数组也可以用来当 map 的 key 使用。
func main() {
array1 := [2]int{1,2}
dict := make(map[[2]int]string)
dict[array1] = "hello"
fmt.Println(dict) // map[[1 2]:hello]
}
引用类型与指针,有什么不同?
切片是一个引用类型,将它作为参数传入函数后,你在函数里对数据作变更是会实时反映到实参切片的。
func foo(s []int) {
s[0] = 666
}
func main() {
slice := []int{1,2}
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [666 2]
}
此时切片这一引用类型,是不是有点像指针的效果?是的。
但它又和指针不一样,这一点主要体现在:
在形参中所作的操作并不一定都会反映在实参上。
还是以切片为例,我在形参上对切片进行扩容,发现形参扩容后,实参并没有发生改变。
func foo(s []int) {
s = append(s, 666)
}
func main() {
slice := []int{1,2}
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [1 2]
}
这是为什么呢?
这是因为当你对一个切片 append 的时候,它会做这些事情:
- 新建一个新的切片 slice2,其实长度与 slice1 一样,但容量是 slice1 的两倍,此时 slice2 底层指向的匿名数组和 slice1 不是同一个。
- 将 slice1 底层的数组的元素,一个一个的拷贝给 slice2 底层的数组。
- 并把扩容的元素也拷贝到 slice2中。
- 最后把新的 slice2 返回回来,这就是为什么指针不用返回,而 slice.append 也要返回的原因。
append 实现引用传递
package main
import "fmt"
func foo(s *[]int) {
*s = append(*s, 666)
}
func main() {
slice := []int{1, 2}
fmt.Println(slice) // [1 2]
foo(&slice)
fmt.Println(slice) // [1 2 666]
}
Golang中函数的参数为切片时是传引用还是传值?
对于这个问题,可能会有很多认为是传引用,就比如下面这段代码:
func foo(s []int) {
s[0] = 666
}
func main() {
slice := []int{1,2}
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [666 2]
}
如果你不了解 Go 中切片的底层结构,你很可能会误信上面的观点。
但其实不是,Go语言中都是值传递,而不是引用传递,也不是指针传递。
Go 中切片的底层结构是这样的:
type slice struct {
array unsafe.Pointer
len int
cap int
}
当你将切片作为实参传给函数时,函数是会拷贝一份实参的结构和数据,生成另一个切片,实参切片和形参切片,不仅是长度、容量相等,连指向底层数组的指针都是一样的。
通过分别打印实参切片和形参切片的指针地址,就能验证这一观点:
func foo(s []int) {
fmt.Printf("%p \n", &s) // 0xc00000c080
s = append(s, 666)
}
func main() {
slice := []int{1,2}
fmt.Printf("%p \n", &slice) // 0xc00000c060
foo(slice)
fmt.Printf("%p \n", &slice) // 0xc00000c060
}
package main
import "fmt"
func foo(s *[]int) {
fmt.Printf("%p \n", &s) // 0xc00012c020
*s = append(*s, 666)
}
func main() {
slice := []int{1, 2}
fmt.Printf("%p \n", &slice) // 0xc000100048
foo(&slice)
fmt.Printf("%p \n", &slice) // 0xc000100048
}
相关文章
- 不使用循环使用递归得到数组的值得求和
- js利用数组length属性清空和截短数组
- leetcode - 子数组最大平均值
- 每日一道 LeetCode (37):两数之和 II - 输入有序数组
- Shell - 简明Shell入门07 - 数组(Array)
- Golang 005. 求列表(或数组)嵌套的最大深度
- [手游项目3]-7-删除排序数组中的重复项的golang实现
- 找到所有数组中消失的数字/C++
- Go语言自学系列 | golang数组
- C++:读写二进制文件到double数组
- GoLang笔记-数组和切片,本质是就是长度不可变的可变的区别
- Golang数组和切片的区别
- 【GoLang】golang context channel 详解
- golang删除数组某个元素
- golang 切片slice使用总结(动态数组、cap与len区别)
- 给定一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度?