集合类型
7.1 数组与切片
Section titled “7.1 数组与切片”数组(Array)
Section titled “数组(Array)”固定长度,在Go中数组是值类型:
// 声明数组var arr [5]int // [0, 0, 0, 0, 0]arr[0] = 10
// 初始化arr2 := [5]int{1, 2, 3, 4, 5}
// 自动计算长度arr3 := [...]int{1, 2, 3} // [3]int{1, 2, 3}
// 指定索引初始化arr4 := [5]int{0: 1, 2: 3} // [1, 0, 3, 0, 0]数组是值类型:
arr1 := [3]int{1, 2, 3}arr2 := arr1 // 复制整个数组arr2[0] = 100
fmt.Println(arr1) // [1, 2, 3](未变)fmt.Println(arr2) // [100, 2, 3]Python列表是引用类型:
arr1 = [1, 2, 3]arr2 = arr1arr2[0] = 100print(arr1) # [100, 2, 3](原数组被修改)切片(Slice)
Section titled “切片(Slice)”动态大小,是数组的视图。Go最常用的集合类型:
// 创建切片slice := []int{1, 2, 3} // 字面量slice := make([]int, 5) // [0, 0, 0, 0, 0],len=5, cap=5slice := make([]int, 0, 10) // len=0, cap=10
// 从数组创建切片arr := [5]int{1, 2, 3, 4, 5}slice := arr[1:4] // [2, 3, 4](arr[1], arr[2], arr[3])
// 省略起始/结束索引slice := arr[:3] // [1, 2, 3]slice := arr[2:] // [3, 4, 5]slice := arr[:] // [1, 2, 3, 4, 5]slice := []int{1, 2, 3}
// append添加元素slice = append(slice, 4) // [1, 2, 3, 4]slice = append(slice, 5, 6) // [1, 2, 3, 4, 5, 6]
// len和capfmt.Println(len(slice)) // 6fmt.Println(cap(slice)) // 6(或更大,取决于实现)
// 遍历for i, v := range slice { fmt.Printf("%d: %d\n", i, v)}Python对比
Section titled “Python对比”# 列表基本操作lst = [1, 2, 3]lst.append(4) # [1, 2, 3, 4]len(lst) # 4
# 切片lst[1:3] # [2, 3]lst[:3] # [1, 2, 3]lst[2:] # [3, 4]7.2 切片底层原理与性能
Section titled “7.2 切片底层原理与性能”切片数据结构
Section titled “切片数据结构”切片是三个字段的结构体:
type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 长度 cap int // 容量}当对切片进行操作时:
- len表示可见元素数量
- cap表示底层数组容量
- 当len==cap时,append会分配新数组
append行为
Section titled “append行为”slice := make([]int, 0, 3) // len=0, cap=3
slice = append(slice, 1) // len=1, cap=3(还有空间)slice = append(slice, 2) // len=2, cap=3(还有空间)slice = append(slice, 3) // len=3, cap=3(刚好满)slice = append(slice, 4) // len=4, cap=6(扩容,新建数组)扩容策略:
- 小切片(cap小於1024):约1.5倍增长
- 大切片(cap大於等於1024):约1.25倍增长
多个切片可以共享同一个底层数组:
arr := [5]int{1, 2, 3, 4, 5}s1 := arr[0:3] // [1, 2, 3]s2 := arr[1:4] // [2, 3, 4]
s1[1] = 20 // 修改底层数组fmt.Println(arr) // [1, 20, 3, 4, 5]fmt.Println(s1) // [1, 20, 3]fmt.Println(s2) // [20, 3, 4](受影响)copy行为
Section titled “copy行为”src := []int{1, 2, 3}dst := make([]int, len(src))
// 复制元素copy(dst, src)
// 修改src不影响dstsrc[0] = 100fmt.Println(dst) // [1, 2, 3]常见性能陷阱
Section titled “常见性能陷阱”1. 重复append导致多次分配
// 不好:每次append可能扩容var slice []intfor i := 0; i < 1000; i++ { slice = append(slice, i)}
// 好:预估容量slice := make([]int, 0, 1000)for i := 0; i < 1000; i++ { slice = append(slice, i)}2. 在函数中修改切片
func badAppend(slice []int) { slice = append(slice, 100) // 修改的是副本}
func goodAppend(slice *[]int) { *slice = append(*slice, 100) // 修改原切片}make vs new
Section titled “make vs new”// make:创建切片、map、channel(返回的是这些类型本身)s := make([]int, 10) // len=10, cap=10, 元素为0
// new:分配内存,返回指针(用于任何类型)p := new([]int) // *[]int,值为nilp2 := new([5]int) // *[5]int,指向新数组7.3 Map:键值对集合
Section titled “7.3 Map:键值对集合”// 使用makem := make(map[string]int)
// 字面量初始化m := map[string]int{ "Alice": 30, "Bob": 25, "Carol": 35,}
// 空mapvar m map[string]int // nil map(不能直接操作)m = make(map[string]int) // 初始化后才能使用m := map[string]int{ "Alice": 30, "Bob": 25,}
// 添加/修改m["Dave"] = 40
// 读取age := m["Alice"] // 30age = m["Eve"] // 0(键不存在,返回零值)
// 检查键是否存在age, ok := m["Eve"]if ok { fmt.Println("存在:", age)} else { fmt.Println("不存在")}
// 删除delete(m, "Bob")
// 检查长度fmt.Println(len(m)) // 3Python对比
Section titled “Python对比”m = {"Alice": 30, "Bob": 25}
# 添加/修改m["Dave"] = 40
# 读取age = m["Alice"] # 30age = m.get("Eve", 0) # 0(默认值)
# 删除del m["Bob"]
# 检查键if "Eve" in m: print(m["Eve"])
# 长度len(m) # 3Map的nil
Section titled “Map的nil”var m map[string]int // nil map
// 读取nil map是安全的fmt.Println(m["key"]) // 0
// 写入nil map会panic// m["key"] = 1 // panic
// 检查if m == nil { fmt.Println("map是nil,需要初始化") m = make(map[string]int)}Map不是线程安全的
Section titled “Map不是线程安全的”多个goroutine同时读写map需要加锁:
var mu sync.RWMutexvar m = make(map[string]int)
// 读(可以并发)mu.RLock()age := m["Alice"]mu.RUnlock()
// 写(需要独占)mu.Lock()m["Bob"] = 30mu.Unlock()Go 1.9+有sync.RWMutex,Go 1.21+实验性sync.Map(适合读多写少场景)。
7.4 遍历与迭代
Section titled “7.4 遍历与迭代”slice := []int{1, 2, 3}
// 索引和值for i, v := range slice { fmt.Printf("%d: %d\n", i, v)}
// 仅索引for i := range slice { fmt.Println(i)}
// 仅值(索引丢弃)for _, v := range slice { fmt.Println(v)}m := map[string]int{ "Alice": 30, "Bob": 25, "Carol": 35,}
// 键和值for k, v := range m { fmt.Printf("%s: %d\n", k, v)}
// 仅键for k := range m { fmt.Println(k)}
// 仅值(需要用map[key]获取)for _, v := range m { fmt.Println(v)}遍历顺序不确定:Map是无序的,每次遍历顺序可能不同。
// 排序遍历keys := make([]string, 0, len(m))for k := range m { keys = append(keys, k)}sort.Strings(keys)
for _, k := range keys { fmt.Printf("%s: %d\n", k, m[k])}遍历字符串(UTF-8)
Section titled “遍历字符串(UTF-8)”str := "Hello世界"
// 索引和runefor i, r := range str { fmt.Printf("%d: %c (U+%04X)\n", i, r, r)}
// 输出:// 0: H (U+0048)// 1: e (U+0065)// 2: l (U+006C)// 3: l (U+006C)// 4: o (U+006F)// 6: 世 (U+4E16)// 9: 界 (U+754C)注意:中文占3字节,所以下一个字符的索引是6而不是5。
字符串字节vs字符
Section titled “字符串字节vs字符”str := "Hello世界"
fmt.Println(len(str)) // 13(字节数)fmt.Println(utf8.RuneCountInString(str)) // 8(字符数)
// 逐字节遍历for i := 0; i < len(str); i++ { fmt.Printf("%x ", str[i]) // 48 65 6c 6c 6f e4 b8 96 e7 95 8c}
// 逐rune遍历for _, r := range str { fmt.Printf("%c ", r) // H e l l o 世 界}过滤切片元素
Section titled “过滤切片元素”numbers := []int{1, 2, 3, 4, 5, 6}
// 过滤偶数result := []int{}for _, n := range numbers { if n%2 == 0 { result = append(result, n) }}fmt.Println(result) // [2, 4, 6]
// 使用append的简洁写法result = result[:0] // 清空slice但保留底层数组for _, n := range numbers { if n%2 == 0 { result = append(result, n) }}| Python | Go |
|---|---|
list | slice([]T) |
[1, 2, 3] | []int{1, 2, 3} |
arr[1:3] | arr[1:4](索引是结束位置+1) |
list.append(x) | slice = append(slice, x) |
dict | map[K]V |
d["key"] / d.get("key") | m["key"] / age, ok := m["key"] |
del d["key"] | delete(m, "key") |
for i, v in enumerate(lst): | for i, v := range lst { } |
for k, v in d.items(): | for k, v := range m { } |
- 创建切片,使用append添加10个元素
- 写函数从切片中间删除一个元素
- 创建map,练习添加、读取、删除操作
- 实现一个统计单词频率的程序
- 写函数过滤切片中的负数,返回新切片