Go数据结构之数组与切片

贾琏
• 阅读 1370

前言

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

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

正文

声明&初始化

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

func TestDemo1(t *testing.T) {
    // 数组 注意声明时数组的长度应为常数,否则报错 non-constant array bound
    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
}

传递

切片,如果不使用 copy() 进行深复制出一个新的切片,直接传递过去的切片底层还是同一个数组,当然,append() 发生了扩容之后,就不会是同一个数组了。

数组,直接传递会变成两个数组,如果运用了指针,会指向同一个。

func TestDemo11(t *testing.T) {
    // 切片,底层引用的还是同一个数组
    slice1 := []int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    go func(v []int) {
        //v = append(v, 20) // 扩容后底层数组就变了
        v[0] = 99999
        t.Log(v)
    }(slice1)
    time.Sleep(1 * time.Second)
    t.Log("slice1", slice1) // slice1 [99999 1 7 3 0 5 6 2 8 4]

    fmt.Println()

    // 切片,copy() 深复制后,底层不是同一个数组
    slice2 := []int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    slice2Copy := make([]int, 10, 10)
    copy(slice2Copy, slice2)
    go func(v []int) {
        v[0] = 99999
    }(slice2Copy)
    time.Sleep(1 * time.Second)
    t.Log("slice2", slice2) // slice2 [9 1 7 3 0 5 6 2 8 4]

    fmt.Println()

    // 数组,不是同一个
    array1 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    go func(v [10]int) {
        v[0] = 99999
    }(array1)
    time.Sleep(1 * time.Second)
    t.Log("array1", array1) // array1 [9 1 7 3 0 5 6 2 8 4]
    fmt.Println()

    // 数组,同一个
    array2 := [10]int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    go func(v *[10]int) {
        v[0] = 99999
    }(&array2)
    time.Sleep(1 * time.Second)
    t.Log("array2", array2) // array2 [99999 1 7 3 0 5 6 2 8 4]

    fmt.Println()

    // 数组 同一个
    array3 := new([10]int)
    go func(v *[10]int) {
        v[0] = 99999
    }(array3)
    time.Sleep(1 * time.Second)
    t.Log("array3", array3) // array3 &[99999 0 0 0 0 0 0 0 0 0]
}

效果和下面一样

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

    fmt.Println()

    slice1 := []int{9, 1, 7, 3, 0, 5, 6, 2, 8, 4}
    slice2 := slice1
    slice2[0] = 999
    t.Log("slice1", slice1) // slice1 [999 1 7 3 0 5 6 2 8 4]
    t.Log("slice2", slice2) // slice2 [999 1 7 3 0 5 6 2 8 4]
}

数组

一段连续的内存空间。

make

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

引用类型

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

值类型

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

总结

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

文章示例代码

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

点赞
收藏
评论区
推荐文章
九路 九路
4年前
一篇文章彻底弄懂理解和高效运用切片(slice)
slice,中文多译为“切片”,是Go语言在数组之上提供的一个重要的抽象数据类型。在Go语言中,绝大多数需要使用数组的场合,切片都实现了完美替代。并且和数组相比,切片提供了更通用、功能更强大且便捷的数据序列访问接口。1.切片究竟是什么在对切片一探究竟之前,我们先来简略了解一下Go语言中的数组。Go语言数组是一个固定长度的、容纳同构类型元素的
go语言中,数组与切片的区别?
切片是Go语言核心的数据结构,然而刚接触Go的程序员经常在切片的工作方式和行为表现上被绊倒。比如,明明说切片是引用类型但在函数内对其做的更改有时候却保留不下来,有时候却可以。究其原因是因为我们很多人用其他语言的思维来尝试猜测Go语言中切片的行为,切片这个内置类型在Go语言底层有其单独的类型定义,而不是我们通常理解的其他语言中数组的概念。文章
Python进阶者 Python进阶者
3年前
一篇文章带你了解CSS单位相关知识
大家好,我是皮皮,今天给大家分享一些前端的知识。一、了解CSS单位测量长度的单位可以是绝对的,例如像素,点等,也可以是相对的,例如百分比(%)和em单位。指定CSS单位对于非零值是必须的,因为没有默认单位。丢失或忽略单位将被视为错误。但是,如果该值为0,则可以省略该单位(毕竟,零像素与零英寸是一样的)。注意:长度是指距离测量。长度包括数字值
Wesley13 Wesley13
3年前
Go 定长的数组
1.Go语言数组的简介  几乎所有的计算机语言都有数组,应用非常的广泛。同样,在Go语言中也有数组并且在数组的基础上还衍生出了切片(slice)。数组是一系列同一类型数据的集合,数组中包含的每个数据被称为数组元素,一个数组包含的元素个数被称为数组的长度,这是数组的基本定义。  在Go语言中数组是一个值类型(ValueType)
Wesley13 Wesley13
3年前
Java 集合类
为什么使用集合数组长度是固定,如果要改变数组的长度需要创建新的数组将旧数组里面的元素拷贝过去,使用起来不方便。java给开发者提供了一些集合类,能够存储任意长度的对象,长度可以随着元素的增加而增加,随着元素的减少而减少,使用起来方便一些。数组和集合的区别区别1:数组既可以存储基本数据类型,又可以存储引用数据类型,基本数据类
Wesley13 Wesley13
3年前
Go 变量声明
变量命名命名方法varnametype是定义单一变量的语法packagemainimport"fmt"funcmain(){varageint//variabledeclarationfmt.Println("Mya
Wesley13 Wesley13
3年前
Java基础14
1.二位数组可以看成以数组为元素的数组2.java中多维数组的声明和初始化一样,应该从高维到低维的顺序进行,例如1intanewint3;2a0newint2;3a1newint4;4a2newint3;5inttnew
Wesley13 Wesley13
3年前
Java中的字符串的最大长度
Java中的字符串的最大长度看String的源码可以看出来,String实际存储数据的是charvalue\\,数组的长度是int类型,整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^311,那么说明了数组的长度是0~2^311,那么计算一下就是(2^31121474
Wesley13 Wesley13
3年前
Java中ArrayList的使用
ArrayList类是一个特殊的数组动态数组。来自于System.Collections命名空间;通过添加和删除元素,就可以动态改变数组的长度。优点:1、支持自动改变大小2、可以灵活的插入元素3、可以灵活的删除元素局限:比一般的数组的速度慢一些;用法一、初始化:1、不初始化容量ArrayList
Wesley13 Wesley13
3年前
Java数组的声明与创建
今天在刷Java题的时候,写惯了C发现忘记了Java数组的操作,遂把以前写的文章发出来温习一下。首先,数组有几种创建方式?Java程序中的数组\\必须先进行初始化才可以使用,\\所谓初始化,就是为数组对象的元素分配内存空间,并为每个数组元素指定初始值,而在Java中,数组是静态的,数组一旦初始化,长度便已经确定,不能再随意更改。
小万哥 小万哥
1年前
C 语言数组教程:定义、访问、修改、循环遍历及多维数组解析
C数组数组用于将多个值存储在单个变量中,而不是为每个值声明单独的变量。要创建数组,请定义数据类型(例如int)并指定数组名称,后面跟着方括号。要将值插入其中,请使用逗号分隔的列表,并在花括号内使用:cintmyNumbers25,50,75,100