5.go切片和map
切片的概念
数组和切片相比较切片的长度是不固定的,可以追加元素,在追加时可能会使切片的容量增大,所以可以将切片理解成 "动态数组",但是,它不是数组,而是构建在数组基础上的更高级的数据结构。
切片和数组的对比
-
数组在声明时需要指定长度,切片不需要。
-
数组的长度是数组类型的一部分,因此 [5]int 和 [10]int 是不同的类型,不能互相赋值或比较。而切片只需要考虑元素类型。
-
数组是值类型,当数组作为参数传递时,会被复制一份,因此对数组的修改不会影响原数组。而切片是引用类型,当切片作为参数传递时,只会传递一个指向底层数组的指针,因此对切片的修改会影响原切片。
-
数组的长度是固定的,在内存中占用连续的空间。而切片的长度是可变的,在内存中是一个结构体,其中包含指向底层数组的指针、长度和容量三个字段。
切片声明
-
使用var声明切片
语法:var 切片名称[] 数据类型
var arr []int //此时变量arr 是切片的零值 [],和nil比较,是相等的。此时len是0,cap是0
-
使用自动推导类型声明切片
语法:切片名称 := []类型{}
arr := []int{} //此时变量是指向一个长度为0的数组,和nil不等,此时len是0,cap是0
arr := []int{1,2,3} //此时变量是指向一个长度为3的数组,此时len是3,cap是3
-
使用make()函数生成切片
语法:make([]类型, 长度, 容量)
长度是已初始化空间
容量是已经开辟空间,包括已经初始化的空间和空闲空间。
arr := make([]int, 3, 5)
fmt.Printf("长度:%d\t容量:%d", len(arr), cap(arr))
slice内存模型
例子:
a1 := make([]int, 1, 3) //此时a1被分配了长度是3,【a1[0,x,x]】
a2 := append(a1, 1, 2) //此时append 两个,底层数组可以容纳,【a2[0,1,2]】
a3 := append(a1, -1) //此时在a1的基础上[0,x,x],添加一个元素,底层数组可以容纳【a3[0,-1]】
//此时因为使用的是同一个数组,影响了a2,此时【a2[0,-1,2]】
fmt.Println(a1, a2, a3)
向切片中添加元素
arr := make([]int, 3, 5)
arr[0] = 100 //添加100
arr[1] = 200 // 添加200
//panic: runtime error: index out of range [3] with length 3
//arr[3] = 200 //错误,不能大于申请的长度
//append可以在申请长度后边新增元素,如果不大于申请容量,直接放在在源底层数组上,
//如果大于申请容量,会重新分底层配数组,把原来的数据拷贝到新数组上,然后在后边加上新元素
fmt.Println(arr)
append()函数切片添加元素
arr = append(arr, 1) // 追加1个元素
fmt.Printf("arr元素%v,arr长度%d,arr容量%d,arr地址%p\n", arr, len(arr), cap(arr), &arr[0])
arr = append(arr, 2, 3) // 最加多个元素
fmt.Printf("arr元素%v,arr长度%d,arr容量%d,arr地址%p\n", arr, len(arr), cap(arr), &arr[0])
arr = append(arr, []int{5, 6, 7}...) // 追加一个切片
fmt.Printf("arr元素%v,arr长度%d,arr容量%d,arr地址%p\n", arr, len(arr), cap(arr), &arr[0])
注意:在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变,会导致内存的重新分配,而且已有元素全部被复制 1 次到新分配的内存地址上。
...
运算符将切片展开为多个独立的参数
var arr = []int{1, 2, 3}
fmt.Printf("arr元素%v,arr长度%d,arr容量%d,arr[0]地址%p\n", arr, len(arr), cap(arr), &arr[0])
arr = append([]int{666}, arr...) // 定义一个新切片,并且把原来切片展开,添加到新切片里
fmt.Printf("arr元素%v,arr长度%d,arr容量%d,arr[0]地址%p\n", arr, len(arr), cap(arr), &arr[0])
内存分析
//此时,内存进行了三次分配,首先定义的a,会分配一个数量是3的底层数组,
// 定义[]int{666},时候,会分配一个数量是1的底层数组,
//往[]int{666} 添加切片时候,空间不够,会在开辟一块底层是4的数组,
//然后 拷贝[]int{666}到新数组,在拷贝a切片内容到新数组,最后把a切片底层指向数组的指针指向新分配的数组
copy()函数复制切片
Go语言的标准库提供了一个非常方便的copy
函数,它可以用来复制切片(slice)或者数组(array)中的内容。
copy( destSlice, srcSlice)
-
srcSlice:数据来源切片
-
destSlice:复制的目标
在使用 copy 函数时,需要确保源切片和目标切片的长度相同
如果长度不同,copy 函数只会复制较短切片的长度。这可能导致数据丢失或意外的行为。
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // n 将等于 3,而不是 5
遍历切片
arr := make([]int, 3, 5)
arr = append(arr, 200, 300)
// // 传统for循环
// for i := 0; i < len(arr); i++ {
// fmt.Println(arr[i])
// }
// for range遍历
for _, v := range arr {
fmt.Println(v)
}
子切片
子切片是指在Go语言中,从一个已有的切片中创建出的一个新的切片。子切片与原始切片共享同一个底层数组,但拥有自己的长度和容量。因为子切片共享原切片的底层数组,因此对数据的更改会影响彼此。
子切片写法
切片[startIndex:endIndex]
startIndex
是子切片的起始索引,endIndex
是子切片的结束索引(但不包括该索引处的元素)。
如果省略 startIndex
(切片[startIndex:]),则从切片的开始处截取;
如果省略 endIndex
(切片[:endIndex]),则截取到切片的末尾。
如果都省略(切片[:]),则截取原切片全部。
例子:
s := []int{1, 2, 3, 4, 5}
sub := s[1:4] // 创建从索引1到索引3的子切片,
fmt.Println(sub) // 输出:[2 3 4]
sub[0] = 20 // 修改sub的第一个元素
fmt.Println(s) // 输出:[1 20 3 4 5],s也被修改
sub = append(sub, 300)
fmt.Println(s) // 输出:[1 20 3 4 300],底层分配的数组,后边还有未使用
//,所以不会扩容,会在原来底层数组上做修改
map
在Go语言中,map
类型是一个内置的引用类型,它存储键值对(key-value pairs)。map
是由一个键类型和一个值类型构成的,其中键的类型必须是可比较的(例如,整型、字符串、结构体等,但不能是切片或函数等不可比较的类型)。
map 零值是 nil 。不能直接使用,需要make创建 或者 字面量赋值
var m map[string]int m["apple"] = 1 //错误:panic: assignment to entry in nil map
创建 map
你可以使用 make
函数或者使用字面量语法来创建一个 map
。
-
使用
make
函数:
-
使用字面量语法:
m := map[string]int{
"apple": 5,
"pear": 6,
"grape": 8,
}
访问 map 中的元素
你可以通过键来访问 map
中的元素。如果键存在,它将返回对应的值;如果键不存在,它将返回零值(对于值类型)或者一个明确的错误。
value, ok := m["apple"] // value 为 5, ok 为 true value, ok = m["banana"] // value 为 0 (int 的零值), ok 为 false
修改 map 中的元素
你可以通过键来修改 map
中的元素。如果键存在,它的值将被更新;如果键不存在,一个新的键值对将被添加到 map
中。
m["apple"] = 10 // 将 "apple" 的值改为 10 m["banana"] = 3 // 添加一个新的键值对 "banana": 3
删除 map 中的元素
你可以使用 delete
函数来删除 map
中的元素。
delete(m, "apple") // 删除键为 "apple" 的元素
遍历 map
你可以使用 range
循环来遍历 map
中的所有键值对。在每次迭代中,range
会返回两个值:键和对应的值。
for key, value := range m { fmt.Println(key, value) }
map 的并发访问和修改
从Go 1.9开始,map 是并发安全的,你可以在多个goroutine中安全地读写同一个map。但是,如果你需要频繁地修改同一个map,最好还是限制对它的并发访问,以避免不必要的性能开销。如果你确实需要频繁地并发修改,可以考虑使用 sync.Map
,它是专门为并发环境设计的。
注意事项
-
零值:当你声明了一个
map
变量但没有初始化(即没有用make
或字面量语法),它的零值是nil
。尝试在nil
的map
上进行读写操作将会导致运行时恐慌(panic)。因此,在使用前应确保map
被正确初始化。 -
并发安全:虽然Go的map在1.9版本后是并发安全的,但在高并发的场景下仍可能遇到性能瓶颈或需要更精细的控制,这时可以考虑使用
sync.Map
。 -
性能考虑:虽然Go的map提供了很好的性能和灵活性,但在某些特定情况下(如频繁的删除操作),可能需要考虑其性能影响或选择其他数据结构(如平衡树)。