编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
        • 目录介绍
        • 5.1 本章学习目标
        • 5.2 数组(array)
          • 5.2.1 定长性:长度是类型的一部分
          • 5.2.2 数组是值类型
          • 5.2.3 数组在 Go 中很少直接用
        • 5.3 切片(slice)
          • 5.3.1 切片底层三元组:(ptr, len, cap)
          • 5.3.2 make 与字面量创建切片
          • 5.3.3 append 扩容算法
          • 5.3.4 子切片共享底层数组(陷阱重灾区)
          • 5.3.5 三索引 s[i:j:k] 限制 cap
          • 5.3.6 copy 与"深拷贝"
        • 5.4 映射(map)
          • 5.4.1 make(map[K]V) 与字面量
          • 5.4.2 双返回值取值 v, ok := m[k]
          • 5.4.3 delete 与 nil map 写入 panic
          • 5.4.4 map 遍历顺序随机(设计意图)
          • 5.4.5 map 不可寻址(不能 &m["key"])
          • 5.4.6 并发读写 map 直接 fatal(不是 panic)
        • 5.5 结构体(struct)
          • 5.5.1 字段定义与初始化(命名 vs 位置)
          • 5.5.2 嵌入字段(无字段名)
          • 5.5.3 Tag:反射的元数据
          • 5.5.4 字段对齐与省内存技巧
          • 5.5.5 == 可比较性规则
        • 5.6 字符串(string)的本质
          • 5.6.1 不可变只读字节序列
          • 5.6.2 字符串与切片互转的零拷贝优化(Go 1.20+)
        • 5.7 综合示例:构建一个 LRU 缓存
        • 5.8 本章底层原理(简介)
        • 5.9 Go 新手陷阱 Top 5
          • ❌ 陷阱 1:函数内 append(s, x) 不返回赋值
          • ❌ 陷阱 2:用子切片 s[1:3] 后修改它,原 slice 也变
          • ❌ 陷阱 3:给 nil map 赋值 panic
          • ❌ 陷阱 4:循环里 &v 共享地址(Go 1.21 及之前)
          • ❌ 陷阱 5:含 slice/map/func 字段的 struct 用 == 比较
        • 5.10 思考题
        • 5.11 推荐阅读
          • 卷内
          • 跨卷
          • 外部资料
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

复合类型

# 第 5 章 复合类型:array / slice / map / struct / string

Go 的复合类型是工程中用得最频繁的核心——slice 是 90% 场景的容器,map 是 80% 场景的字典,struct 是 OOP 的载体。 关键词:array 定长、slice 三元组、map 引用语义、struct 字段对齐、string 不可变


# 目录介绍

  • 5.1 本章学习目标
  • 5.2 数组(array)
    • 5.2.1 定长性:长度是类型的一部分
    • 5.2.2 数组是值类型
    • 5.2.3 数组在 Go 中很少直接用
  • 5.3 切片(slice)
    • 5.3.1 切片底层三元组:(ptr, len, cap)
    • 5.3.2 make 与字面量创建切片
    • 5.3.3 append 扩容算法
    • 5.3.4 子切片共享底层数组(陷阱重灾区)
    • 5.3.5 三索引 s[i:j:k] 限制 cap
    • 5.3.6 copy 与"深拷贝"
  • 5.4 映射(map)
    • 5.4.1 make(map[K]V) 与字面量
    • 5.4.2 双返回值取值 v, ok := m[k]
    • 5.4.3 delete 与 nil map 写入 panic
    • 5.4.4 map 遍历顺序随机(设计意图)
    • 5.4.5 map 不可寻址(不能 &m["key"])
    • 5.4.6 并发读写 map 直接 fatal(不是 panic)
  • 5.5 结构体(struct)
    • 5.5.1 字段定义与初始化(命名 vs 位置)
    • 5.5.2 嵌入字段(无字段名)
    • 5.5.3 Tag:反射的元数据
    • 5.5.4 字段对齐与省内存技巧
    • 5.5.5 == 可比较性规则
  • 5.6 字符串(string)的本质
    • 5.6.1 不可变只读字节序列
    • 5.6.2 字符串与切片互转的零拷贝优化(Go 1.20+)
  • 5.7 综合示例:构建一个 LRU 缓存
  • 5.8 本章底层原理(简介)
  • 5.9 Go 新手陷阱 Top 5
  • 5.10 思考题
  • 5.11 推荐阅读

