go-数组-slice-map

聚合类型包括:数组、slice、map和结构体

数组

数组一旦声明,里面存储的数据类型和数组长度都不能改变了(单个元素值可以通过下标修改(但元素值的类型不能更改))。如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
数组是具有固定长度且拥有0个或者多个相同数据类型元素的序列。由于数组长度固定,所以在Go里面很少直接使用。slice的长度可以增长和缩短,在很多场合下使用的更多。

1
2
3
4
5
6
7
8
9
10
11
var a [3]int // 3个整数的数组
fmt.Println(a[0]) //输出数组的第一个元素
fmt.Println(a[len(a)-1]) //输出数组最后一个元素
// 输出索引和元素
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// 仅输出元素
for _, v := range a {
fmt.Printf("%d\n", v)
}

默认情况下,一个新数组中的元素初始值为元素类型的零值,对于数字来说,就是0。
也可以使用数组字面量根据已组织来初始化一个数组:

1
2
3
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

数组长度由初始化数组元素个数决定的方式:

1
q := [...]int{1, 2, 3}

数组的长度是数组类型的一部分,所以[3]int 和 [4]int 是两种不同的数组类型。数组的长度必须是常量表达式,也就是说,这个表达式的值在程序编译时就可以确定。

1
2
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // 编译错误:不可用将[4]int 赋值给 [3]int

创建和初始化

1
2
3
4
5
6
7
8
9
10
var array [5]int // 声明一个包含5个元素的整型数组
array := [5]int{10, 20, 30, 40, 50} // 声明一个包含5个元素的整型数组,用具体指初始化每个元素
array := [...]int{10, 20, 30, 40, 50} // 声明一个整型数组,用具体值初始化每个元素,容量由初始化值的数量决定
array := [5]int{1: 10, 2: 20} // 声明一个有5个元素的数组,用具体值初始化索引为1和2的元素,其余元素保持零值
array := [5]*int{0: new(int), 1: new(int)} // 声明包含5个元素的指向整型的数组,用整型指针初始化索引为0和1的数组元素
*array[0] = 10 // 为索引为0的元素赋值

使用数组

1
2
3
4
5
6
7
8
9
var array1 [5]string // 声明一个包含5个元素的字符串数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"} // 声明一个包含5个元素的字符串数组,用颜色初始化数组
array1 = array2 // 把array2的值赋值到array1,复制之后,两个数组的值完全一样, 即他们的长度和元素类型都一样
var array3 [4]string // 声明一个包含4个元素的字符串数组
array3 = array2 // 报错, 两个数组长度不一样

在函数建传递数组

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。如果这个变量时一个数组,意味着整个数组,不管有多长,都会完整赋值,并传递给函数。所以我们可以只传入指向数组的指针就好:

1
2
3
4
5
6
7
var array [le6]int // 分配一个需要8 MB的数组
foo(&array) // 将数组的地址传递给函数foo
func foo(array *[le6]int) { // 函数foo接收一个指向100万个整型值的数组的指针
...
}

数组优缺点

  • 优点
    占用内存是连续的,元素类型相同,数据索引迭代快
  • 缺点
    长度和数据类型固定

slice

指针、长度、容量
slice表示一个拥有相同类型元素的可变长度的序列。slice通常写成[]T,其中元素的类型都是T; 看上去就像是没有长度的数组类型
数组和slice是紧密关联的。slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部的元素,而这个数组称为slice的底层数组。slice有三个属性:指针、长度和容量

slice操作符s[i:j] 会创建一个新的slice

slice空判断:len(s) == 0

slice追加:

1
2
3
4
var runes []rune
for _, r := range "Hello, World" {
runes = append(runes, r)
}

创建和初始化

切片的容量必须大于等于长度
使用 make 或者 字面量 创建切片

1
2
3
4
5
6
7
8
9
10
11
slice := make([]string, 5) //make 创建了一个字符串切片,其长度和容量都是5个元素
slice := make([]int, 3, 5) //make 创建了一个整型切片,其长度为3个元素,容量为5个元素
slice := []string{99: ""} //字面量 创建字符串切片,使用空字符串初始化第100个元素。记住,如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值的时候,才会创建切片
array := [3]string{10, 20, 30} // 创建有3个元素的整型数组
slice := []int{10, 20, 30} // 创建长度和容量都是3的整型切片
var slice []int // 创建nil整型切片

