参考资料:
Go 语言切片(Slice)
GO编程模式:切片,接口,时间和性能
Go 切片绕坑指南

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

package main

import "fmt"

func main() {
   var numbers []int

   printSlice(numbers)

   if(numbers == nil){
      fmt.Printf("切片是空的")
   }
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) // s= []  s.len= 0  s.cap= 0
}

切片截取
case1:foo和bar的内存是共享的,所以,foo和bar的对数组内容的修改都会影响到对方。

import (
    "bytes"
    "testing"
)

func TestSlice1(t *testing.T) {
    foo := make([]int, 5) // 首先先创建一个foo的slice,其中的长度和容量都是5
    foo[3] = 42           // 然后开始对foo所指向的数组中的索引为3和4的元素进行赋值
    foo[4] = 100
    t.Log("foo=", foo) // foo= [0 0 0 42 100]
    bar := foo[1:4]
    bar[1] = 99
    // foo和bar的内存是共享的,所以,foo和bar的对数组内容的修改都会影响到对方。
    t.Log("foo=", foo) // foo= [0 0 99 42 100]
    t.Log("bar=", bar) // bar= [0 99 42]
}

case2: 数据操作 append() 的示例
append()这个函数在 cap不够用的时候就会重新分配内存以扩大容量,而如果够用的时候不不会重新分享内存!

func TestSlice2(t *testing.T) {
    a := make([]int, 5)
    t.Log("a=", a, " a.len=", len(a), " a.cap=", cap(a)) // a= [0 0 0 0 0]  a.len= 5  a.cap= 5
    b := a[1:3]                                          //此时,a 和 b 的内存空间是共享的
    t.Log("b=", b, " b.len=", len(b), " b.cap=", cap(b)) // b= [0 0]  b.len= 2  b.cap= 4
    a = append(a, 1)                                     //这个操作会让 a 重新分享内存,导致 a 和 b 不再共享
    //b = append(b, 1)                 // 由于b的cap为4,空间够用,因此不需要重新分配内容,此时与a还是共享内存
    t.Log("a=", a, " a.len=", len(a), " a.cap=", cap(a)) // a= [0 0 0 0 0 1]  a.len= 6  a.cap= 10
    t.Log("b=", b, " b.len=", len(b), " b.cap=", cap(b)) // b= [0 0]  b.len= 2  b.cap= 4
    a[2] = 42
    t.Log("a=", a, " a.len=", len(a), " a.cap=", cap(a)) // a= [0 0 42 0 0 1]  a.len= 6  a.cap= 10
    t.Log("b=", b, " b.len=", len(b), " b.cap=", cap(b)) // b= [0 0]  b.len= 2  b.cap= 4
}

case3: dir1 和 dir2 共享内存
虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。

func TestSlice3(t *testing.T) {
    path := []byte("AAAAxBBBBBBBBB")
    t.Log("path =>", string(path), " path.len=", len(path), " path.cap=", cap(path)) //path => AAAAxBBBBBBBBB  path.len= 14  path.cap= 14
    sepIndex := bytes.IndexByte(path, 'x')
    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    t.Log("......................................................")
    t.Log("dir1 =>", string(dir1), " dir1.len=", len(dir1), " dir1.cap=", cap(dir1)) //prints: dir1 => AAAA dir1.len= 4  dir1.cap= 14
    t.Log("dir2 =>", string(dir2), " dir2.len=", len(dir2), " dir2.cap=", cap(dir2)) //prints: dir2 => BBBBBBBBB dir2.len= 9  dir2.cap= 9

    t.Log("......................................................")
    dir1 = append(dir1, "suffix"...)
    t.Log("dir1 =>", string(dir1), " dir1.len=", len(dir1), " dir1.cap=", cap(dir1)) //prints: dir1 => AAAAsuffix dir1.len= 10  dir1.cap= 14
    t.Log("dir2 =>", string(dir2), " dir2.len=", len(dir2), " dir2.cap=", cap(dir2)) //prints: dir2 => uffixBBBB  dir2.len= 9  dir2.cap= 9

}

case4: 如果要解决这个问题,我们只需要修改一行代码。

func TestSlice4(t *testing.T) {
    pathz := []byte("AAAAxBBBBBBBBB")
    sepIndex := bytes.IndexByte(pathz, 'x')
    dirz1 := pathz[:sepIndex:sepIndex] //新的代码使用了 Full Slice Expression,其最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作将会导致重新分配内存。
    dirz2 := pathz[sepIndex+1:]
    t.Log("dirz1 =>", string(dirz1), " dir1.len=", len(dirz1), " dir1.cap=", cap(dirz1)) //dirz1 => AAAA  dir1.len= 4  dir1.cap= 4
    t.Log("dirz2 =>", string(dirz2), " dir2.len=", len(dirz2), " dir2.cap=", cap(dirz2)) //dirz2 => BBBBBBBBB  dir2.len= 9  dir2.cap= 9
    t.Log("......................................................")
    dirz2[0] = 'o'
    t.Log("dirz1 =>", string(dirz1), " dir1.len=", len(dirz1), " dir1.cap=", cap(dirz1)) //dirz1 => AAAA  dir1.len= 4  dir1.cap= 4
    t.Log("dirz2 =>", string(dirz2), " dir2.len=", len(dirz2), " dir2.cap=", cap(dirz2)) //dirz2 => oBBBBBBBB  dir2.len= 9  dir2.cap= 9

    t.Log("......................................................")
    dirz1 = append(dirz1, "suffix"...)
    t.Log("dirz1 =>", string(dirz1), " dir1.len=", len(dirz1), " dir1.cap=", cap(dirz1)) //dirz1 => AAAAsuffix  dir1.len= 10  dir1.cap= 16
    t.Log("dirz2 =>", string(dirz2), " dir2.len=", len(dirz2), " dir2.cap=", cap(dirz2)) //dirz2 => oBBBBBBBB  dir2.len= 9  dir2.cap= 9
}

