数组(Array):

Go 语言中,数组是固定长度的、同一类型的数据集合。数组中包含的每个数据项被称为数组元素,一个数组包含的元素个数被称为数组的长度,数组长度在声明后就不可更改。

声明数组


var a [8]byte // 长度为8的数组,每个元素为一个字节
var b [3][3]int // 二维数组(9宫格)
var c [3][3][3]float64 // 三维数组(立体的9宫格)
var d = [3]int{1, 2, 3}  // 声明时初始化
var e = new([3]string)   // 通过 new 初始化

var arr [4]int // 申明一个数组变量默认是类型的0或""
fmt.Println(arr)  // [0 0 0 0]

var arr [4]string// 申明一个数组变量默认是类型的0或""
fmt.Println(arr)  // [   ]




// 使用[]来申明一个数组
//指定数组长度,元素类型,赋值初始值
// 一维数组
arr := [4]int{0, 1, 2, 3}
arr2 := [4]string{"a", "b", "c", "d"}

fmt.Println(arr) // 结果:[0 1 2 3]
fmt.Println(arr2)// 结果:[a b c d]

// 不指定数组长度也可以,Go会自动计算出数组长度。

arr := [...]int{0, 1, 2, 3}
arr2 := [...]string{"a", "b", "c", "d"}

fmt.Println(arr) // 结果:[0 1 2 3]
fmt.Println(arr2)// 结果:[a b c d]


//数组长度在声明后就不可更改,数组的长度是该数组类型的一个内置常量,可以用 Go 语言的内置函数 len() //来获取:

fmt.Println(len(arr2)) // 4

访问,设置和遍历

arr := []int{1, 2, 3, 4}

//可以使用数组下标来访问 Go 数组中的元素,数组下标默认从 0 开始,len(arr)-1 表示最后一个元素的下标:
a := arr[0] // a = 1

//设置 下标0的值为 100
arr[0] = 100  

//使用关键字 range,用于以更优雅的方式遍历数组中的元素,range 表达式返回两个值,第一个是数组下标索引值,第二个是索引对应数组元素值

for i, v := range arr {
	fmt.Printf("itme:%d--", i)
	fmt.Printf("value:%d\n", v)
}
//结果
itme:0--value:100 
itme:1--value:2
itme:2--value:3
itme:3--value:4

数组类型的不足

1、数组类型变量一旦声明后长度就固定了,不能动态添加元素到数组,如果要改变它,需要先创建一个容量更大的数组,然后把老数组的元素都拷贝过来,最后再添加新的元素,如果数组很大的话,这样会影响程序性能。

2、数组是值类型,这意味着作为参数传递到函数时,传递的是数组的值拷贝,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,当数组很大时,值拷贝会降低程序性能。

综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,实际上,我们在 Go 语言中很少使用数组,大多数时候会使用切片取代它。

切片(Slice):

Go 语言提供了切片(slice)来弥补数组的不足,Slice切片的底层其实就是一个数组, Slice切片是对底层数组的操作视图,它可以支持对元素做动态增删操作。

创建一个Slice

基于数组

切片可以基于一个已存在的数组创建。 Slice切片的底层其实就是一个数组,切片则可以看作是对底层数组进行操作的一个视图。切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的切片:

// 先定义一个数组
fruits := [...]string{"Apple", "Apricot", "Banana", "Blackberry", "Blueberry", "Cherry", "peach"}

// 基于数组创建切片
a := fruits[:3]  // 前3个
b := fruits[2:5] // 中间3个
c := fruits[:]   // 全部

fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
//结果
[Apple Apricot Banana]
[Banana Blackberry Blueberry]
[Apple Apricot Banana Blackberry Blueberry Cherry peach]

切片底层引用了一个数组,由三个部分构成 —— 指针、长度和容量,指针指向数组起始下标,长度对应切片中元素的个数,容量则是切片起始位置到底层数组结尾的位置:

Slice逻辑

切片长度不能超过容量,切片的容量就是 开始位置到底层数组的最后一位。我们可以使用cap函数,看看容量