使用切片

1
2
3
4
slice := []int{10, 20, 30, 40, 50} // 创建一个整型切片,其容量和长度都是5个元素
slice[1] = 25 // 修改索引为1的元素的值
newSlice := slice[1:3] // 创建一个新切片,其长度为2个元素,容量为4个元素

说明:
对于底层数组容量是k的切片slice[i:j]来说:
长度:j - i
容量:k - i
所以上面新切片的长度为2(3-1),容量为4(5-1)

由于两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另外一个切片也能感知:

1
2
3
4
5
slice := []int{10, 20, 30, 40, 50} // 创建一个整型切片,其容量和长度都是5个元素
newSlice := slice[1:3] // 创建一个新切片,其长度为2个元素,容量为4个元素
newSlice[1] = 35 // 修改newSlice索引为1的元素,同时也修改了原来的slice的索引为2的元素

切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行异常:

1
2
3
4
5
slice := []int{10, 20, 30, 40, 50} // 创建一个整型切片,其容量和长度都是5个元素
newSlice := slice[1:3] // 创建一个新切片,其长度为2个元素,容量为4个元素
newSlice[3] = 35 // 修改newSlice索引为3的元素,这个元素对于newSlice来说并不存在,因其长度只有2,虽然容量为4,我们需要通过append的方式往里追加,才能把长度增长到3

切片增长:

1
2
3
4
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice = append(newSlice, 60) // 使用原有的容量来分配一个新元素,将新元素赋值为60,其长度由2变成了3,同时slice的元素值40也随之变为了60

如果切片的底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值:

1
2
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)

当这个append操作完成后,newSlice拥有一个全新的底层数组,与原有的底层数组分离(操作新切片不会影响原来的),这个数组的容量是原来的两倍

切片增长规则:
函数append会智能的处理底层数组的容量增长。在切片的容量小于1000个元素的时候,总是会成倍的增加容量。一旦元素个数超过1000,容量的增长因子会设为1.25,也就是会每次增加25%的容量。

创建切片时的3个索引:
第三个索引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。
切片slice[i:j:k] 或[2:3:4]来说:
长度:j - i 或 3 - 2 = 1
容量:k - i 或 4 - 2 = 2

1
2
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
slice := source[2:3:4] // 将第三个元素切片,并限制容量,其长度为1个元素,容量为2个元素

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,就可以安全的进行后续修改:

1
2
3
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
slice := source[2:3:3] // 对第三个元素做切片,并限制容量,其长度和容量都是1个元素
newSlice = append(slice, "Kiwi") // 向slice追加新字符串

如果不加第三个索引,由于剩余的所有容量都属于slice,向slice追加Kiwi会改变原有底层数组索引为3的元素的值Banana。不过这里我们限制了slice的容量为1.当我们第一次对slice调用append的时候,会创建一个新的底层数组,这个数组包括2个元素,并将水果Plum复制进来,再追加新水果Kiwi,并返回一个引用了这个底层数组的的新切片.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
fmt.Println("%d", source)
slice := source[2:3:3]
fmt.Println("%d", slice)
slice = append(slice, "Kiwi")
fmt.Println("%d", slice)
fmt.Println("%d", source)
}
输出:
%d [Apple Orange Plum Banana Grape]
%d [Plum]
%d [Plum Kiwi]
%d [Apple Orange Plum Banana Grape]

内置函数append也是一个可变参数的函数,这意味着可以在一次调用传递多个追加的值。如果使用…运算符,可以将一个切片的所有元素追加到另外一个切片里:

1
2
3
4
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Printf("%v\n", append(s1, s2...)) // 输出: [1 2 3 4]

迭代切片:

1
2
3
4
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}

在函数间传递切片

在函数间的传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数建复制和传递切片成本也很小。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以讲切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组。

1
2
3
4
5
6
7
slice := make([]int, le6) // 分配包含100万个整型值的切片
slice = foo(slice) // 将slice传递到函数foo
func foo(slice []int) []int { // 函数foot接收一个整型切片,并返回这个切片
...
return slice
}