# 5.1 本章学习目标

  • ✅ 能解释 slice 的三元组结构以及 append 何时分配新底层数组
  • ✅ 知道 map 的 5 个关键陷阱(nil 写、并发、不可寻址、随机遍历、零值取出)
  • ✅ 能根据字段类型重排 struct 来省内存
  • ✅ 知道 string 不可变的"语言级保证"以及它的 unsafe 突破方式
  • ✅ 能默写 LRU 缓存(map + 双向链表 + 容量上限)

# 5.2 数组(array)

# 5.2.1 定长性:长度是类型的一部分

var a [5]int        // 长度 5,零值 [0 0 0 0 0]
var b [3]string     // 长度 3,零值 ["" "" ""]
c := [4]int{1, 2, 3, 4}
d := [...]int{1, 2, 3} // 编译器自动推导长度为 3
1
2
3
4

重点:[5]int 与 [6]int 是两个完全不同的类型,不能互相赋值:

var a [5]int
var b [6]int
a = b // ❌ 编译错:cannot use b (type [6]int) as type [5]int
1
2
3

这与 C/C++ 的"数组退化为指针"截然不同。Go 数组的"长度即类型"让编译器在编译期就能做边界检查。

# 5.2.2 数组是值类型

a := [3]int{1, 2, 3}
b := a       // 整体复制(不是共享)
b[0] = 99
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [99 2 3]
1
2
3
4
5

函数传参也是完整复制——一个 [1024]int 数组传参就是 8KB 复制。这就是为什么实践中我们几乎总是用 slice 而不是 array。

# 5.2.3 数组在 Go 中很少直接用

工程实践里,数组主要出现在:

  • 哈希底层(如 SHA256 返回 [32]byte)
  • 固定形状的数学结构(如 [3][3]float64 表示 3×3 矩阵)
  • 作为 slice 的"底层"被动出现(slice 内部就指向一个 array)

其他场景几乎都用 slice。记住这个比例:array : slice ≈ 5% : 95%。


# 5.3 切片(slice)

# 5.3.1 切片底层三元组:(ptr, len, cap)

切片不是数组,它是一个3 字段的小 struct:

slice struct (24 字节, 64-bit 平台):
┌─────────┬─────┬─────┐
│  ptr    │ len │ cap │
└─────────┴─────┴─────┘
   ↓
[底层数组] [0] [1] [2] [3] [4] ... [cap-1]
            ←── len ──→
            ←──────── cap ────────→
1
2
3
4
5
6
7
8
  • ptr:指向底层数组的指针
  • len:当前可访问的元素数(s[0..len-1] 合法,越界 panic)
  • cap:底层数组实际容量(s[len..cap-1] 是预留空间)

获取它们:

s := []int{10, 20, 30}
fmt.Println(len(s), cap(s)) // 3 3
1
2

# 5.3.2 make 与字面量创建切片

4 种创建方式:

// 1. 字面量(最常用)
s1 := []int{1, 2, 3}            // len=3, cap=3

// 2. make(类型, len)
s2 := make([]int, 5)            // len=5, cap=5, 元素全是 0

// 3. make(类型, len, cap)(提前分配 cap,避免后续 append 频繁扩容)
s3 := make([]int, 0, 100)       // len=0, cap=100

// 4. nil 切片
var s4 []int                    // len=0, cap=0, ptr=nil
1
2
3
4
5
6
7
8
9
10
11

nil 切片 vs 空切片:

var s1 []int          // nil 切片
s2 := []int{}         // 空切片(不是 nil)

fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1), len(s2)) // 0 0(都是 0)
1
2
3
4
5
6

实务上两者行为几乎一样——都能 len()、能 for range、能 append,唯一区别是 == nil 比较。首选 var s []int(nil 切片)。

# 5.3.3 append 扩容算法

append 是 slice 的核心操作:

s := []int{1, 2, 3}
s = append(s, 4)        // 单值追加
s = append(s, 5, 6, 7)  // 多值追加
s = append(s, other...) // 拼接另一个 slice
1
2
3
4

关键:当 len == cap 时,append 会分配新的底层数组并复制:

s := make([]int, 3, 3)
fmt.Printf("ptr=%p len=%d cap=%d\n", s, len(s), cap(s))
// ptr=0xc0000... len=3 cap=3