// 先定义一个数组
fruits := [...]string{"Apple", "Apricot", "Banana", "Blackberry", "Blueberry", "Cherry", "peach"}
a := fruits[:3] // 前3个
fmt.Printf("value:%s--len:%d--cap:%d", a, len(a), cap(a))
//结果
value:[Apple Apricot Banana]--len:3--cap:7

我们可以看到,容量是7,也就是从切片开始位置到底层数组的最后一位。

基于切片

基于切片创建切片时,可以获取到超过基切片长度的内容,因为切片本质是基于底层数组的操作视图,我们从容量中就可以发现,因此只要不超出底层数组的容量范围,就是允许的。


fruits := [...]string{"Apple", "Apricot", "Banana", "Blackberry", "Blueberry", "Cherry", "peach"}

// 基于切片创建切片
a := fruits[:3] // 前3个
b := a[0:2]

fmt.Printf("value:%s--len:%d--cap:%d", b, len(b), cap(b))
//结果:value:[Apple Apricot]--len:2--cap:7
b = a[2:6]
fmt.Printf("value:%s--len:%d--cap:%d", b, len(b), cap(b))
//结果:value:[Banana Blackberry Blueberry Cherry]--len:4--cap:5

直接创建

使用 make()函数可以灵活地创建切片,make内置函数分配并初始化一个类型对象,slice, map,或chan (only)。与new一样,第一个参数是类型,而不是值,指定不同的容量;它必须不小于长度。例如,make([]int, 0,10)分配一个底层数组,返回长度为0,容量为10的slice,所以也是基于底层数组而实现的。

// 使用make 创建一个长度5,容量10的切片,make会先分配一个底层数组,再返回切片。
aSlice := make([]int, 5,10)
fmt.Printf("value:%d--len:%d--cap:%d", aSlice, len(aSlice), cap(aSlice))
//结果
value:[0 0 0 0 0]--len:5--cap:10

//还可以直接创建并初始化包含 5 个元素的数组切片(长度和容量均为5):
mySlice := []int{1, 2, 3, 4, 5}

同时,切片也支持遍历,和操作数组的操作一样。

动态增加元素

通过 append() 函数向切片追加新元素,append() 函数并不会改变原来的切片,而是会生成一个容量更大的切片,然后把原有的元素和新元素一并拷贝到新切片中。

默认情况下,扩容后新切片的容量将会是原切片容量的 2 倍,如果还不足以容纳新元素,则按照同样的操作继续扩容,直到新容量不小于原长度与要追加的元素数量之和。但是,当原切片的长度大于或等于 1024 时,Go 语言将会以原容量的 1.25 倍作为新容量的基准。

因此,如果事先能预估切片的容量并在初始化时合理地设置容量值,可以大幅降低切片内部重新分配内存和搬送内存块的操作次数,从而提高程序性能。

//如果追加的元素个数超出 aSlice 的默认容量,则底层会自动进行扩容
aSlice := make([]int, 5)
bSlice := append(aSlice, 6)
fmt.Printf("value:%d--len:%d--cap:%d", bSlice, len(bSlice), cap(bSlice))
//结果value:[0 0 0 0 0 6]--len:6--cap:10

动态删除元素

切片除了支持动态增加元素之外,还可以动态删除元素,在切片中动态删除元素可以通过多种方式实现(其实是通过切片的切片实现的「伪删除」):

slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice = slice[:len(slice) - 5]  // 删除 slice 尾部 5 个元素
slice = slice[5:]  // 删除 slice 头部 5 个元素

此时切片 slice 的所有元素被删除,长度是0,容量也变成 5,注意这里不是自动缩容,而是第二个切片容量计算逻辑决定的。

复制

 copy() 用于将元素从一个切片复制到另一个切片。如果两个切片不一样大,就会按其中较小的那个切片的元素个数进行复制。

slice1 := []int{1, 2, 3, 4, 5} 
slice2 := []int{5, 4, 3}

// 复制 slice1 到 slice 2
copy(slice2, slice1) // 只会复制 slice1 的前3个元素到 slice2 中
// slice2 结果: [1, 2, 3]
// 复制 slice2 到 slice 1
copy(slice1, slice2) // 只会复制 slice2 的 3 个元素到 slice1 的前 3 个位置
// slice1 结果:[5, 4, 3, 4, 5]

数据共享问题

