0%

Go基础之内建容器

数组

数组的类型名是 [n]elementType

1
2
3
4
5
6
7
8
var arr0 []int      // 这种写法是不对的,该写法为数组的切片

var arr1 [5]int // 定义数组的同时指定数组的长度
arr2 := [3]int{1, 3, 5} // 使用短变量声明的同时赋初始值
arr3 := [10]int{0:3,8:3} // 可以在指定数组长度之后,使用 下标:内容 的方式指定特定的值。
arr3 := [...]int{0:3,8:2} // 若是没有指定数组长,则默认使用最大下标的值加1为长度值
arr4 := [...]int{2, 4, 6, 8, 10} // 长度交给编译器 使用... 代替
var grid [4][5]int

注意

  • 变量名写在数组之前
  • 不对数组长度规定的时候,一定要使用...来代替长度。
  • Go数组的度的长度是固定的,一旦指定,不可以变化,但是数组的值是可以改变的。并且数组的长度是数组类型的组成部分,[10]int和[20]int表示的是不同的类型
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var a = []int{1,2,43}
fmt.Printf("%T", a) //[]int

var b = [3]int{1,2,43}
fmt.Printf("%T", b) //[3]int
}

注意:数组的声明中[]中一定要有长度限制,或者使用...代替,此时会自动设置为实际长度,如果什么都没有,则是切片的声明方式,内部的执行过程是先创建一个数组,然后再赋值给了一个切片。

数组的遍历

1
2
3
4
// 一般方法
for i := 0; i < len(arr3); i++{
fmt.Println(arr3[i])
}

但是在遍历数组的时候,有一个专门的关键字range关键字

1
2
3
for i := range arr3 {   // range关键字专门用于获取数组的下标
fmt.Println(arr3[i])
}

rang关键字也可以用来获取值

1
2
3
4
5
6
7
8
for i,v := range arr3 {   // range关键字专门用于获取数组的下标
fmt.Println(i, v ) // i 是下标,v 为下标对应的值
}

// 若只想获得值,可以使用_来接受下标
for _, v := range arr3{
fmt.Println(v)
}

数组是值类型 — 作为函数参数是值传递

  • 调用func f(arr [10]int)会拷贝数组

是类型意味着在作为函数参数进行传递时,传递的是拷贝值,在函数体内对传递的数组进行操作,不会修改原有的数组。

Go语言中都是值传递,意味着传递时会拷贝。

Go语言中,[5]int[3]int是两中不同的类型,不能相互之间被使用赋值。

若想传递的时候可以对数组进行改变而不是值传递,可以使用指针的方式

1
2
3
4
5
6
7
8
9
// 使用指针
func printArray(arr *[5]int) {
arr[0] = 100 // 注意此处的写法,等价于(*arr)[0]=100
for i, v := range arr {
fmt.Println(i, v)
}
}

printArray(&arr1) // 使用时,也传递的是数组的引用&数组名

上面的例子中arr[0] = 100 中,使用数组指针的话,可以不使用*进行调用,而是直接在函数内使用数组名,这是十分灵活以及方便的,当然也可以使用*,但是需要使用括号(*arr)[0] = 100这样的赋值方式。rang后可以直接用for i:= range *arr效果和for i:= range arr一致。

  • 在Go语言中一般不直接使用数组,而是使用的切片(切片名实际上是一个指针类型,拥有指针的属性)

切片(Slice) — 相当于可变数组

Go语言数组的定长性和值拷贝限制了其使用场景,Go提供了另一个数据类型slice, 是一种可变长数组。

1
2
3
4
arr := [...]int{0,1,2,3,4,5,6,7}
s := arr[2:6] // 切片下标不能超过原先数组的长度

// s 就是切片 值为[2,3,4,5]