s = append(s, 4)
fmt.Printf("ptr=%p len=%d cap=%d\n", s, len(s), cap(s))
// ptr=0xc0001... len=4 cap=6  ← 新地址,cap 翻倍
1
2
3
4
5
6
7

扩容策略(Go 1.18+ 简化版):

当前 cap 新 cap
< 256 翻倍(2 倍)
≥ 256 渐进收敛到 1.25 倍

精确公式见 runtime/slice.go: growslice。实务建议:能预估容量就用 make([]T, 0, n) 一次性分配,避免多次扩容拷贝。

// ❌ 多次扩容
ids := []int{}
for _, u := range users { ids = append(ids, u.ID) }

// ✅ 预分配
ids := make([]int, 0, len(users))
for _, u := range users { ids = append(ids, u.ID) }
1
2
3
4
5
6
7

# 5.3.4 子切片共享底层数组(陷阱重灾区)

arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3]           // [2 3],但 cap = 4(从 arr[1] 到 arr[4])
sub[0] = 999              // ⚠️ 同时修改了 arr!
fmt.Println(arr)          // [1 999 3 4 5]
1
2
3
4

为什么 cap=4? 因为 sub 共享 arr 的底层数组,从下标 1 起到末尾,剩下 4 个槽位。

更隐蔽的坑——子切片 append 可能覆盖原 slice:

arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3]                // [2 3],cap=4
sub = append(sub, 999)         // 没扩容(cap 够),写入 arr[3]
fmt.Println(arr)               // [1 2 3 999 5] ← arr 被污染!
1
2
3
4

修复办法 1:复制独立:

sub := append([]int(nil), arr[1:3]...) // 独立的新底层数组
// 或 Go 1.21+
sub := slices.Clone(arr[1:3])
1
2
3

修复办法 2:三索引限制 cap(见下节)。

# 5.3.5 三索引 s[i:j:k] 限制 cap

arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3:3]              // len=2, cap=2(k=3 限制 cap = k-i = 2)
sub = append(sub, 999)         // 必扩容到新数组,不会影响 arr
fmt.Println(arr)               // [1 2 3 4 5]
1
2
3
4

s[i:j:k] 的含义:len = j-i,cap = k-i。这是"逃避共享底层数组陷阱"的最干净方式。返回子切片给外部时,强烈建议用三索引。

# 5.3.6 copy 与"深拷贝"

copy(dst, src) 复制元素,返回实际复制的元素数(min(len(dst), len(src))):

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // n = 3, dst = [1 2 3]
1
2
3

经典模式:

// 1. 完整克隆
clone := make([]int, len(src))
copy(clone, src)
// 或 Go 1.21+
clone := slices.Clone(src)

// 2. 删除中间元素(保持顺序)
i := 2
src = append(src[:i], src[i+1:]...)
// 或 Go 1.21+
src = slices.Delete(src, i, i+1)
1
2
3
4
5
6
7
8
9
10
11

⚠️ 注意:copy 只做浅拷贝——如果元素是指针/slice/map,复制的是引用,不是底层数据。深拷贝需要自己写或借助库(encoding/gob、json.Marshal/Unmarshal 等)。


# 5.4 映射(map)

# 5.4.1 make(map[K]V) 与字面量

// 1. make
m1 := make(map[string]int)        // 空 map,可写
m2 := make(map[string]int, 100)   // 提示 hint cap,减少扩容

// 2. 字面量
m3 := map[string]int{
    "one":   1,
    "two":   2,
    "three": 3,
}

// 3. nil map(只读)
var m4 map[string]int             // nil
fmt.Println(m4["any"])            // 0(读 nil map 合法,返回零值)
m4["x"] = 1                       // ❌ panic: assignment to entry in nil map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Key 类型必须可比较(支持 ==)。所以 slice/map/func 不能做 key;指针、interface、struct(含可比较字段)可以。

# 5.4.2 双返回值取值 v, ok := m[k]

map 索引有两种返回形式:

m := map[string]int{"a": 1}

// 单值:key 不存在时返回**零值**,无法区分"没有"和"是 0"
v := m["a"]      // 1
v = m["zzz"]     // 0

// 双值:ok 表示是否存在
v, ok := m["a"]   // v=1, ok=true
v, ok = m["zzz"]  // v=0, ok=false
1
2
3
4
5
6
7
8
9

