Go数据结构之数组与切片

sown · · 354 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

数组的长度是声明的时候就固定好的,后面不可能变大,而且长度和容量相等。

切片的长度和容量后面可以随着元素增多而增长,但是容量不可能小于长度。

正文

声明&初始化

在 Go 中声明即初始化,如果在声明的时候没有初始化值,那么就会赋值为声明类型的「零值」。

func TestDemo1(t *testing.T) {
    // 数组
    var array1 [5]int        // 只需设置长度,后面不可变
    var array2 = new([5]int) // 返回指针

    // 切片
    var slice1 []int
    var slice2 = make([]int, 5, 5) // 设置长度、容量,后面可变

    t.Log("array1 val:", array1)      // [0 0 0 0 0]
    t.Log("array1 len:", len(array1)) // 5
    t.Log("array1 cap:", cap(array1)) // 5

    fmt.Println("")

    t.Log("array2 val:", array2)      // &[0 0 0 0 0]
    t.Log("array2 len:", len(array2)) // 5
    t.Log("array2 cap:", cap(array2)) // 5

    fmt.Println("")

    t.Log("slice1 val:", slice1)      // []
    t.Log("slice1 len:", len(slice1)) // 0
    t.Log("slice1 cap:", cap(slice1)) // 0

    fmt.Println("")

    t.Log("slice2 val:", slice2)      // [0 0 0 0 0]
    t.Log("slice2 len:", len(slice2)) // 5
    t.Log("slice2 cap:", cap(slice2)) // 5
}

在声明的时候就初始化:

func TestDemo2(t *testing.T) {
    // 数组
    var array1 = [5]int{4: 1, 2: 5}
    var array2 = [...]int{4: 1, 2: 5}

    // 切片
    var slice1 = []int{4: 1, 2: 5}
    var slice2 = array1[:] // 从数组截取来的切片

    t.Log("array1 val:", array1)      // [0 0 5 0 1]
    t.Log("array1 len:", len(array1)) // 5
    t.Log("array1 cap:", cap(array1)) // 5

    fmt.Println("")

    t.Log("array2 val:", array2)      // [0 0 5 0 1]
    t.Log("array2 len:", len(array2)) // 5
    t.Log("array2 cap:", cap(array2)) // 5

    fmt.Println("")

    t.Log("slice1 val:", slice1)      // [0 0 5 0 1]
    t.Log("slice1 len:", len(slice1)) // 5
    t.Log("slice1 cap:", cap(slice1)) // 5

    fmt.Println("")

    t.Log("slice2 val:", slice2)      // [0 0 5 0 1]
    t.Log("slice2 len:", len(slice2)) // 5
    t.Log("slice2 cap:", cap(slice2)) // 5
}

添加&更新元素值

数组因为长度固定,且的值都是初始化好了的,所以只有更新。

切片更新操作和数据一样,只不过新增元素只能通过 append() 方法。

append():将元素追加大切片的末尾,如果容量不够,会进行扩容。
func TestDemo3(t *testing.T) {
    // 数组
    var array1 = [5]int{4: 1, 2: 5}
    array1[0] = 100 // 更新
    array1[4] = 100 // 更新

    // 切片
    var slice1 = []int{4: 1, 2: 5}
    array1[4] = 100 // 更新
    //array1[5] = 100 // 报错
    slice1 = append(slice1, 1) // 切片增加元素只能使用此方法

    t.Log("array1 val:", array1)      // [100 0 5 0 100]
    t.Log("array1 len:", len(array1)) // 5
    t.Log("array1 cap:", cap(array1)) // 5

    fmt.Println("")

    t.Log("slice1 val:", slice1)      // [0 0 5 0 1 1]
    t.Log("slice1 len:", len(slice1)) // 6
    t.Log("slice1 cap:", cap(slice1)) // 10
}

表达式

数组与切片,都可以使用表达式截取,截取之后的数据它的类型为切片。

func TestDemo4(t *testing.T) {
    array1 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}

    slice1 := array1[3:]              // 从 index 3 取到 index end
    t.Log("slice1 val:", slice1)      // [3 0 5 6 2 8 4]
    t.Log("slice1 len:", len(slice1)) // 7
    t.Log("slice1 cap:", cap(slice1)) // 7

    fmt.Println("")

    slice2 := array1[3:4]             // 从 index 3 取到 index 4
    t.Log("slice2 val:", slice2)      // [3]
    t.Log("slice2 len:", len(slice2)) // 1
    t.Log("slice2 cap:", cap(slice2)) // 7

    fmt.Println("")

    slice3 := array1[3:6:6]           // 从 index 3 取到 index 6,容量取到 index 6
    t.Log("slice3 val:", slice3)      // [3 0 5]
    t.Log("slice3 len:", len(slice3)) // 3
    t.Log("slice3 cap:", cap(slice3)) // 3

    fmt.Println("")

    slice4 := array1[3:6:9]           // 从 index 3 取到 index 6,容量取到 index 9
    t.Log("slice4 val:", slice4)      // [3 0 5]
    t.Log("slice4 len:", len(slice4)) // 3
    t.Log("slice4 cap:", cap(slice4)) // 6
}

遍历

使用 for、range

func TestDemo5(t *testing.T) {
    array1 := [...]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    slice1 := make([]int, 5, 5)
    for k, v := range array1 {
        fmt.Println(k, "-", v)
    }
    fmt.Println()
    for k, v := range slice1 {
        fmt.Println(k, "-", v)
    }
}