Slice "不是"值类型(当然,Go语言所有的类型都是值类型),但是Slice内部有一个数据结构,是对array的一个view视图,所以对Slice的操作也会影响到原array的数据内容。
slice的结构中有指向数组的指针,所以实际上是一种引用类型,切片的操作会影响到原数组内容,切片变量名可以直接作为指针使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
arr := [4]int{1,2,3,4}
s1 := arr[2:3]
fmt.Printf("%p\n", s1) // 可以发现,切片变量名是一个指针,指向了切片的第一个元素的地址
fmt.Printf("%p\n", &s1[0])
fmt.Printf("%p\n", &arr[2])


a := 1
b := &a
fmt.Printf("%p\n",b)
fmt.Printf("%p\n",&a)
}

// 打印结果
0xc000018210
0xc000018210
0xc000018210

0xc00001e0d0
0xc00001e0d0

结论:切片变量名可以作为指针使用,执行切片的第一个元素。不过这里要和切片类型的指针区分,后者指的是切片的位置指针(但是这个特性好像没有用,因为只是首位的元素地址不能代表切片,而且Golang中是不支持指针运算的,知晓了第一元素地址也不能以此类推到第二个元素等等)。

切片的内部构造:

1
2
3
4
5
6
7
// src/runtinme/slice.go

type slice struct {
array unsafe.Pointer
len int
cap int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:]
fmt.Println("s1 = ", s1)
s1[0] = 100 // 对切片进行操作,会修改原先的数组值
fmt.Println("arr = ",arr)
}
// 结果
s1 = [2 3 4 5 6 7]
arr = [0 1 100 3 4 5 6 7]

可见对切片的操作会反映到原数组上。( 和Python不一样,Python的slice是对数组的截断拷贝,对切片的操作不会影响到Python列表(数组))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func updateSlice(s []int) {     // 此处使用的是切片,因为没有指定数组大小  
s[0] = 100
}
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

fmt.Println("arr[2:6] =", arr[2:6])
fmt.Println("arr[:6] =", arr[:6])
s1 := arr[2:]
fmt.Println("s1 =", s1)
s2 := arr[:]
fmt.Println("s2 =", s2)

fmt.Println("After updateSlice(s1)")
updateSlice(s1) // 需要注意的是,函数updateSlices()接受的参数为切片,所以这里传递的参数也需要为切片
fmt.Println(s1)
fmt.Println(arr)
}

有了切片,之前的对数组使用指针的操作就不需要了(因为切片内部保存了数组的指针,实际操作都会注册在原先的数组上面),而是使用切片,就可以实现同样的效果。唯一需要注意的是,含有切片参数的函数接受的参数也需要为切片。

Reslice

1
2
3
4
5
6
7
8
9
10
11
12
13
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

s := arr[2:6]
fmt.Println("s",s)
s = s[:3] // 切片可以在切片的基础上继续切片
fmt.Println("s",s)
s = arr[:]
fmt.Println("s",s)

// 结果
s [2 3 4 5]
s [2 3 4]
s [0 1 2 3 4 5 6 7]

时刻注意,切片是对原数组的视图,只要不对切片进行修改操作,就不会对数组进行修改。

Slice的扩展

切片含有一个cap(),capacity容量,会向后保存原始的数据。 使用cap()可以查看切片的容量

1
2
3
4
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

s1 := arr[2:6]
s2 := s1[3:5]
  • s1 的值为[2 3 4 5],s2的值为[5 6]
  • slice可以向后扩展,不可以向前扩展
  • s[i] 不可以超越len(s),向后扩展不可以超越底层数组cap(s)。如上面若是s2 = s1[5]是取不到值的。
  • 向后拓展的值可以被新的切片取到。如上面的s2 := s1[3:5]

Slice 操作

向Slice添加元素

使用append(slice, val),注意的是,在添加的时候,若添加的内容的下标未超过原始数组的下标(但是在切片的容量之内),则添加的内容会覆盖掉原始数组相应位置的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main(){
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

fmt.Println("arr =", arr)
s1 = arr[2:6]
s2 = s1[3:5] // [s1[3], s1[4]] 实际上为 [arr[5], arr[6]]
fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n",
s1, len(s1), cap(s1))
fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n",
s2, len(s2), cap(s2))