最佳实践:除非你确定值的零值不可能是合法值,否则永远用双返回值。

# 5.4.3 delete 与 nil map 写入 panic

m := map[string]int{"a": 1}
delete(m, "a")       // 安全,无返回值
delete(m, "zzz")     // 删不存在的 key 也不会 panic

var m2 map[string]int
m2["x"] = 1          // ❌ panic
1
2
3
4
5
6

最常见 bug:在 struct 字段上忘了 make:

type Cache struct {
    data map[string]int  // 未初始化
}

c := Cache{}
c.data["x"] = 1 // ❌ panic
1
2
3
4
5
6

修复:在构造函数里 make:

func NewCache() *Cache {
    return &Cache{data: make(map[string]int)}
}
1
2
3

# 5.4.4 map 遍历顺序随机(设计意图)

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)
}
// 第一次跑:c 3 / a 1 / b 2
// 第二次跑:b 2 / c 3 / a 1
1
2
3
4
5
6

Go 故意每次启动都用不同随机种子,让你不能依赖 map 遍历顺序——这是为了避免代码"看起来稳定但其实暗中依赖某次哈希结果",迁移到不同 Go 版本就坏。

需要稳定顺序?把 key 提取出来排序:

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println(k, m[k])
}
1
2
3
4
5
6
7
8

或 Go 1.21+:

import "slices"

keys := slices.Sorted(maps.Keys(m))
for _, k := range keys { ... }
1
2
3
4

# 5.4.5 map 不可寻址(不能 &m["key"])

m := map[string]int{"a": 1}
p := &m["a"] // ❌ 编译错:cannot take the address of m["a"]
1
2

为什么? map 内部为了支持扩容(rehash),元素的内存位置随时可能变。如果允许取地址,扩容后这个指针就成了野指针。Go 选择直接禁止取址。

典型坑——struct 值类型:

type User struct {
    Name string
    Age  int
}

m := map[string]User{"u1": {"Alice", 20}}
m["u1"].Age = 21  // ❌ 编译错:cannot assign to struct field m["u1"].Age in map
1
2
3
4
5
6
7

修复:要么改用指针,要么取出来改回去:

// 方案 A:value 用指针
m := map[string]*User{"u1": {"Alice", 20}}
m["u1"].Age = 21 // ✅

// 方案 B:取出 → 改 → 写回
u := m["u1"]
u.Age = 21
m["u1"] = u
1
2
3
4
5
6
7
8

# 5.4.6 并发读写 map 直接 fatal(不是 panic)

m := map[int]int{}
go func() { for { m[1] = 1 } }()
go func() { for { _ = m[1] } }()
time.Sleep(time.Second)

// fatal error: concurrent map read and map write
1
2
3
4
5
6

注意是 fatal,不是 panic——recover 救不回来!这是 Go runtime 在检测到并发读写时直接调用 fatalthrow。

修复方案:

方案 适用场景
sync.Mutex 全锁 key 数量不大、读写都频繁
sync.RWMutex 读多写少
sync.Map key 集合稳定、读多写少(key 反复增删则慢)
分片 map(sharded map) 高并发热点

详见 第 14 章 同步 sync 包。


# 5.5 结构体(struct)

# 5.5.1 字段定义与初始化(命名 vs 位置)

type User struct {
    ID   int
    Name string
    Age  int
}

// 命名初始化(强烈推荐)
u1 := User{ID: 1, Name: "Alice", Age: 20}

// 位置初始化(不推荐,字段顺序变了就 broken)
u2 := User{1, "Alice", 20}

// 部分初始化(其他字段取零值)
u3 := User{Name: "Bob"} // ID=0, Age=0

// 零值 struct
var u4 User // ID=0, Name="", Age=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

永远命名初始化——位置初始化在 struct 加新字段时所有调用点都要改。

# 5.5.2 嵌入字段(无字段名)

type Base struct {
    ID    int
    CTime time.Time
}

type User struct {
    Base       // 嵌入字段:没有显式字段名
    Name string
}

u := User{
    Base: Base{ID: 1, CTime: time.Now()},
    Name: "Alice",
}

fmt.Println(u.ID)     // 字段提升:直接访问 ID(不必 u.Base.ID)
fmt.Println(u.Base.ID) // 也可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