比较

数组与数组可以使用 == 比较,不能与 nil 比较

切片与切片不能使用 == 比较,可以使用 reflect.DeepEqual 比较,可以与 nil 比较

func TestDemo6(t *testing.T) {
    array1 := [...]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    array2 := [...]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 9}
    array3 := [...]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 9}

    t.Logf("array1 == array2 %t\n", array1 == array2) // false
    t.Logf("array2 == array3 %t\n", array2 == array3) // true
    //t.Logf("%t\n", array2 == nil) // 会报错,数组不能与nil比

    slice1 := make([]int, 5, 5)
    var slice2 []int
    slice3 := []int{4: 0}

    // t.Logf("%t\n", slice1 == slice2) // 会报错,切片与切片不能比
    t.Logf("slice1 == nil %t\n", slice1 == nil) // false
    t.Logf("slice2 == nil %t\n", slice2 == nil) // true
    t.Logf("slice3 == nil %t\n", slice3 == nil) // false

    t.Logf("slice1 == slice2 %t\n", reflect.DeepEqual(slice1, slice2)) // false
    t.Logf("slice2 == slice3 %t\n", reflect.DeepEqual(slice2, slice3)) // false
    t.Logf("slice1 == slice3 %t\n", reflect.DeepEqual(slice1, slice3)) // true
}

删除

需使用 append()、切片表达式 结合来完成

func TestDemo7(t *testing.T) {
    slice1 := []int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    slice1 = append(slice1[:2], slice1[3:]...)
    t.Log(slice1)
}

扩展

数组与切片的关系

数组为值类型,切片为引用类型,他们又有何关系呢?

程序示例:

func TestDemo8(t *testing.T) {
    array1 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}

    slice1 := array1[:]

    t.Log("slice1 val:", slice1)      // [9 1 7 3 0 5 6 2 8 4]
    t.Log("slice1 len:", len(slice1)) // 10
    t.Log("slice1 cap:", cap(slice1)) // 10

    array1[9] = 96969696 // array1 的修改会影响到 slice1

    fmt.Println("")

    t.Log("slice1 val:", slice1)      // [9 1 7 3 0 5 6 2 8 96969696]
    t.Log("slice1 len:", len(slice1)) // 10
    t.Log("slice1 cap:", cap(slice1)) // 10
}

在这个示例程序中,可以说 slice1 是 array1 的引用。

不光是在示例程序中,这种在数组上通过表达式截取出的切片,为数组的引用,就算在程序中,直接声明一个新切片(var slice1 []int),在切片的底层实现,其实也是引用了一个数组。

他们的关系就是:数组是切片的底层实现,切片是数组的引用。

切片扩容

在示例程序 TestDemo8 中,slice1 会一直引用 array1 么?

一般情况下是这样,但有种情况下引用会发生变化,就是在 slice 发生扩容的情况下

func TestDemo9(t *testing.T) {
    array1 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    slice1 := array1[:] // 从 array1 截取出 slice1

    t.Log("slice1 val:", slice1)      // [9 1 7 3 0 5 6 2 8 4]
    t.Log("slice1 len:", len(slice1)) // 10
    t.Log("slice1 cap:", cap(slice1)) // 10

    slice1 = append(slice1, 9) // 进行扩容后,slice1 指向了新的底层数组,不在是 array1 的引用
    array1[9] = 96969696

    fmt.Println("")

    t.Log("slice1 val:", slice1)      // [9 1 7 3 0 5 6 2 8 4 9]
    t.Log("slice1 len:", len(slice1)) // 11
    t.Log("slice1 cap:", cap(slice1)) // 20
}

当切片添加新元素,发现容量不够时,会开辟一个新的底层数组,然后把旧数组的数据和添加的新元素一并拷贝到新数组中。

扩容策略:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)

深复制

靠扩容解决引用问题,显得不是那么优雅。

可以使用 copy() 进行深复制

func TestDemo10(t *testing.T) {
    array1 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}

    var slice1 = make([]int, 10, 10)
    copy(slice1, array1[:]) // 深复制,slice1 不会引用 array1
    array1[9] = 96969696

    t.Log("slice1 val:", slice1)      // [9 1 7 3 0 5 6 2 8 4]
    t.Log("slice1 len:", len(slice1)) // 10
    t.Log("slice1 cap:", cap(slice1)) // 10

    fmt.Println("")

    t.Log("array1 val:", array1)      // [9 1 7 3 0 5 6 2 8 96969696]
    t.Log("array1 len:", len(array1)) // 10
    t.Log("array1 cap:", cap(array1)) // 10
}

数组

一段连续的内存空间。

make

make 只能用于 slice、map、channel,返回的初始化后的(非零)值。

引用类型

  • 切片
  • 字典
  • 通道
  • 函数

值类型

  • 数组
  • 基础数据类型
  • 结构体类型

总结

  1. 切片是数组的引用,数组是切片的底层实现。
  2. 数组的长度(len)等于容量(cap),切片的长度(len)小于等于容量(cap)。
  3. 数组声明的时候默认就会初始化,值为类型的「零值」;切片声明的时候,如果不初始化,值是 nil。
  4. 使用 copy() 深复制解决引用问题。

文章示例代码

Sown专栏地址:https://segmentfault.com/blog/sown


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:sown

查看原文:Go数据结构之数组与切片

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

354 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传