Go语言基础五:引用类型-切片和映射
切片
Go的数组长度不可以改变,在某些特定的场景中就不太适用了。对于这种情况Go语言提供了一种由数组建立的、更加灵活方便且功能强大的包装(Wapper),也就是切片。与数组相比切片的长度不是固定的,可以追加元素。
切片本身不拥有任何数据,它们只是对现有数组的引用。
切片的定义
可以声明一个未指定大小的数组来定义切片:
var slice_name []type
//type用来指定切片内元素的数据类型
切片不需要声明长度。还可以使用make()
函数来创建切片:
var slice_name []type = make([]type, len)
//len是切片的长度
slice_name := make([]type,len)
//make()函数定义时,也可以指定切片的容量,其中capacity为可选参数
make([]type, length, capacity)
切片的初始化
直接初始化
直接初始化一个切片,[]
表示是切片类型,{1,2,3}
初始化值依次是1,2,3
,对于下面这个切片,初始长度为3容量为3
slice_name := []int {1,2,3}
引用数组或切片
//初始化切片slice,是数组array的引用
slice := array[:]
//将array中从下标startIndex到下标endIndex-1下的元素创建为一个新的切片
slice := array[startIndex:endIndex]
//endIndex为空时,从索引为的startIndex元素到最后一个元素
slice := array[startIndex:]
//startIndex空时,从第一个元素(索引为0)到索引为endIndex-1的元素
slice := array[:endIndex]
//通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
make()
函数
使用make()
函数创建切片时,默认情况下切片的元素的值为0。
切片的修改
切片它自己不拥有任何数据,他只是对底层数组的一种表示,对切片所做的任何修改都会反映在底层的数组当中。
当多个切片共用相同的底层数组时,每个切片的修改都将会反映在这个数组中。
package main
import (
"fmt"
)
func main() {
arr := [3]int {1,2,3}
slice_1 := arr[:]
slice_2 := arr[:]
fmt.Println("原始数组",arr)
slice_1[1] = 10
fmt.Println("修改slice_1之后的数组",arr)
slice_2[2] = 20
fmt.Println("修改slice_2之后的数组",arr)
}
运行结果
原始数组 [1 2 3]
修改slice_1之后的数组 [1 10 3]
修改slice_2之后的数组 [1 10 20]
从输出中可以清晰的看出,对切片的修改会反映在底层数组当中;当多个切片共享一个底层数组时,每个切片所作出的修改都会反映在数组中。
切片的长度和容量
切片的长度是切片中的元素数。切片的容量是从创建切片的索引开始的底层数组中的元素数。
可以内置函数len()
测量切片的长度,内置函数cap()
获取切片的容量。
package main
import (
"fmt"
)
func main() {
arr := [...]string{"A","B","C","D","E","F"}
s := arr[1:3]
fmt.Printf("切片的长度为%d,切片的容量为%d",len(s),cap(s))
s = s[:1]
fmt.Printf("切片的长度为%d,切片的容量为%d",len(s),cap(s))
}
输出结果为:
切片的长度为2,切片的容量为5
在上述程序中,s
是由从arr
的索引1到2创建的一个切片。因此切片s
的长度为2
。
切片s
是从arr[1]
开始,即索引从1开始创建的。根据切片容量的定义,可以得知,s
的容量为从此索引开始的底层数组arr
中的元素数也就是5。
所以切片的长度为2,切片的容量为5。
追加和移除切片元素
追加元素
正如我们已经知道的,数组的长度是固定的,它的长度是不能增加。切片是动态的,使用append()
函数可以将新元素追加到切片上。append()
函数的定义是 func append(s[]T,x ... T)[]T
。此函数只能将元素追加到切片末尾。
x ... T
在函数的定义中表示该函数接受参数x
的个数是可变的。这些类型的函数被称为可变函数
append()
函数的第二参数可以是一个数字,也可以是一组数组。例
append(slice,1)或append(slice,1,2,3)或append(slice,slice2...)`
正如我们所了解的,切片的对数组的一种抽象、一种包装,但是数组本身的长度是固定的,那么切片是如何具有动态长度的?而且切片的容量是由他所引用的数组的长度决定的,那么当新的元素添加到切片当中,使得切片的长度变大以后超过了这个切片的容量,这个切片和它引用的数组发生了什么变化呢?
当新的元素添加到切片中时,系统会创建一个新的数组,将现有的数组元素复制到这个新的数组当中,并且返回这个新数组的新切片引用。并且新的切片容量为旧切片的两倍。
package main
import(
"fmt"
)
func main() {
arr := [...]string{"A","B","C","D","E","F"}
s1 := arr[:]
fmt.Println("s1",s1,"长度为",len(s1),"容量为",cap(s1))
s1 = append(s1,"G")
fmt.Println("添加一个元素后s1",s1,"长度为",len(s1),"容量为",cap(s1))
s1[0] = "1"
fmt.Println(arr,s1)
}
输出结果
s1 [A B C D E F] 长度为 6 容量为 6
添加一个元素后s1 [A B C D E F G] 长度为 7 容量为 12
[A B C D E F] [1 B C D E F G]
在上述程序当中,s1
是由arr
声明的切片,其初始长度为6,初始容量为6。当向其中添加一个新的元素,这时系统会在创建一个新的数组,并返回一个新的切片给s1
,这时s1
的长度为7,容量为12。可以看到容量翻了一倍。此时切片s1
所表示的数组已经不是arr
,而是系统新创建的数组。
追加元素到切片头部
如果想把元素追加到切片的开头,Go没有提供原生的函数。可以通过append()
函数变相实现,这种方法实际上是合并两个切片。
nums := []int {1,2,3}
//... 三个点表示将切片元素展开传递给函数
nums = append([]int{4},nums...)
fmt.Println(nums)
//输出
[4,1,2,3]
移除元素
使用切片子集和append()
函数变相实现。
nums := []int {1,2,3,4,5}
nums = append(nums[:2],nums[3:]...)
fmt.Println(nums)
//输出
[1,2,4,5]
切片的拷贝
从上面我们可以了解到切片是对一个数组的引用,因此不能向数组那样直接赋值给一个新的变量就会产生拷贝。
数组是值类型,对数组而言,将一个数组赋值给另一个新的数组,那么这里会产生一次拷贝。直白地说,赋值之后两个数组是不一样的他们指向地地址不同,一个数组发生变化,另一个数组不会发生变化。
切片是一个结构体,是对底层数组地一个引用。将切片的值赋值给另一个新的切片,这两个切片都是引用的同一个数组。对切片地修改会映射引用的底层数组上,那么修个一个切片的值,另一个切片的值也会发生变化。
如果希望两个切片引用的不是同一个数组,就需要用到copy()
函数完成对切片的拷贝。
copy(dst, src []Type)
第一个参数为目标切片,第二个参数为源切片。
package main
import(
"fmt"
)
func main() {
arr := [...]int{1,2,3}
s1 := arr[:]
s2 := s1
s1_copy := make([]int,3)
copy(s1_copy,s1)
s1[0] = 10
fmt.Println("s1值为:",s1,",s2值为:",s2,",s1_copy值为:",s1_copy)
}
输出结果
s1值为: [10 2 3] ,s2值为: [10 2 3] ,s1_copy值为: [1 2 3]
上述代码中s1
是对arr
引用过的一个切片;s2
是由s1赋值而来的切片;s1_copy
是由s1
拷贝而来的切片。由输出结果可知,赋值会将两个切片指向同一个底层数组,使用copy()
函数会将源切片的值复制到新的切片当中。
切片在函数传递
我们可以认为,切片在内部可以由一个结构体类型表示。这是它的表现形式。
type slice struct {
Length int
Capactiy int
ZerothElement *byte
}
切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使他通过值传递,指针变量也将引用相同的底层数组。因此,当切片作为参数传递给函数时,函数内做的更改,也会在函数外可见。
在Go语言中我们通常不会把数组的指针作为参数在函数中传递,而是使用切片进行传递。切片就是一种对数组的引用,这样会使代码更加简洁。
多维切片
多维切片类似与多维数组,唯一的不同在于多维切片没有指明长度。
package main
import(
"fmt"
)
func main() {
nums := [][]int{
{1,2,3},
{10,20,30},
{100,200,300}
}
for _,v1 := range nums {
for _,v2 := range v1 {
fmt.Printf("%d,",v2)
}
fmt.Printf("\n")
}
}
运行结果
1,2,3,
10,20,30,
100,200,300,
tips
- 切片是对底层数组的引用。只要切片还在内存中,这个被引用的底层数组就不能被垃圾回收。如果我们需要对很大的一个数组的一个小片段进行处理,那么我们可以由这个数组创建一个切片,并开始处理切片。但是同时这个很大的输入仍然在内存中,一种解决方法就是使用
copy()
函数来生成一个切片副本,这样我们就可以使用新的切片,原始的数组就会被垃圾回收 - *切片的长度是当前切片可以访问元素的数量,而切片的容量则是当前切片拓展后能访问的元素的个数。当然这些可以访问的元素都是底层数组中的元素
- 切片是一个结构体,它由一个指向数组中元素的指针、长度、容量来组成的。因此切片这个结构体类型同时具有引用类型的特征和值类型的特征
- 使用
append()
函数为切片添加元素时,如果使切片的长度超过了容量时,会返回一个新的切片,这个新切片的容量不一定为旧切片容量的2倍,具体参考这里
映射
GO语言中的Map是一种键值对的无序列表。可以快速并高效地查找唯一键的值。
Map是一种集合,所以可以像迭代数组和切片那样迭代它。不过,Map是无序的集合,因为它底层是一个hash表,因此无法决定它的返回顺序。
Map的定义和初始化
使用make()
函数
map_name := make(map[key_type]value_type)
map_name[key] = value
make(map[keyType]valueType, cap)
cap表示容量,作为可选参数。
使用make()
函数创建map
创建时,可以指定一个合理的初始容量大小,这样就会申请一块合适的内存,避免在后续的使用中频繁扩浪费性能。
使用make()
函数创建map
,其中没有数据且不是零值。
map
的零值为nil
使用字面值创建
var map_name = map[key_type]value_type{key1:v1,k2:v2}
或
map_name := map[key_type]value_type{key1:v1,k2:v2}
定义一个map
后,如果不初始化map
,那么就会创建一个nil map
。nil map
不能用来存放键值对。
//定义一个map但是不初始化值
var student map[int]string
//如果将值赋给这个map编译就会报错
student[1] = "bob"
运行结果
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/box/main.go:9 +0x43
Exited with error status 2
Map的数据操作
key-value
向map
中添加数据的语法与数组是类似的。
students[1] = "zs"
在Go语言中允许多返回值,在map
中根据key
获取value
,在返回value
的同时会返回一个boolean
类型的值,这个值用表示地是map
中是否存在对应key
。如果不存在对应key
的value
,则返回map
中value
数据类型对应的零值。当然也可以只返回一个value
//根据key获取map中的数据
value := students[1]
//exists为true说明存在为false说明不存在,如过存在返回对应值,否则返回对应数据类型零值这里string零值为""空字符串
value,exists := students[1]
range()
函数迭代Map
range()
迭代map
,返回key和value
package main
import "fmt"
func main() {
students := map[int]string{
1:"zs",
2:"ls",
3:"ww",
}
//使用range()函数迭代Map
for key, value := range students {
fmt.Printf("Key: %d Value: %s\n", key, value)
}
}
delete()
函数删除Map中元素
delete(map,key)
函数可以删除map
中指定key
的键值对。接上面的代码。delete()
函数没有返回值。
//删除键为1的键值对
delete(map,1)
//输出students中所有键值对
for key, value := range students {
fmt.Printf("Key: %d Value: %s\n", key, value)
}
len()
函数获取Map的长度
使用len()
函数可以用来获取map
的长度
l := len(students)
map
的函数传递
与切片一样,映射也是一个引用类型,它们都都指向了底层的数据结构。切片指向的数据结构是数组,而映射指向的数据结构是散列表。当映射作为参数在函数之间传递时,是进行map指针的拷贝,相对于指针来说是值拷贝,相对于底层来说是引用传递。直白的说就是进行参数传递时它们引用的底层数据结构都是一个,也就是对与一个发生变化另一个也会变化。
package main
import (
"fmt"
)
func main() {
students := map[int]string{
1:"zs",
2:"ls",
3:"ww",
}
fmt.Println("原始的students", students)
modified := students
modified[1] = "zss"
fmt.Println("修改之后的students", students)
}
运行结果
原始的students map[1:zs 2:ls 3:ww]
修改之后的students map[1:zss 2:ls 3:ww]
Maps equality
映射之间不能使用==
来进行比较操作。==
只能用于检查映射是否为nil
package main
func main() {
map1 := map[string]int{
"one": 1,
"two": 2,
}
map2 := map1
if map1 == map2 {
}
}
输出结果:错误
./prog.go:11:10: invalid operation: map1 == map2 (map can only be compared to nil)
Go build failed.