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。尝试在 nilmap 上进行读写操作将会导致运行时恐慌(panic)。因此,在使用前应确保 map 被正确初始化。

  • 并发安全 :虽然Go的map在1.9版本后是并发安全的,但在高并发的场景下仍可能遇到性能瓶颈或需要更精细的控制,这时可以考虑使用 sync.Map

  • 性能考虑:虽然Go的map提供了很好的性能和灵活性,但在某些特定情况下(如频繁的删除操作),可能需要考虑其性能影响或选择其他数据结构(如平衡树)。

相关推荐
_不会dp不改名_12 分钟前
Windows10 下QT社区版的安装记录
开发语言·qt
joekl16 分钟前
python练习题
开发语言·python
盛满暮色 风止何安22 分钟前
VLAN的高级特性
运维·服务器·开发语言·网络·网络协议·网络安全·php
噜啦噜啦嘞好22 分钟前
c++的特性——多态
开发语言·c++
巷北夜未央30 分钟前
Python每日一题(9)
开发语言·python
MessiGo34 分钟前
Python 爬虫(5)Beautiful Soup 4 实战
开发语言·爬虫·python
慕容魏35 分钟前
面经分享,中科创达(安卓开发,二面挂)
java·开发语言
里昆37 分钟前
【COMSOL】参数化建模以及通过MATLAB运行
开发语言·matlab
ydm_ymz1 小时前
初阶8 list
c语言·开发语言·数据结构·c++·list
YiHanXii1 小时前
Axios的二次封装
开发语言·前端