slice优缺点

  • 优点
    可按需自动增长和缩小, 底层分配的内存连续,索引迭代快; 函数调用时的值传递内存占用小、效率高(只复制切片本身,不需要复制底层数组)
  • 缺点
    固定类型,

map

无序键值对
在Go语言中,map是散列表的引用,map的类型是map[K]V, 其中K和V是字典的键和值对应的数据类型。map中所有的键都拥有相同的数据类型,同时所有的值也都拥有相同的数据类型,但是键的数据类型和值的数据类型不一定相同。
键的类型K,必须是可以通过操作符==来进行比较的数据类型,所以map可以检测某一个键是否已经存在。虽然浮点型是可以比较的,但是比较浮点型的相等性不是一个好主意; 切片、函数以及包含切片的结构类型不能作为映射的键

创建和初始化

通过 内置函数make和映射(map)字面量创建

1
2
3
4
5
6
7
8
9
10
11
ages := make(map[string]int) //内置函数make 创建映射,键的类型是string,值的类型是int
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"} //map字面量 创建映射,键和值的类型都是string,使用两个键值对初始化映射
ages := map[string]int { // 使用map的字面量来创建一个带初始化键值对元素的字典
"alice": 31,
"charlie": 34,
}
// 等价于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

新的空map的另外一种表达式:map[string]int{}

使用映射

map类型的零值是nil

1
2
3
var ages map[string]int // 通过声明映射创建了一个nil映射
fmt.Println(ages == nil) // true
fmt.Println(len(ages) == 0) // true

如果向未初始化的map(即零值map)设置元素会报错:

1
ages["carol"] = 21 //报错:向nil映射中的赋值

所以设置元素之前必须初始化map

判断元素是否在map中:

1
2
3
4
age, ok := ages["bob"]
if !ok {
/*"bob"不是字典中的键,age == 0*/
}

通常这两条语句合并成一条语句:

1
if age, ok := ages["bob"]; !ok {/*...*/}

和slice一样,map不可比较,唯一合法的比较就是和nil做比较。为了判断两个map是否拥有相同的键和值,必须写一个循环:

1
2
3
4
5
6
7
8
9
10
11
12
func equal(x, y map([string]int) bool ) {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}

注意这里使用!ok 来区分 “元素不存在” 和 “元素存在但值为零” 的情况。如果简单的写成了 xv != yv, 那么如下调用将错误的报告两个map是相等的:

1
2
3
equal(map[string]int{"A": 0}, map[string]int{"B": 42}) // 如果equal函数写法错误,结果为true
可以使用内置函数delete来从字典中根据键移除一个元素:

delete(ages, “alice”) //移除元素 ages[“alice”]

1
即使键不再map中,上面的操作也是安全的。map使用给定的键来查找元素,如果对应的元素不存在,就返回值类型的零值:

ages[“bob”] = ages[“bob”] + 1] // 1

1
即使键值bob不存在,也可以工作,因为ages["bob"]的值是1

ages[“bob”] += 1
// or
ages[“bob”]++

1
2
但是map元素不是一个变量,不可以获取它的地址,比如如下是不对的:

_ = &ages[“bob”] //编译错误,无法获取map元素的地址

1
2
3
无法获取map元素的地址的一个原因是map的增长可能会导致已有元素被重新散列到新的存储位置,这样就可能是的获取的地址无效。
map元素的迭代顺序是不固定的,不同的实现方法会使用不通的散列算法,得到不同的元素顺序。

在函数建传递映射

在函数建传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改

Go没有提供集合类型,有map的键都是唯一的,就可以使用map来实现这个功能

小结

  • 数组是构造切片和映射的基石
  • Go语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据
  • 内置函数make可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值
  • 切片有容量限制,不过可以使用内置的append函数扩展容量
  • 映射的增长没有容量或者任何限制
  • 内置函数len可以用来获取切片或者映射的长度
  • 内置函数cap只能用于切片
  • 通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。但是切片不能用作映射的键
  • 将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构
  • 切片进行append的时候,如果容量已经满了,则会创建一个新的底层数组