s3 := append(s2, 10) // 此处添加的10 对应着arr[7],原先的值被覆盖
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Println("s3, s4, s5 =", s3, s4, s5)
// s4 and s5 no longer view arr.
fmt.Println("arr =", arr)
}

// 结果arr = [0 1 2 3 4 5 6 7]
s1=[2 3 4 5], len(s1)=4, cap(s1)=6
s2=[5 6], len(s2)=2, cap(s2)=3
s3, s4, s5 = [5 6 10] [5 6 10 11] [5 6 10 11 12]
arr = [0 1 2 3 4 5 6 10]

超出的部分,这里的11 和 12 ,此时的s2 和 s3因为有超出的原先arry的内容,所以这里的s2和s3就不再是原先数组arry的视图view了,而是在内存中重新开辟了新的arry,将原先的arry拷贝过来,并将长度设置更长。

原先的数组若没有被使用,就会被垃圾回收掉,但是这里的不会,因为调用了。

切片和原先数组的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import "fmt"

func updateSlice(s []int) { // 此处使用的是切片,因为没有指定数组大小
s[0] = 100
}
func main() {
arr := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Printf("arr=%v, len(arr)=%d, cap(arr)=%d\n",
arr, len(arr), cap(arr))
fmt.Printf("数组的地址: %p\n", &arr)
fmt.Printf("数组的首元素的地址: %p\n", &arr[0]) // 可以发现就是数组的地址
s1 := arr[2:6]
fmt.Printf("切片的地址: %p\n", &s1)
fmt.Printf("切片的首元素的地址: %p\n", &s1[0])
fmt.Printf("数组对应切片首元素的地址: %p\n", &arr[2])
// 结果发现,切片内元素的地址和原先数组对应位置的元素是一样的,但是切片的地址是不同的
// 说明,进行切片时,是创建新的内存空间来存储原先数组对应元素的地址。


fmt.Println(s1)
fmt.Printf("第一次切片: %p\n", &s1)
s2 := append(s1, 100) // append会在操作的切片基础上生成一个新的切片
fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n",
s2, len(s2), cap(s2))
fmt.Printf("第二次切片: %p\n", &s2)
// 新的切片的地址和第一次切片的地址不一样
fmt.Printf("第二次切片首元素的地址: %p\n", &s2[0])
fmt.Printf("第二次切片切片变量的地址: %p\n", s2) // 切片名的地址是切片内首元素的地址

s3 := append(s2, 200)
fmt.Printf("第三次切片首元素的地址: %p\n", &s3[0])
fmt.Printf("s3=%v, len(s3)=%d, cap(s3)=%d\n",
s3, len(s3), cap(s3))
s4 := append(s3, 300)

//fmt.Println(arr)
fmt.Printf("arr=%v, len(arr)=%d, cap(arr)=%d\n",
arr, len(arr), cap(arr))
fmt.Printf("s4=%v, len(s4)=%d, cap(s4)=%d\n",
s4, len(s4), cap(s4))
fmt.Printf("第四次切片的首元素的地址: %p\n", &s4[0])

// 在切片添加操作超出原先数组后,切片映射的数组就不再是原先的数组,而是一个新的数组对象。
}
// 打印结果
arr=[0 1 2 3 4 5 6 7], len(arr)=8, cap(arr)=8
数组的地址: 0xc00001a1c0
数组的首元素的地址: 0xc00001a1c0
切片的地址: 0xc00000c060
切片的首元素的地址: 0xc00001a1d0
数组对应切片首元素的地址: 0xc00001a1d0
[2 3 4 5]
第一次切片: 0xc00000c060
s2=[2 3 4 5 100], len(s2)=5, cap(s2)=6
第二次切片: 0xc00000c0a0
第二次切片首元素的地址: 0xc00001a1d0
第二次切片切片变量的地址: 0xc00001a1d0
第三次切片首元素的地址: 0xc00001a1d0
s3=[2 3 4 5 100 200], len(s3)=6, cap(s3)=6
arr=[0 1 2 3 4 5 100 200], len(arr)=8, cap(arr)=8
s4=[2 3 4 5 100 200 300], len(s4)=7, cap(s4)=12
第四次切片的首元素的地址: 0xc000074060

