跳至主要內容

Golang 切片(Slice)和数组

安格后端golanggogolang大约 9 分钟...

// 数组: new([len]Type)
arr := new([5]int)
arr := [5]int{1, 2} // [5]int{1, 2, 0, 0, 0}
arr := [5]{1, 2, 3, 4, 5}
arr := [...]{1, 2, 3, 4, 5} // [5]int{1, 2, 3, 4, 5}
arrKV := [...]int{1: 10, 6: 20, 30} // [8]int{0, 10, 0, 0, 0, 0, 20, 30}


// 切片: make([]Type, size[, cap])
slice := make([]int, 0) // []int{}
slice := make([]int, 5) // []int{0, 0, 0, 0, 0}
slice := make([]int, 5, 10) // []int{0, 0, 0, 0, 0}

// 在预先知道所需切片大小时可以预先分配好底层数组, 避免 append 时频繁扩容
slice := make([]int, 0, 100) // []int{}

// 从现有数组/切片中截取: arr[start:end:max]
// 其中: start <= end <= max <= len(arr)
arr := [20]int{}
slice := arr[1:5:10]

1. 数组[1]

1.1 概念

数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 [5]int[10]int 是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的。

数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目(也称为长度或者数组大小)必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2GB

声明的格式是:

var identifier [len]type

例如:

var arr1 [5]int

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)

那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2 的类型是 [5]int

1.2 数组常量

如果数组值已经提前知道了,那么可以通过 数组常量 的方法来初始化数组,而不用依次使用 []= 方法(所有的组成元素都有相同的常量语法)。

var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22}
var arrLazy1 = []int{5, 6, 7, 8, 22}	//注:初始化得到的实际上是切片slice
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
var arrKeyValue1 = []string{3: "Chris", 4: "Ron"}	//注:初始化得到的实际上是切片slice


 

 

第一种变化:

var arrAge = [5]int{18, 20, 15, 22, 16}

注意 [5]int 可以从左边起开始忽略:[10]int {1, 2, 3} :这是一个有 10 个元素的数组,除了前三个元素外其他元素都为 0

第二种变化:数组长度写成 ...

var arrLazy = [...]int{5, 6, 7, 8, 22}

... 同样可以忽略,从技术上说它们其实变成了切片。

第三种变化:key: value 语法

var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}

只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串,所以输出结果为:

Person at 0 is
Person at 1 is
Person at 2 is
Person at 3 is Chris
Person at 4 is Ron

在这里数组长度同样可以写成 ...

// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"Tom" 6:"Dav" ]
var arr = [...]string{3: "Chris", 4: "Ron", "Tom", "Dav"}

// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"Tom" 6:"Dav" 7:"" 8:"" 9:"" ]
var arr = [10]string{3: "Chris", 4: "Ron", "Tom", "Dav"}

// [ 0:"" 1:"" 2:"" 3:"Chris" 4:"Ron" 5:"" 6:"" 7:"" 8:"Tom" 9:"Dav" ]
var arr = [10]string{3: "Chris", "Ron", 8: "Tom", "Dav"}

2. 切片[2]

2.1 概念

切片 (slice) 是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。

切片是可索引的,并且可以由 len() 函数获取长度。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0, 最大为相关数组的长度:切片是一个 长度可变的数组

切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)

多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。

优点:因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。

注意:绝对不要用指针指向切片。切片本身已经是一个引用类型,所以它本身就是一个指针!!

声明切片的格式是:

var identifier []type // 不需要说明长度

一个切片在未初始化之前默认为 nil,长度为 0。

切片的初始化格式是:

// start <= end <= max <= len(arr1)
var slice1 []type = arr1[start:end:max]

这表示 slice1 是由数组 arr1start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end:max 被称为切片表达式)。所以 slice1[0] 就等于 arr1[start]。这可以在 arr1 被填充前就定义好。

如果某个人写:var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1

// 示例
var arr = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := arr[:]     // [0 1 2 3 4 5 6 7 8 9]
s2 := arr[1:5]   // [1 2 3 4]
s3 := arr[1:]    // [1 2 3 4 5 6 7 8 9]
s4 := arr[5:]    // [5 6 7 8 9]
s5 := arr[1:5:8] // [1 2 3 4]

注意:切片表达式中的 max 表示向 slice1 append 元素时使用的底层数组 arr1 对应的索引最大只能使用到 max-1,超出 max-1slice1 便会开辟一个新的底层数组并复制 arr1startmax-1 的值到新数组中,并在新数组末尾继续 append 新的元素。例如:

var arr = [...]int{0, 1, 2, 3, 4, 5, 6}
var s = arr[2:4:5]  // [2 3]
fmt.Println(s, arr) // [2 3] [0 1 2 3 4 5 6]

s = append(s, 100)  // 此时还共享 arr 地址
fmt.Println(s, arr) // [2 3 100] [0 1 2 3 100 5 6]

s = append(s, 200)  // 此时已新开辟新的数组, 不再和 arr 共享地址
fmt.Println(s, arr) // [2 3 100 200] [0 1 2 3 100 5 6]

s[1] = 300          // 不再和 arr 共享地址
fmt.Println(s, arr) // [2 300 100 200] [0 1 2 3 100 5 6]

2.2 用 make() 创建一个切片

当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:var slice1 []type = make([]type, len)

也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。

所以定义 s2 := make([]int, 10),那么 cap(s2) == len(s2) == 10

make() 接受 2 个参数:元素的类型以及切片的元素个数。

如果你想创建一个 slice1,它不占用整个数组,而只是占用 len 个项,那么只要:slice1 := make([]type, len, cap)

make() 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数。

所以下面两种方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

2.3 new() 和 make() 的区别

看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new(T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 *T 的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体,相当于 &T{}
  • make(T) 返回一个类型为 T 的初始值,它 只适用 于 3 种内建的引用类型:切片mapchannel

换言之,new() 函数分配内存,make() 函数初始化。

new() 是一个函数,不要忘记它的括号


  1. 《the-way-to-go》 7.1 数组open in new window ↩︎

  2. 《the-way-to-go》 7.2 切片open in new window ↩︎

上次编辑于:
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3