前言
仅记录学习过程中遇到的问题和一些怪东西,如有错误请指正
1 第一个陷阱
1.1 下面程序的输出是?
package main
import "fmt"
func foo(a [2]int) {
a[0] = 200
}
func main() {
a := [2]int{1, 2}
foo(a)
fmt.Println(a)
}
1.2 答案
正确的输出是 [1 2],数组 a 没有发生改变。
- 在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如
[2]int和[20]int属于不同的类型。 - 当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。
为了避免数组的拷贝,提高性能,建议传递数组的指针作为参数,或者使用切片代替数组。
1.3 more
我们可以简单修改一下上述代码:
package main
import "fmt"
func foo(a *[2]int) {
(*a)[0] = 200
}
func main() {
a := [2]int{1, 2}
foo(&a)
fmt.Println(a)
}
或者:
package main
import "fmt"
func foo(a []int) {
a[0] = 200
}
func main() {
a := []int{1, 2}
foo(a)
fmt.Println(a)
}
输出将会变成 [200 2]。
这里我们就不得不提到一个点: 切片由三个值构成:
*ptr指向底层数组的指针len长度cap容量
因此,将切片作为参数时,拷贝了一个新切片,即拷贝了构成切片的三个值,包括底层数组的指针。对切片中某个元素的修改,实际上是修改了底层数组中的值,因此原切片也发生了改变。
2 第二个陷阱
2.1 下面程序的输出是?
package main
import "fmt"
func foo(a []int) {
a = append(a, 1, 2, 3, 4, 5, 6, 7, 8)
a[0] = 200
}
func main() {
a := []int{1, 2}
foo(a)
fmt.Println(a)
}
2.2 答案
输出仍是 [1 2],切片 a 没有发生改变。
传参时拷贝了新的切片,因此当新切片的长度发生改变时,原切片并不会发生改变。而且在函数 foo 中,新切片 a 增加了 8 个元素,原切片对应的底层数组不够放置这 8 个元素,因此申请了新的空间来放置扩充后的底层数组。这个时候新切片和原切片指向的底层数组就不是同一个了。因此,对新切片第 0 个元素的修改,并不会影响原切片的第 0 个元素。
如果如果希望 foo 函数的操作能够影响原切片呢?
在这里我们给出两种方式:
- 设置返回值,将新切片返回并赋值给
main函数中的变量a。 - 切片也使用指针方式传参。
package main
import "fmt"
func foo(a []int) []int {
a = append(a, 1, 2, 3, 4, 5, 6, 7, 8)
a[0] = 200
return a
}
func main() {
a := []int{1, 2}
foo(a)
fmt.Println(a)
}
或者
package main
import "fmt"
func foo(a *[]int){
*a = append(*a, 1, 2, 3, 4, 5, 6, 7, 8)
(*a)[0] = 200
}
func main() {
a := []int{1, 2}
foo(&a)
fmt.Println(a)
}
上述两个程序的输出均为:
[200 2 1 2 3 4 5 6 7 8]
从可读性上来说,更推荐第一种方式。
more and more
切片的本质
在 Go 语言中,切片(slice)可能是使用最为频繁的数据结构之一,切片类型为处理同类型数据序列提供一个方便而高效的方式。
数组
Go 的切片(slice)是在数组(array)之上的抽象数据类型,数组类型定义了长度和元素类型。例如, [3]int 类型表示由 3 个 int 整型组成的数组,数组以索引方式访问,例如表达式 s[n] 访问数组的第 n 个元素。数组的长度是固定的,长度是数组类型的一部分。长度不同的 2 个数组是不可以相互赋值的
性能陷阱
大量内存得不到释放
在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。比较推荐的做法,使用 copy 替代 re-slice。