嵌入字段是 Go 实现"代码复用"的方式,不是继承——详见 第 9 章。

# 5.5.3 Tag:反射的元数据

type User struct {
    ID   int    `json:"id"           gorm:"primaryKey"`
    Name string `json:"name"         gorm:"size:64;not null"`
    Age  int    `json:"age,omitempty"`
}
1
2
3
4
5
  • Tag 是反引号包起来的字符串,紧跟在字段类型后
  • 多个 Tag 用空格分隔,惯例 key:"value" 格式
  • 反射通过 reflect.StructTag.Get("json") 取出

encoding/json、gorm、validator 等库严重依赖 Tag。常见的 json Tag 选项:

Tag 含义
json:"name" JSON 里键名为 name
json:"-" 不参与序列化
json:",omitempty" 零值则忽略
json:"name,string" 序列化为字符串(即使是 int)

# 5.5.4 字段对齐与省内存技巧

// 16 字节
type Bad struct {
    a bool   // 1 字节 + 7 字节 padding
    b int64  // 8 字节
}

// 9 字节(实际占用根据对齐算 16 字节,但更紧凑)
type Good struct {
    b int64  // 8 字节
    a bool   // 1 字节 + 7 字节 padding
}

// 含 3 个 bool 时差异更明显
type Wasteful struct {  // 24 字节
    a bool;  b int64;  c bool
}
type Compact struct {   // 16 字节
    b int64;  a, c bool
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

规则:把大字段放前面、bool/byte 这类小字段聚在末尾。go vet -fieldalignment 工具会自动检测。

➡ 卷三第 3 章 结构体与对齐 详讲对齐规则。

# 5.5.5 == 可比较性规则

字段类型 struct 是否可比较
全部基础类型(int、string、bool) ✅
数组(元素类型可比较) ✅
嵌套 struct(字段全可比较) ✅
含指针字段 ✅(比的是指针值,不是指向的内容)
含接口字段 ✅(运行时比较底层类型 + 值,不可比时 panic)
含 slice / map / func 字段 ❌ 编译错
type A struct {
    Name string
    Age  int
}
a1 := A{"X", 1}
a2 := A{"X", 1}
fmt.Println(a1 == a2) // true ✅

type B struct {
    Tags []string
}
var b1, b2 B
fmt.Println(b1 == b2) // ❌ 编译错:struct containing []string cannot be compared
1
2
3
4
5
6
7
8
9
10
11
12
13

如何深比较含 slice 的 struct?用 reflect.DeepEqual(性能差)或自己写 Equal 方法。


# 5.6 字符串(string)的本质

# 5.6.1 不可变只读字节序列

string 在 Go 里是只读字节切片,底层结构与 slice 类似但少了 cap:

string struct (16 字节):
┌─────────┬─────┐
│  ptr    │ len │
└─────────┴─────┘
   ↓
[只读 UTF-8 字节] ...
1
2
3
4
5
6

关键事实:

s := "hello, 世界"
fmt.Println(len(s))       // 13(字节数,不是字符数)
fmt.Println(s[0])         // 104('h' 的字节,类型是 byte)
fmt.Println(string(s[0])) // "h"

// 中文每字符 3 字节(UTF-8)
fmt.Println(s[7])         // 228("世"的第一字节,单看没意义)

// 正确遍历字符(rune)
for i, r := range s {
    fmt.Printf("byte %d: %c (%d)\n", i, r, r)
}
// byte 0: h (104)
// byte 1: e (101)
// ...
// byte 7: 世 (19990)
// byte 10: 界 (30028)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

记住:s[i] 是 byte(字节),for i, r := range s 给的 r 才是 rune(Unicode 码点)。

字符串拼接最好用 strings.Builder 而不是 +:

// ❌ 每次 + 都分配新字符串
result := ""
for _, p := range parts {
    result += p // O(n²) 内存分配
}

// ✅ 预分配 + 一次性拼接
var b strings.Builder
b.Grow(estimateSize)
for _, p := range parts {
    b.WriteString(p)
}
result := b.String()
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.6.2 字符串与切片互转的零拷贝优化(Go 1.20+)

// 标准转换(有内存拷贝)
b := []byte(s)
s2 := string(b)
1
2
3

string([]byte) 与 []byte(string) 默认都会复制底层数据——因为 string 必须保证不可变,而 []byte 可变。

Go 1.20+ 引入零拷贝 API(仅在编译器能证明安全时使用):

import "unsafe"

// Go 1.20+:[]byte 转 string 零拷贝
b := []byte("hello")
s := unsafe.String(&b[0], len(b))

// string 转 []byte 零拷贝
data := unsafe.Slice(unsafe.StringData(s), len(s))
1
2
3
4
5
6
7
8

⚠️ 使用前提:转换后承诺不会修改 byte 数据——一旦修改,破坏 string 不可变性,行为未定义。仅在性能热点用。

➡ 卷三第 4 章 字符串与切片底层 详讲。


# 5.7 综合示例:构建一个 LRU 缓存

把本章的 slice / map / struct 串起来,写一个 100 行的 LRU 缓存——它是面试高频题,也是 Redis 内部用的算法之一。

// lrucache/cache.go
package lrucache

// node 双向链表节点
type node struct {
    key        string
    value      any
    prev, next *node
}

// LRU 是基于 map + 双向链表的最近最少使用缓存
type LRU struct {
    cap        int
    items      map[string]*node
    head, tail *node // head 是最近用、tail 是最久未用
}

// New 创建一个容量为 cap 的 LRU
func New(cap int) *LRU {
    if cap <= 0 {
        panic("lrucache: cap must be > 0")
    }
    head := &node{}
    tail := &node{}
    head.next = tail
    tail.prev = head
    return &LRU{
        cap:   cap,
        items: make(map[string]*node, cap),
        head:  head,
        tail:  tail,
    }
}

// Get 取值,若存在则提升到最近使用
func (c *LRU) Get(key string) (any, bool) {
    n, ok := c.items[key]
    if !ok {
        return nil, false
    }
    c.moveToFront(n)
    return n.value, true
}

// Put 写入值,超容则淘汰最久未用
func (c *LRU) Put(key string, value any) {
    if n, ok := c.items[key]; ok {
        n.value = value
        c.moveToFront(n)
        return
    }
    n := &node{key: key, value: value}
    c.items[key] = n
    c.addToFront(n)
    if len(c.items) > c.cap {
        evict := c.tail.prev
        c.remove(evict)
        delete(c.items, evict.key)
    }
}

// Len 当前条目数
func (c *LRU) Len() int { return len(c.items) }

// --- 内部链表操作 ---

func (c *LRU) addToFront(n *node) {
    n.prev = c.head
    n.next = c.head.next
    c.head.next.prev = n
    c.head.next = n
}

func (c *LRU) remove(n *node) {
    n.prev.next = n.next
    n.next.prev = n.prev
    n.prev, n.next = nil, nil
}

func (c *LRU) moveToFront(n *node) {
    c.remove(n)
    c.addToFront(n)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

测试:

// lrucache/cache_test.go
package lrucache

import "testing"

func TestLRU(t *testing.T) {
    c := New(2)
    c.Put("a", 1)
    c.Put("b", 2)

    if v, ok := c.Get("a"); !ok || v != 1 {
        t.Fatalf("expected a=1, got %v ok=%v", v, ok)
    }

    c.Put("c", 3) // 此时 b 是最久未用,应被淘汰

    if _, ok := c.Get("b"); ok {
        t.Fatal("b should be evicted")
    }

    if v, _ := c.Get("c"); v != 3 {
        t.Fatalf("expected c=3, got %v", v)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

跑测试:

$ go test -v ./...
=== RUN   TestLRU
--- PASS: TestLRU (0.00s)
PASS
1
2
3
4

用到的本章知识点:

知识点 在哪一行
struct 定义 type node struct { ... }
指针字段 prev, next *node
map items map[string]*node
make(map[K]V, hint) make(map[string]*node, cap)
双返回值 n, ok := c.items[key]
delete(map, key) delete(c.items, evict.key)
多个返回值赋值 n.prev, n.next = nil, nil
空接口 any value any

# 5.8 本章底层原理(简介)

类型 内部结构 详解卷三
slice runtime.slice {ptr, len, cap} 第 4 章
map runtime.hmap + bmap 桶数组 第 10 章
string runtime.stringHeader {ptr, len} 第 4 章
struct 按字段顺序连续布局 + padding 第 3 章
array 固定长度连续内存 第 1 章

# 5.9 Go 新手陷阱 Top 5

# ❌ 陷阱 1:函数内 append(s, x) 不返回赋值

func add(s []int, v int) {
    s = append(s, v) // ❌ 调用方看不到变化(如果发生扩容)
}

s := []int{1, 2}
add(s, 3)
fmt.Println(s) // [1 2],不是 [1 2 3]
1
2
3
4
5
6
7

根因:slice 是值(三元组),传入函数是复制;扩容后内部 ptr 指向新数组,外部依然指向旧数组。

修复:要么返回新 slice,要么传 *[]int:

func add(s []int, v int) []int { return append(s, v) }

s = add(s, 3) // ✅
1
2
3

# ❌ 陷阱 2:用子切片 s[1:3] 后修改它,原 slice 也变

详见 §5.3.4 / §5.3.5。返回切片给外部时一律用三索引 s[i:j:k] 或 slices.Clone。

# ❌ 陷阱 3:给 nil map 赋值 panic

type Cache struct {
    data map[string]int // 忘了在构造函数里 make
}
c := &Cache{}
c.data["x"] = 1 // ❌ panic
1
2
3
4
5

修复:永远在构造函数里 make,或用 sync.Map(但有性能代价)。

# ❌ 陷阱 4:循环里 &v 共享地址(Go 1.21 及之前)

// Go 1.21 及之前
ps := []*int{}
for _, v := range []int{1, 2, 3} {
    ps = append(ps, &v) // 所有元素指向同一个 v
}
fmt.Println(*ps[0], *ps[1], *ps[2]) // 3 3 3
1
2
3
4
5
6

Go 1.22 起循环变量每轮新建,此问题消失。但老代码、老服务还在的话,仍然要懂这个坑。修复(兼容老版本):

for _, v := range []int{1, 2, 3} {
    v := v // 显式 shadow,构造每轮独立变量
    ps = append(ps, &v)
}
1
2
3
4

# ❌ 陷阱 5:含 slice/map/func 字段的 struct 用 == 比较

type Conf struct {
    Tags []string
}

var a, b Conf
fmt.Println(a == b) // ❌ 编译错
1
2
3
4
5
6

修复:

  • 用 reflect.DeepEqual(a, b)(性能差)
  • 自己写 func (c Conf) Equal(o Conf) bool
  • 用 slices.Equal(a.Tags, b.Tags) && ... 字段级比较(Go 1.21+)

# 5.10 思考题

  1. 为什么 [5]int 与 [6]int 是两个不同类型?这种"长度即类型"的设计带来什么好处?
  2. 写一段代码验证:make([]int, 0, 100) 和 make([]int, 100) 在内存占用与 len(s) 上有什么差别?
  3. 一个 slice s 已经 cap=100, len=50,再 s = append(s, x) 会扩容吗?什么情况下会?什么情况下不会?
  4. 为什么 Go 选择让 delete(m, "noexist") 静默成功,而 m[nilkey] = 1(nil map 写入)panic?这种"读宽松、写严格"哲学是否一致?
  5. 写一个函数 Unique(s []int) []int,去重并保持原顺序。提示:用 map[int]struct{} 做"集合"。
  6. 把 5.7 的 LRU 改造为支持 TTL(每个条目可指定过期时间)。提示:每个 node 加 expireAt time.Time,Get 时若过期则删掉。
  7. 一个 1000 字节的 struct 数组 []big,遍历时 for _, b := range arr 与 for i := range arr; b := &arr[i] 性能差距多大?为什么?

# 5.11 推荐阅读

# 卷内

  • 卷一第 7 章 函数(slice 作为参数)
  • 卷一第 9 章 结构体与方法(struct 接收者)
  • 卷一第 14 章 同步 sync 包(map 并发安全)
  • 卷一第 16 章 标准库与泛型(slices / maps 包)

# 跨卷

  • 卷三第 3 章 结构体与对齐
  • 卷三第 4 章 字符串与切片底层
  • 卷三第 10 章 map 与哈希实现
  • 卷四第 9 章 性能优化实战(slice 预分配)

# 外部资料

  • Go Slices: usage and internals (opens new window)
  • Go maps in action (opens new window)
  • Strings, bytes, runes and characters in Go (opens new window)
上次更新: 2026/06/10, 11:13:41
运算符
流程语句

← 运算符 流程语句→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式