坑1
下面的代码中虽然通过值传递了s,为什么在函数调用后在外部仍能看到s的变化?
大家都知道切片是指向底层数组的指针,切片本身不存储任何数据。这意味着即使在这里按值传递切片,函数中的切片仍指向相同的内存地址。所以在reverse()内部使用的切片是一个不同的指针对象,但仍将指向相同的内存地址,共享相同的数组。所以在函数调用之后,该数组中的数字重新排列,函数外部的切片与内部的切片共享着相同的底层数组,所以外部的 s 表现出来的就是它也被排序了。

func TestReverse(t *testing.T) {
    var s []int
    for i := 1; i <= 3; i++ {
        s = append(s, i)
    }
    reverse(s)
    t.Log("s=", s) // s= [3 2 1]
} 

func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
}

坑2:在reverse()函数内稍微更改一下代码,在函数里添加单个append调用。它如何改变我们的输出?
当我们调用append时,将创建一个新切片。新切片具有新的“长度”属性,该属性不是指针,但仍指向同一数组。因此,我们函数内的代码最终会反转原始切片所引用的数组,但是原始切片的长度属性还是之前的长度值,这就是造成了上面 1被丢掉的原因。

func TestReverse(t *testing.T) {
    var s []int //  s= []  s.len= 0  s.cap= 0,cap数为0,1,2,4,8...
    for i := 1; i <= 3; i++ {
        s = append(s, i)
    }
    reverse(s)
    t.Log("s=", s) //s= [999 3 2]
}

func reverse(s []int) {
    fmt.Println("s=", s, " s.len=", len(s), " s.cap=", cap(s))
    // s= [1 2 3]  s.len= 3  s.cap= 4
    
    s = append(s, 4)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
    
    fmt.Println("s内=", s, " s内.len=", len(s), " s内.cap=", cap(s))
    // s内= [999 3 2 1]  s内.len= 4  s内.cap= 4
}

坑3:超过容量后,不共享内存
以下测验中,不仅切片长度没有保留,而且切片的顺序也不受影响。为什么?
如前所述,当我们调用append时,会创建一个新的切片。在第二个测验中,此新切片仍指向同一底层数组,因为它具有足够的容量来添加新元素,因此该数组没有更改,但是在此示例中,我们添加了2个元素,而我们的切片没有足够的容量。于是 系统分配了一个新数组,让切片指向该数组。当我们最终在reverse函数内开始反转切片中的元素时,它不再影响我们的初始数组,而是在完全不同的数组上运行。

func TestReverse(t *testing.T) {
    var s []int
    t.Log("s=", s, " s.len=", len(s), " s.cap=", cap(s))
    for i := 1; i <= 3; i++ {
        s = append(s, i)
        t.Log("s=", s, " s.len=", len(s), " s.cap=", cap(s))
    }
    reverse(s)
    t.Log("s=", s) // s= [1 2 3]
}

func reverse(s []int) {
    fmt.Println("s=", s, " s.len=", len(s), " s.cap=", cap(s))
    // s= [1 2 3]  s.len= 3  s.cap= 4
    s = append(s, 4, 5)
    for i, j := 0, len(s)-1; i < j; i++ {
        j = len(s) - (i + 1)
        s[i], s[j] = s[j], s[i]
    }
    fmt.Println("s内=", s, " s内.len=", len(s), " s内.cap=", cap(s))
    // s内= [5 4 3 2 1]  s内.len= 5  s内.cap= 8
}

结论:
只要不超出切片的容量,我们最终就会在main()函数中看到reverse函数对切片进行的更改。我们仍然看不到长度的变化,但是我们将看到切片的底层数组中元素的重新排列。

如果在将切片填充到容量长度后,在s上再调用append(),我们将不会再在main()函数中看到这些更改,因为我们的reverse 函数中的代码将一个新切片指向到了一个完全不同的数组。

从切片或数组派生的切片也会受到影响
如果我们恰巧在代码中创建了从现有切片或数组派生的新切片,那么我们也可以看到相同的效果。例如,如果您调用s2:= s [:]然后将s2传递到我们的reverse()函数中,则可能最终仍会影响s,因为s2和s都指向同一个支持数组。同样,如果我们向s2附加新元素,最终导致其超出支持数组,我们将不再看到对一个切片的更改会影响另一个切片。

严格来说,这不是一件坏事。通过在绝对需要之前不随意复制基础数组,我们最终获得了效率更高的代码,但编写代码时需要考虑到这一点,所以想确保在函数外也能看到函数内程序对切片的更改,那么在函数中一定要把新切片 return 给外部,即使切片是一种引用类型。这也是不要其他编程语言经验带入到 Go上的原因。


一曲长歌一剑天涯
3 声望3 粉丝