漫谈 Golang 之 slice

Part1: array & slice

// define a slice
s := make([]int, 10)
// define an array
a := [10]int{}
  • 定义 slice 第一种方式
    var s []int
  • 定义 slice 第二种方式
    s := make([]int, len, cap)
  • array
    var a [length]type
    // var a [10]int

Part2: 类型

package main
import "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 main
import "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 main
import "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 数据填充?

  1. 声明一个 slice 直接开始 append
  2. 声明固定长度 slice 后开始 append
  3. 声明固定长度 slice 后,使用 index 进行数据填充

可以看到方法三是最快的。

对于大数组的赋值,常用的优化方式还有一种 BCE,可以参考这篇文章中的用法,不再赘述。golang 边界检查优化

package main
import "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 main
import "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 main
import "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 main
import (
    "encoding/json"
    "fmt"
)
func main() {
    var s []int
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}
// Output:
// null
package main
import (
    "encoding/json"
    "fmt"
)
func main() {
    s := []int{}
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}
// Output:
// []
package main
import (
    "encoding/json"
    "fmt"
)
func main() {
    s := make([]int, 0, 1)
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}
// Output:
// []
package main
import (
    "encoding/json"
    "fmt"
)
func main() {
    s := make([]int, 1)
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}
// Output:
// [0]

TODO 如果有更多再补充


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nickchenyx@gmail.com

Title:漫谈 Golang 之 slice

Count:1.2k

Author:nickChen

Created At:2022-07-19, 22:39:40

Updated At:2023-05-08, 23:27:10

Url:http://nickchenyx.github.io/2022/07/19/golang-slice-dive-into/

Copyright: 'Attribution-non-commercial-shared in the same way 4.0' Reprint please keep the original link and author.