Slice创建

1
2
3
4
5
6
// 方式一:
var s []int // Zero value for slice is nil
// 方式二:
s1 := []int{2,4,6,8} // 意思是先创建爱了一个数组[2 4 6 8],然后又赋值给了一个 []int 切片
// 方式三: 在需要建造一个指定长度的切片,但是还不确定内部的值的情况下
s2 := make(Type, size[,cap])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func printSlice(s []int){
fmt.Printf("%v len=%d cap=%d", s, len(s), cap(s))
}
func main(){
s1 := []int{2,4,6,8}
printSlice(s1)

s2 := make([]int , 4)
printSlice(s2)

s3 := make([]int, 6 ,19)
printSlice(s3)
}

// 结果
[2 4 6 8] len=4 cap=4
[0 0 0 0] len=4 cap=4
[0 0 0 0 0 0] len=6 cap=19

var s []int ,Go语言的特性,使用var创建变量之后没有付初值的情况下,有一个zero值,而切片的zero就是nil。直接使用var s []int创建的切片没有值但是可以只用,cap在装不下的时候会进行扩充,每次在原先的基础上乘以2。

数组的len以及cap的值是一致的,切片:

  • 在通过make(slice_name, len[, cap])创建的时候,可以使用指定的len以及cap,但是len的长度一定要小于等于cap的值,当添加数据超出cap后,新切片的cap为未扩充前的2倍。
  • 通过数组进行切片的,len为切片长度,切片的cap为数组的cap-切片的起始索引

无论通过什么方式创建的切片,在其cap长度内,都是在原先的数组基础上进行扩充的,一旦cap到达后就会进行双倍扩容,此时的就会拷贝原有数组的内容创建新的数组,新数组和之前的数组是没有关系的。

Slice 的复制

copy(dst, src []Type) int,将src中的内容拷贝到dst中,覆盖原先中的内容,超出的部分抹去 。

  • 为slice特有的方法,参数为切片类型 []Type
  • 返回值类型为int,返回的是实际复制了的内容的长度。

Slice 元素的删除

没有内建的函数来执行该操作,需手动执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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

func main(){
fmt.Println("Deleting elements from slice")
s1 := []int{1,2,3,0,0,0,0,0,0,4,5,6}
printSlice(s1)
// 可以使用切片从中截断,将需要的部分进行相加组合,但是slice没有加法,所以采用append
s1 = append(s1[:3], s1[9:]...) // append(slice []Type, elem ...Type)第二个参数为可变参数, 即可接受若干个Type类型的参数
printSlice(s1)

fmt.Println("Popping from front")
fornt := s1[0]
s1 = s1[1:]
fmt.Printf("fornt=%d\n",fornt)
printSlice(s1)

fmt.Println("Popping form back")
tail := s1[len(s1)-1]
s1 = s1[:len(s1)-1]

fmt.Printf("tail=%d\n", tail)
printSlice(s1)
}

// 结果
Deleting elements from slice
[1 2 3 0 0 0 0 0 0 4 5 6] len=12 cap=12
[1 2 3 4 5 6] len=6 cap=12
Popping from front
fornt=1
[2 3 4 5 6] len=5 cap=11
Popping form back
tail=6
[2 3 4 5] len=4 cap=11

