漫谈 Golang 之 slice
Part1: array & slice
// define a slices := make([]int, 10)// define an arraya := [10]int{}
定义 slice 第一种方式
var s []int
定义 slice 第二种方式
s := make([]int, len, cap)
array
var a [length]type// var a [10]int
Part2: 类型
package mainimport "fmt"func main() { var a [8]int printArray(a)}func printArray(a [10]int) { fmt.Println(len(a)) fmt.Println(cap(a))}
以上代码中,printArray(a) 是否可以正常执行?答案是否定的,编译期就会提示错误
cannot use a (type [8]int) as type [10]int in argument to printArray
可以看到提示中变量 a 的类型是 type [8]int,而函数 printArray 要求的入参类型是 type [10]int。可见 array 的长度也是其类型的一部分!
Part3: slice grow
package mainimport "fmt"func main() { var s []int for i := 0; i < 1025; i++ { s = append(s, i) } fmt.Println(len(s)) // 1025 fmt.Println(cap(s)) // 1280 = 1024*1.25}
slice 扩容的方式是:source code
cap < 1024 –> cap * 2
cap > 1024 –> cap * 1.25
append 多个参数的对于 slice 容量的影响是不同的,特殊case:
package mainimport "fmt"func main() { var s1, s2, s3, s4, s5 []int s1 = append(s1, 0) // len 1, cap 1 printSlice(s1) s2 = append(s2, 0, 1) // len 2, cap 2 printSlice(s2) s3 = append(s3, 0, 1, 2) // len 3, cap 3 printSlice(s3) s4 = append(s4, 0, 1, 2, 3) // len 4, cap 4 printSlice(s4) s5 = append(s5, 0, 1, 2, 3, 4) // len 5, cap 6 printSlice(s5)}func printSlice(s []int) { fmt.Println(len(s)) fmt.Println(cap(s))}
Part4: slice append
如何快速完成一次 slice 数据填充?
声明一个 slice 直接开始 append
声明固定长度 slice 后开始 append
声明固定长度 slice 后,使用 index 进行数据填充
可以看到方法三是最快的。
对于大数组的赋值,常用的优化方式还有一种 BCE,可以参考这篇文章中的用法,不再赘述。golang 边界检查优化
package mainimport "testing"func BenchmarkAppend(b *testing.B) { var s []int for i := 0; i < b.N; i++ { s = append(s, i) }}func BenchmarkMakeSliceAppend(b *testing.B) { s := make([]int, 0, b.N) for i := 0; i < b.N; i++ { s = append(s, i) }}func BenchmarkIndex(b *testing.B) { s := make([]int, b.N) for i := 0; i < b.N; i++ { s[i] = i }}// Output// goos: darwin// goarch: amd64// cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz// BenchmarkAppend-8 457261122 14.93 ns/op// BenchmarkMakeSliceAppend-8 1000000000 1.407 ns/op// BenchmarkIndex-8 1000000000 1.246 ns/op
Part5: slice 扩容带来的”意外”
当使用 index 修改 slice 时,可能会出现”时而生效时而不生效的情况”,究其原因是 slice 在 grow 的过程中重新分配了内存地址。
下面这个情况展示接收 slice 的函数 sliceAppend 修改了 index 但是在函数外不生效的情况。
package mainimport "fmt"func main() { var s []int s = append(s, 1) printSlice(s) sliceAppend(s) printSlice(s)}func sliceAppend(s []int) { s = append(s, 1) // 此处发生了扩容操作,导致 s 的内存地址改变 s[0] = 0 printSlice(s)}func printSlice(s []int) { fmt.Printf("len: %v, cap: %v, val: %v\n", len(s), cap(s), s)}// Output:// len: 1, cap: 1, val: [1]// len: 2, cap: 2, val: [0 1]// len: 1, cap: 1, val: [1]
其实,扩容的发生导致函数内外的可见性也不一样了。和上个例子差不多的一个案例。
可以看到,此处两个 slice s 打印出来,结果是不一样的。看起来就是函数内的 s 比函数外的 s 数据更多了!换句话说,在实际场景中,很有可能因为如下这样的误操作,导致看似操作过 s,但是数据缺丢失了。
package mainimport "fmt"func main() { var s []int s = append(s, 1) printSlice(s) sliceAppend(s) printSlice(s)}func sliceAppend(s []int) { s = append(s, 1) // 此处发生了扩容操作,导致 s 的内存地址改变 printSlice(s)}func printSlice(s []int) { fmt.Printf("len: %v, cap: %v, val: %v\n", len(s), cap(s), s)}// Output:// len: 1, cap: 1, val: [1]// len: 2, cap: 2, val: [1 1]// len: 1, cap: 1, val: [1]
!!如果发生了扩容,修改会在新的内存中!!
所以针对 slice 的操作,务必使用 append 函数返回的 slice 对象进行后续操作,避免出现奇怪的数据异常!
Part6: slice 序列化
如下案例所示,slice 的 zero value 经过默认的 json 库序列化结果是 null,但是初始化的 slice 经过默认的 json 库序列化结果就是 []。
package mainimport ( "encoding/json" "fmt")func main() { var s []int b, _ := json.Marshal(s) fmt.Println(string(b))}// Output:// null
package mainimport ( "encoding/json" "fmt")func main() { s := []int{} b, _ := json.Marshal(s) fmt.Println(string(b))}// Output:// []
package mainimport ( "encoding/json" "fmt")func main() { s := make([]int, 0, 1) b, _ := json.Marshal(s) fmt.Println(string(b))}// Output:// []
package mainimport ( "encoding/json" "fmt")func main() { s := make([]int, 1) b, _ := json.Marshal(s) fmt.Println(string(b))}// Output:// [0]
TODO 如果有更多再补充