一、切片
之前我们在数组那举过一个例子:关于 值类型 和 引用类型的,如下:
func main(){
// 值类型: 数组
var a1 = [...]int{1, 2, 3}
a2 := a1
a1[0] = 11
fmt.Println(a1) // 11 2 3
fmt.Println(a2) // 1 2 3
// 引用类型: 切片
var b1 = []int{1, 2, 3}
b2 := b1
b1[0] = 11
fmt.Println(b1) // 11 2 3
fmt.Println(b2) // 11 2 3
}
现在我们来正式了解一下切片是什么吧
定义:切片(slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个 引用类型,它的内部结构包含 地址、长度 和 容量
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片长度
cap int // 切片容量
}
声明切片类型的基本语法:var name []T
- name:表示变量名
- T:表示切片中的元素类型
如下:
package main
import "fmt"
func main() {
// 切片和数组的区别
var arr [5]int // 数组:固定长度
var slice []int // 切片:动态长度
fmt.Printf("数组: %v, 长度: %d\n", arr, len(arr))
fmt.Printf("切片: %v, 长度: %d\n", slice, len(slice))
}
1. 切片的创建方式
下面是多种创建切片的方法:
func main() {
// 1. 从数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // [2 3 4] - 从索引1到3
slice2 := arr[:] // [1 2 3 4 5] - 整个数组
slice3 := arr[:3] // [1 2 3] - 从开始到索引2
slice4 := arr[2:] // [3 4 5] - 从索引2到结束
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
fmt.Println("slice3:", slice3)
fmt.Println("slice4:", slice4)
// 2. 使用 make 创建切片
slice5 := make([]int, 5) // 长度5,容量5,元素为0
slice6 := make([]int, 3, 10) // 长度3,容量10
fmt.Printf("slice5: %v, 长度: %d, 容量: %d\n", slice5, len(slice5), cap(slice5))
fmt.Printf("slice6: %v, 长度: %d, 容量: %d\n", slice6, len(slice6), cap(slice6))
// 3. 字面量创建切片
slice7 := []int{1, 2, 3, 4, 5}
slice8 := []string{"hello", "world"}
fmt.Println("slice7:", slice7)
fmt.Println("slice8:", slice8)
// 4. 使用 new 创建(不常用)
slice9 := *new([]int) // 创建空切片
fmt.Printf("slice9: %v, 长度: %d, 容量: %d\n", slice9, len(slice9), cap(slice9)) // [] 0 0
}
理解:这里的数组创建切片 [] 的意思
语法格式:array[low : high : max]
其中:
- low :起始索引(包含)
- high :结束索引(不包含 )
- max :可选参数,指定切片的容量上限
而实际的切片长度和容量计算为:(在2.2 切片长度和容量那里有举例说明)
- 长度 :
high - low
- 容量 :
max - low
(如果不指定max
,则容量到数组末尾)
2. 切片的基本操作
2.1 访问和修改元素
func main() {
slice := []int{10, 20, 30, 40, 50}
// 访问元素
fmt.Println("第一个元素:", slice[0])
fmt.Println("最后一个元素:", slice[len(slice)-1])
// 修改元素
slice[0] = 100
fmt.Println("修改后:", slice)
// 遍历切片
fmt.Println("正向遍历:")
for i := 0; i < len(slice); i++ {
fmt.Printf("slice[%d] = %d ", i, slice[i])
}
fmt.Println()
fmt.Println("range 遍历:")
for index, value := range slice {
fmt.Printf("索引 %d: 值 %d ", index, value)
}
fmt.Println()
// 只遍历索引
for index := range slice {
fmt.Printf("索引: %d ", index)
}
fmt.Println()
// 只遍历值
for _, value := range slice {
fmt.Printf("值: %d ", value)
}
}
2.2 切片长度和容量
func printSliceInfo(name string, s []int) {
fmt.Printf("%s: %v, 长度: %d, 容量: %d\n", name, s, len(s), cap(s))
}
func main() {
arr := [6]int{1, 2, 3, 4, 5, 6}
// 不同切片的长度和容量
s1 := arr[1:4] // [2 3 4],
s2 := arr[2:5] // [3 4 5]
s3 := arr[1:4:5] // [2 3 4],指定容量为5-1=4
printSliceInfo("s1", s1)
printSliceInfo("s2", s2)
printSliceInfo("s3", s3)
// make 创建的切片
s4 := make([]int, 3, 10) // 长度3,容量10
printSliceInfo("s4", s4)
}
注意:
- 切片的长度表示切片中当前包含的元素个数,可以通过
len(slice)
函数获取;- 而切片的容量是指从切片的第一个元素开始,到其底层数组末尾的元素个数,可以通过 cap(slice)函数获取。
简单来说,长度是实际元素的数量,容量是切片在不分配新内存的情况下,最多能追加的元素数量。
2.3 切片扩容操作
① append 操作和扩容
func main() {
// 初始切片
slice := make([]int, 0, 2) // 长度0,容量2
fmt.Printf("初始时: ")
printSliceInfo(slice)
// 逐步添加元素观察扩容
for i := 1; i <= 5; i++ {
slice = append(slice, i)
fmt.Printf("添加 %d 后: ", i)
printSliceInfo(slice)
}
}
func printSliceInfo(s []int) {
fmt.Printf("长度: %d, 容量: %d, 内容: %v\n", len(s), cap(s), s)
}
结果如下:
初始时: 长度: 0, 容量: 2, 内容: []
添加 1 后: 长度: 1, 容量: 2, 内容: [1]
添加 2 后: 长度: 2, 容量: 2, 内容: [1 2]
添加 3 后: 长度: 3, 容量: 4, 内容: [1 2 3]
添加 4 后: 长度: 4, 容量: 4, 内容: [1 2 3 4]
添加 5 后: 长度: 5, 容量: 8, 内容: [1 2 3 4 5]
② 切片扩容规则
func main() {
// 演示扩容机制
slice := make([]int, 0, 1)
addresses := make([]uintptr, 0)
for i := 0; i < 10; i++ {
// 记录底层数组地址
if len(slice) > 0 {
addresses = append(addresses, getSliceAddress(slice))
}
slice = append(slice, i)
}
// 检查地址变化(表示重新分配了底层数组)
fmt.Println("地址变化点:")
for i := 1; i < len(addresses); i++ {
if addresses[i] != addresses[i-1] {
fmt.Printf("在添加第 %d 个元素时发生扩容\n", i)
}
}
}
// 获取切片底层数组地址的辅助函数
func getSliceAddress(s []int) uintptr {
if len(s) == 0 {
return 0
}
return uintptr(unsafe.Pointer(&s[0]))
}
// 输出如下:
地址变化点:
在添加第 1 个元素时发生扩容
在添加第 2 个元素时发生扩容
在添加第 4 个元素时发生扩容
在添加第 8 个元素时发生扩容
3. 切片的高级操作
3.1 切片复制和追加
func main() {
// 1. append 操作
slice1 := []int{1, 2, 3}
slice1 = append(slice1, 4, 5) // 添加多个元素
fmt.Println("append 多个元素:", slice1) // 1 2 3 4 5
// 追加另一个切片
slice2 := []int{6, 7, 8}
slice1 = append(slice1, slice2...) // 使用 ... 展开切片
fmt.Println("append 切片:", slice1) // 1 2 3 4 5 6 7 8
// 2. copy 操作
src := []int{10, 20, 30, 40, 50}
dst := make([]int, 3)
n := copy(dst, src) // 从 src 复制到 dst
fmt.Printf("复制了 %d 个元素: %v\n", n, dst)
// 复制到更大切片
dst2 := make([]int, 10)
n2 := copy(dst2, src)
fmt.Printf("复制了 %d 个元素: %v\n", n2, dst2)
// 3. 切片截取
original := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sub1 := original[2:5] // [3 4 5]
sub2 := original[:4] // [1 2 3 4]
sub3 := original[6:] // [7 8 9 10]
fmt.Println("原切片:", original)
fmt.Println("sub1:", sub1)
fmt.Println("sub2:", sub2)
fmt.Println("sub3:", sub3)
}
3.2 切片删除操作
func main() {
// 1. 删除指定索引的元素
slice := []int{1, 2, 3, 4, 5}
fmt.Println("原切片:", slice)
// 删除索引为2的元素(值为3)
index := 2
slice = append(slice[:index], slice[index+1:]...)
fmt.Println("删除索引2后:", slice)
// 2. 删除第一个元素
slice = slice[1:]
fmt.Println("删除第一个元素后:", slice)
// 3. 删除最后一个元素
slice = slice[:len(slice)-1]
fmt.Println("删除最后一个元素后:", slice)
// 4. 按值删除(删除所有匹配的值)
slice2 := []int{1, 2, 3, 2, 4, 2, 5}
slice2 = removeValue(slice2, 2)
fmt.Println("删除所有值为2的元素:", slice2)
}
// 删除指定值的所有元素
func removeValue(slice []int, value int) []int {
result := make([]int, 0)
for _, v := range slice {
if v != value {
result = append(result, v)
}
}
return result
}
// 更高效的删除方式(保持原切片顺序)
func removeAt(slice []int, index int) []int {
if index < 0 || index >= len(slice) {
return slice
}
return append(slice[:index], slice[index+1:]...)
}
4. 切片注意事项
① 共享底层数组的问题
func main() {
// 切片共享底层数组的陷阱
original := []int{1, 2, 3, 4, 5}
slice1 := original[1:4] // [2 3 4]
fmt.Println("原始切片:", original)
fmt.Println("slice1:", slice1) // [2, 3, 4]
// 修改 slice1 会影响 original
slice1[0] = 100
fmt.Printf("修改 slice1[0] = 100 后, ")
fmt.Printf("原始切片: %d, ", original) // [1 100 3 4 5]
fmt.Println("slice1: ", slice1) // [100 3 4]
// 正确的复制方式
slice2 := make([]int, len(original[1:4]))
copy(slice2, original[1:4]) // [100 3 4]
slice2[0] = 200
fmt.Printf("正确复制后修改,")
fmt.Printf("原始切片: %d,", original) // [1 100 3 4 5]
fmt.Println("slice2:", slice2) // [200 3 4]
}
② 切片扩容陷阱
func main() {
// 切片扩容陷阱
s1 := []int{1, 2, 3}
s2 := s1[1:] // [2 3]
fmt.Printf("s1: %v, 地址: %p\n", s1, &s1[0])
fmt.Printf("s2: %v, 地址: %p\n", s2, &s2[0])
// 当 s2 扩容时,可能会重新分配底层数组
s2 = append(s2, 4, 5, 6, 7, 8, 9) // 足够多的元素触发扩容
fmt.Printf("扩容后 s1: %v\n", s1)
fmt.Printf("扩容后 s2: %v\n", s2)
fmt.Printf("扩容后 s2 地址: %p\n", &s2[0])
}
5. 应用场景
① 字符串处理
func main() {
// 字符串转切片
str := "hello,world,go"
parts := strings.Split(str, ",")
fmt.Println("分割结果:", parts)
// 切片转字符串
joined := strings.Join(parts, "-")
fmt.Println("连接结果:", joined)
// 字符串切片操作
runes := []rune("Hello, 世界")
fmt.Println("字符切片:", runes)
fmt.Printf("第一个字符: %c\n", runes[0])
fmt.Printf("最后一个字符: %c\n", runes[len(runes)-1])
}
② 切片排序
func main() {
// 整数切片排序
numbers := []int{5, 2, 8, 1, 9, 3}
fmt.Println("排序前:", numbers)
sort.Ints(numbers)
fmt.Println("排序后:", numbers)
// 字符串切片排序
words := []string{"banana", "apple", "cherry", "date"}
fmt.Println("排序前:", words)
sort.Strings(words)
fmt.Println("排序后:", words)
// 自定义排序
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// 按年龄排序
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println("按年龄排序:")
for _, p := range people {
fmt.Printf("%s: %d岁\n", p.Name, p.Age)
}
}
③ 切片搜索
func main() {
// 有序切片的二分搜索
sorted := []int{1, 3, 5, 7, 9, 11, 13, 15}
// 使用 sort.SearchInts
index := sort.SearchInts(sorted, 7)
if index < len(sorted) && sorted[index] == 7 {
fmt.Printf("找到 7,索引为: %d\n", index)
} else {
fmt.Println("未找到 7")
}
// 自定义搜索
target := 6
index2 := sort.Search(len(sorted), func(i int) bool {
return sorted[i] >= target
})
if index2 < len(sorted) && sorted[index2] == target {
fmt.Printf("找到 %d,索引为: %d\n", target, index2)
} else {
fmt.Printf("%d 应该插入到索引: %d\n", target, index2)
}
}
6. 性能优化技巧
① 预分配容量
func main() {
// 不预分配容量
start := time.Now()
var slice1 []int
for i := 0; i < 100000; i++ {
slice1 = append(slice1, i)
}
duration1 := time.Since(start)
// 预分配容量
start = time.Now()
slice2 := make([]int, 0, 100000) // 预分配足够容量
for i := 0; i < 100000; i++ {
slice2 = append(slice2, i)
}
duration2 := time.Since(start)
fmt.Printf("不预分配耗时: %v\n", duration1)
fmt.Printf("预分配耗时: %v\n", duration2)
fmt.Printf("性能提升: %.2f 倍\n", float64(duration1)/float64(duration2))
}
② 避免不必要复制
func main() {
// 大切片的处理
bigSlice := make([]int, 1000000)
for i := range bigSlice {
bigSlice[i] = i
}
// 错误的方式:复制整个大切片
smallSlice1 := bigSlice[999990:] // 可能复制整个底层数组
// 正确的方式:创建新的小切片
smallSlice2 := make([]int, 10)
copy(smallSlice2, bigSlice[999990:])
fmt.Printf("smallSlice1 长度: %d, 容量: %d\n", len(smallSlice1), cap(smallSlice1))
fmt.Printf("smallSlice2 长度: %d, 容量: %d\n", len(smallSlice2), cap(smallSlice2))
}
二、Map 映射
Map 是 Go 语言中的键值对(key-value)数据结构,也称为字典或哈希表。它提供了快速的键值查找能力。
map 语法结构
map[键类型]值类型
// 比如
map[string]string
map[string]int
1. Map 的基本操作
① 遍历 Map 的多种方式
func main() {
// 1. 使用 make 创建空 map
var map1 map[string]int
map1 = make(map[string]int)
// 或者简写
map2 := make(map[string]int)
// 2. 字面量创建 map
map3 := map[string]int{
"张三": 25,
"李四": 30,
"王五": 28,
}
// 3. 创建空的字面量 map
map4 := map[string]int{}
// 4. 指定初始容量(可选优化)
map5 := make(map[string]int, 100) // 预分配容量
fmt.Printf("map1: %v\n", map1)
fmt.Printf("map2: %v\n", map2)
fmt.Printf("map3: %v\n", map3)
fmt.Printf("map4: %v\n", map4)
fmt.Printf("map5: %v\n", map5)
// 不同类型的 map
stringMap := map[string]string{
"name": "张三",
"city": "北京",
"country": "中国",
}
intMap := map[int]string{
1: "一",
2: "二",
3: "三",
}
fmt.Printf("stringMap: %v\n", stringMap)
fmt.Printf("intMap: %v\n", intMap)
}
② 增、删、改、查操作
func main() {
// 创建 map
ages := make(map[string]int)
// 1. 添加/修改元素(赋值操作)
ages["张三"] = 25
ages["李四"] = 30
ages["王五"] = 28
fmt.Println("添加元素后:", ages)
// 2. 修改元素
ages["张三"] = 26 // 修改张三的年龄
fmt.Println("修改后:", ages)
// 3. 查找元素
age, exists := ages["李四"]
if exists {
fmt.Printf("李四的年龄: %d\n", age)
} else {
fmt.Println("未找到李四")
}
// 简化的查找方式(如果不需要区分零值和不存在)
fmt.Printf("王五的年龄: %d\n", ages["王五"])
// 4. 删除元素
delete(ages, "王五")
fmt.Println("删除王五后:", ages)
// 删除不存在的键不会报错
delete(ages, "不存在的人")
fmt.Println("删除不存在的键后:", ages)
}
③ Map 的遍历
func main() {
scores := map[string]float64{
"数学": 95.5,
"英语": 87.0,
"语文": 92.5,
"物理": 88.0,
}
fmt.Println("原始 map:", scores)
fmt.Println()
// 1. 基本遍历(注意:顺序不固定)
fmt.Println("基本遍历:")
for subject, score := range scores {
fmt.Printf("%s: %.1f分\n", subject, score)
}
fmt.Println()
// 2. 只遍历键
fmt.Println("只遍历键:")
for subject := range scores {
fmt.Printf("科目: %s\n", subject)
}
fmt.Println()
// 3. 只遍历值
fmt.Println("只遍历值:")
for _, score := range scores {
fmt.Printf("分数: %.1f\n", score)
}
fmt.Println()
// 4. 按照特定顺序遍历(需要先排序键)
fmt.Println("按字母顺序遍历:")
keys := make([]string, 0, len(scores))
for key := range scores {
keys = append(keys, key)
}
// 对键进行排序
// 注意:需要导入 sort 包
fmt.Println("keys:", keys)
}
2. Map 特性
① Map 的零值和 nil
func main() {
// 1. nil map
var nilMap map[string]int
fmt.Printf("nilMap: %v, 是否为 nil: %t\n", nilMap, nilMap == nil) // map[] true
// nil map 可以读取(返回零值)
fmt.Printf("nilMap['key']: %d\n", nilMap["key"]) // 0
// nil map 不能写入(会导致 panic)
// nilMap["key"] = 1 // 这会 panic!
// 2. 空 map
emptyMap := make(map[string]int)
fmt.Printf("emptyMap: %v, 是否为 nil: %t\n", emptyMap, emptyMap == nil) // map[] false
// 空 map 可以正常读写
emptyMap["key"] = 1
fmt.Printf("emptyMap['key']: %d\n", emptyMap["key"])
// 3. 检查键是否存在
value, exists := emptyMap["不存在的键"]
if !exists {
fmt.Println("键不存在,返回零值:", value)
}
}
② Map 的键类型限制
func main() {
// Map 的键必须是可比较的类型
// ✅ 合法的键类型
intMap := map[int]string{1: "一", 2: "二"}
stringMap := map[string]int{"one": 1, "two": 2}
boolMap := map[bool]string{true: "真", false: "假"}
// 复合键类型
pointMap := map[[2]int]string{[2]int{0, 0}: "原点"}
fmt.Println("intMap:", intMap)
fmt.Println("stringMap:", stringMap)
fmt.Println("boolMap:", boolMap)
fmt.Println("pointMap:", pointMap)
// ❌ 非法的键类型(编译错误)
// sliceMap := map[[]int]string{} // 错误:slice 不可比较
// mapMap := map[map[string]int]string{} // 错误:map 不可比较
// funcMap := map[func()]string{} // 错误:函数不可比较
}
3. Map 嵌套和高级操作
嵌套 Map 和复杂数据结构
func main() {
// 1. 嵌套 Map
studentGrades := map[string]map[string]float64{
"张三": {
"数学": 95.5,
"英语": 87.0,
"语文": 92.5,
},
"李四": {
"数学": 88.0,
"英语": 91.5,
"语文": 89.0,
},
}
fmt.Println("嵌套 Map:")
for student, grades := range studentGrades {
fmt.Printf("%s 的成绩:\n", student)
for subject, score := range grades {
fmt.Printf(" %s: %.1f\n", subject, score)
}
}
// 访问嵌套 Map
zhangsanMath := studentGrades["张三"]["数学"]
fmt.Printf("\n张三的数学成绩: %.1f\n", zhangsanMath)
// 2. Map + Slice 组合
classStudents := map[string][]string{
"一班": {"张三", "李四", "王五"},
"二班": {"赵六", "钱七", "孙八"},
}
fmt.Println("\n班级学生:")
for class, students := range classStudents {
fmt.Printf("%s: %v\n", class, students)
}
}
Map 函数
// 统计字符出现次数
func countChars(s string) map[rune]int {
counts := make(map[rune]int)
for _, char := range s {
counts[char]++
}
return counts
}
// 查找最大值对应的键
func findMaxKey(m map[string]int) (string, int) {
if len(m) == 0 {
return "", 0
}
maxKey := ""
maxValue := 0
for key, value := range m {
if value > maxValue {
maxValue = value
maxKey = key
}
}
return maxKey, maxValue
}
func main() {
// 字符统计示例
text := "hello world"
charCounts := countChars(text)
fmt.Printf("'%s' 中字符统计: %v\n", text, charCounts)
// 查找最大值
scores := map[string]int{
"张三": 95,
"李四": 87,
"王五": 92,
"赵六": 89,
}
bestStudent, highestScore := findMaxKey(scores)
fmt.Printf("最高分: %s 得分 %d\n", bestStudent, highestScore)
}
4. 并发安全
① Map 不是并发安全
func main() {
regularMap := make(map[int]int) // 普通 map 在并发访问时会有问题
var wg sync.WaitGroup // 并发读写会导致 panic
// 启动多个 goroutine 同时写入
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
regularMap[id*100+j] = id
}
}(i)
}
// 启动多个 goroutine 同时读取
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
_ = regularMap[j]
}
}()
}
wg.Wait()
fmt.Println("普通 map 并发操作完成(可能 panic)")
}
② sync.Map 并发安全的 Map
func main() {
// sync.Map 是并发安全的
var safeMap sync.Map
// 存储值
safeMap.Store("key1", "value1")
safeMap.Store("key2", "value2")
// 读取值
if value, ok := safeMap.Load("key1"); ok {
fmt.Println("key1:", value)
}
// 删除值
safeMap.Delete("key1")
// 遍历
fmt.Println("遍历 sync.Map:")
safeMap.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true // 返回 true 继续遍历,false 停止
})
// 原子操作
if actual, loaded := safeMap.LoadOrStore("key3", "value3"); loaded {
fmt.Println("key3 已存在,值为:", actual)
} else {
fmt.Println("key3 不存在,已存储新值")
}
}
5. 性能优化
① 预分配容量优化
func main() {
// 测试预分配容量的性能差异
const size = 100000
// 不预分配容量
start := time.Now()
map1 := make(map[int]int)
for i := 0; i < size; i++ {
map1[i] = i * 2
}
duration1 := time.Since(start)
// 预分配容量
start = time.Now()
map2 := make(map[int]int, size) // 预分配容量
for i := 0; i < size; i++ {
map2[i] = i * 2
}
duration2 := time.Since(start)
fmt.Printf("不预分配耗时: %v\n", duration1)
fmt.Printf("预分配耗时: %v\n", duration2)
fmt.Printf("性能提升: %.2f%%\n",
float64(duration1-duration2)/float64(duration1)*100)
}
② 内存使用优化
func main() {
// 1. 及时删除不需要的键值对
cache := make(map[string]string)
// 模拟缓存使用
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key_%d", i)
cache[key] = fmt.Sprintf("value_%d", i)
}
fmt.Printf("缓存大小: %d\n", len(cache))
// 清理旧缓存
for key := range cache {
if len(key) > 10 {
delete(cache, key)
}
}
fmt.Printf("清理后大小: %d\n", len(cache))
// 2. 重置 map(完全清空)
// 方法1:重新创建
cache = make(map[string]string)
// 方法2:逐个删除(不推荐)
// for key := range cache {
// delete(cache, key)
// }
}