解释:切片没有提供原生的删除操作,删除操作需要手动实现,如使用通过append拼接、创建新的切片等方式实现,但是注意:这些操作都是产生一个新的切片,此时需要使用原先的切片变量来接受,此时从结果上看此时对原来的切片进行了修改(因为Golang和Python不一样,Golang变量就是内存地址,变量开辟内存存储匹配类型的数据,而Python的变量只是别名用来指向数据的内存地址)。

小结

  • Slice本身没有数据,是对底层array的一个view
  • 添加元素时如果超越cap,系统会重新分配更大的底层数组,并将原来的arry拷贝过去。
  • 由于值传递的关系,必须接受append的返回值 — 因为,slice 的三个指标 ptr指针,len长度, cap容量都可能发生变化,而一旦cap容量超过之后,系统就会重新分配新的更大的内存去存储该slice,所以必须要设置参数用来接受append的返回值。
  • S = append(s, val)

Slice底层是对array的一个view,所以对Slice的修改会影响到底层的array,以及有关联的Slice(如同一个array的切片)

1
2
3
4
5
6
7
8
slicea := arr[:2]
sliceb := arr[:1]
fmt.Print(slicea)
slice[0] = 8
fmt.Print(arr)
fmt.Print(sliceb)
// 结果
[0 1][8 1 2][8]

Map

1
2
3
4
5
6
7
8
9
10
11
// 方式一:
m := map[string]string{
"name": "ccmouse",
"course": "golang",
"site": "imooc",
"quality": "notbad",
}
// 方式二:
m2 := make(map[string]int) // m2 == empty map
// 方式三:
var m3 map[string]int // m3 == nil

语法格式:map[key的类型]value的类型,还支持复合结构map[K1]map[K2]V

注意区分方式二和方式三的区别,m2是一个为空的map,已经分配了内存,只是内容为空,而m3是的值为nil,是Go中的特殊类型0值,但是nil也可以参与运算。

Map的遍历

1
2
3
4
fmt.Println("Traversing map m")
for k, v := range m {
fmt.Println(k, v)
}

需要注意的是,key 在Map中是无序的,是一个哈希Map

  • Map不保证遍历顺序,如需顺序,需手动对key排序 – 取出所有的key 放到slice中(slice可以排序)进行排序,然后在按照key取出。
  • 使用len获得元素个数

Map的key:

  • map使用哈希表,必须可以比较相等
  • 除了slice、map、function的其他内建类型都可以作为key
  • 不包含slice、map、function的Struct类型都可以作为key

Map操作

  • 读操作:courseName := m["course"],当key不存在时,会打印ZeroValue值,如这里会打印空字符串’ '。读操作有两个返回值,第一个返回值为key对应的value值,第二个返回值为反映该key对应的value值是否存在的标志,若key存在,则返回true, 反之返回false。

    1
    2
    3
    4
    5
    if causeName, ok := m["cause"]; ok {
    fmt.Println(causeName)
    } else {
    fmt.Println("key 'cause' does not exist")
    }
  • 删除操作:delete(map, key),该函数没有返回值。删除不存在的key也不会报错。

小结

  • 创建: make(map[string]int)
  • 获取元素: m[key]
  • key不存在时,获得Value类型的初始值
  • 使用value, ok := m[key]来判断是否存在key
  • delete(map, key)来删除一个key

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 寻找最长不含有重复字符的子串
func lengthOfNonRepeatingSubStr(s string) int{
lastOccurred := make(map[byte]int)
start := 0
maxLength := 0

for i, ch := range []byte(s) { // 将字符串每个字符取出,组成切片
// 目前该方法只支持英文,若想支持因为之外的其他多字节内容,使用 range []rune(s)
if lastI, ok := lastOccurred[ch]; ok && lastI >= start { // 有该值且不是同一字符
start = lastI + 1
}
if i-start+1 > maxLength {
maxLength = i - start +1
}
lastOccurred[ch] = i
}
return maxLength
}
1
2
3
4
5
6
package main 

func main(){
cache := make(map[string]string)
cache["name"] = "ccmouse"
}