Slice 是对底层数组的地址引用,所以在使用过程中,会遇到数据共享问题

slice1 := []int{1, 2, 3, 4, 5}

slice2 := slice1[1:3]
slice2[1] = 6

fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
// 结果
slice1: [1 2 6 4 5]
slice2: [2 6]

我们可以看到,由于 slice1 和 slice2 都是对于同一个数据地址的修改( slice2 是从 slice1 上创建的,指针指向同一个数组),在改变 slice2 的元素值时, slice1 也会改变

如何解决

我们可以通过修改切片的容量,去让系统重新分配内存地址,当地址重新分配后,它们指针指向的就不是同一个数据了,例如通过 append ()

slice1 := make([]int, 4)
slice2 := slice1[1:3]
slice1 = append(slice1, 0)
slice1[1] = 2
slice2[1] = 6

fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
//结果
slice1: [0 2 0 0 0]
slice2: [0 6]

可以看到,虽然 slice2 是基于 slice1 创建的,但是修改 slice2 不会再同步到 slice1,因为 append 函数会重新分配新的内存,然后将结果赋值给 slice1,这样一来,slice2 会和老的 slice1 共享同一个底层数组内存,不再和新的 slice1 共享内存,也就不存在数据共享问题了。

Map

Map , 是一种存储 key=>value 键值对的结构,在Go 中,需要在声明时指定键和值的类型,它是个无序集合,底层不会按照元素添加顺序维护元素的存储顺序。

使用

// 先声明一个Map 类型变量,指定key 为 string 类型,value 为 int 类型
var testMap map[string]int
testMap = map[string]int{
	"one":   1,
	"two":   2,
	"three": 3,
}


//我们也可以直接初始化
testMap := map[string]int{
	"one":   1,
	"two":   2,
	"three": 3,
}


k := "two"
v, ok := testMap[k]
if ok {
	fmt.Printf("The element of key %q: %d\n", k, v)
} else {
	fmt.Println("Not found!")
}

使用 make() 声明

通过这种方式初始化后,可以往字典中添加键值对(前面那种声明方式不能这么操作,否则编译期间会抛出 panic)

var testMap = make(map[string]int,100) // 第二个参数为map 初始容量,超过会自动扩容
testMap["one"] = 1
testMap["two"] = 2
testMap["three"] = 3

查找

从字典中查找指定键时,会返回两个值,第一个是真正返回的键值,第二个是是否找到的标识,判断是否在字典中成功找到指定的键,不需要检查取到的值是否为 nil,只需查看第二个返回值 ok,这是一个布尔值,如果查找成功,返回 true,否则返回 false,配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。

if one, ok := testMap["one"]; ok {
	fmt.Println(one)
} else {
	fmt.Println("undefined")
}

删除

Go 语言提供了一个内置函数 delete(),用于删除容器内的元素,我们可以通过这个函数来实现字典元素的删除:

delete(testMap, "four")

上面的代码将会从 testMap 中删除键为「four」的键值对。如果「four」这个键不存在或者字典尚未初始化,这个调用也不会有什么副作用。

遍历

一样使用 range,遍历的结果是无序的

testMap := map[string]int{
    "one": 1,
    "two": 2,
    "three": 3,
}

for key, value := range testMap {
    fmt.Println(key, value)
}

如何从Map得到一个有序的结果

使用切片和Go 语言内置的 sort 包,这个包提供了一系列对切片和用户自定义集合进行排序的函数。

 // 对键进行排序
keys := make([]string, 0) // 创建一个切片
for k, _ := range testMap { // 添加进切片中
    keys = append(keys, k)
}
sort.Strings(keys) 

fmt.Println("Sorted map by key:")
for _, k := range keys {
    fmt.Println(k, testMap[k])
}

//结果
Sorted map by key:
one 1
three 3
two 2

// 对值进行排序

invMap := make(map[int] string, 3)
for k, v := range testMap { // 键值对调
    invMap[v] = k
}

for k, v := range invMap {
    fmt.Println(k, v)
}
values := make([]int, 0)
for _, v := range testMap {
    values = append(values, v)
}

sort.Ints(values)   

fmt.Println("Sorted map by value:")
for _, v := range values  { 
    fmt.Println(invMap[v], v)
}