第 4 章 · 数组、切片与 map
本章目标:掌握 slice 动态数组、append、copy 与容量 cap;熟练使用 map 增删查改;理解 slice 共享底层数组 的陷阱;在 toolkit-go 用 map 维护 SKU 库存、用 slice 表示订单行;完成商品库存 map 练习;对照 java-dev ch04 集合与 python-dev ch04 列表/字典。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 go-dev ch03;建议已学 java-dev ch04 数组与集合。
4.1 场景说明:库存与订单行
toolkit-go(~/learn-go/toolkit-go,模块 example.com/toolkit-go)模拟商城库存与购物车:
| 结构 | Go 类型 | toolkit-go 用途 |
|---|---|---|
| 固定测试数据 | 数组 [N]T | 基准样例、常量表 |
| 动态订单行 | []LineItem | 订单计算、日志聚合 |
| SKU → 库存 | map[string]int | 商品 slug 库存表 |
| slug 去重 | map[string]struct{} | 集合语义 |
toolkit-go/
├── internal/
│ ├── inventory/
│ │ └── stock.go # Stock 结构体 + 扣减
│ ├── model/
│ │ └── line.go # LineItem slice 操作
│ └── calc/
│ └── order.go # ch03 已有
└── cmd/toolkit/main.go
| Java | Go | Python |
|---|---|---|
int[] 定长 | [N]int 定长 | 无原生定长数组 |
ArrayList<T> | []T slice | list |
HashMap<K,V> | map[K]V | dict |
4.2 数组 vs 切片
// 数组:长度是类型一部分,值类型
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // 编译器推断长度
// 切片:动态视图,引用底层数组
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(len(s), cap(s)) // 长度 vs 容量
| 概念 | 说明 |
|---|---|
len(s) | 当前元素个数 |
cap(s) | 底层数组容量,超出则分配新数组并拷贝 |
s[low:high] | 切片表达式,共享底层 |
s[low:high:max] | 三索引切片,限制 cap |
共享底层陷阱(必考):
orig := []int{1, 2, 3, 4}
sub := orig[1:3] // [2, 3],cap 延伸到 orig 末尾
sub[0] = 99
// orig 变为 [1, 99, 3, 4]
安全拷贝:
dup := make([]int, len(orig))
copy(dup, orig)
// 或
dup2 := append([]int(nil), orig...)
4.3 append 与预分配
// internal/model/line.go
package model
type LineItem struct {
Slug string
Price int64 // 分
Quantity int
}
func AppendLine(lines []LineItem, item LineItem) []LineItem {
return append(lines, item)
}
// 性能友好:已知大小时预分配
func NewCart(capacity int) []LineItem {
return make([]LineItem, 0, capacity)
}
func TotalQuantity(lines []LineItem) int {
var n int
for _, ln := range lines {
n += ln.Quantity
}
return n
}
| 写法 | 适用 |
|---|---|
append(s, x) | 动态增长 |
make([]T, 0, cap) | 减少扩容次数 |
s = s[:0] | 复用 slice 清空(池化场景) |
append(s, s2...) | 合并两个 slice |
append 陷阱:若 append 触发扩容,可能不再共享原底层数组;未扩容则仍共享。
a := []int{1, 2, 3}
b := append(a, 4) // 可能共享或新数组,勿依赖未文档化行为
_ = b
4.4 map 基本操作
// internal/inventory/stock.go
package inventory
import "fmt"
type Stock struct {
items map[string]int // slug -> 库存
}
func NewStock() *Stock {
return &Stock{items: make(map[string]int)}
}
func (s *Stock) Set(slug string, qty int) {
s.items[slug] = qty
}
func (s *Stock) Get(slug string) (int, bool) {
qty, ok := s.items[slug]
return qty, ok
}
func (s *Stock) Delete(slug string) {
delete(s.items, slug)
}
func (s *Stock) All() map[string]int {
out := make(map[string]int, len(s.items))
for k, v := range s.items {
out[k] = v
}
return out
}
| 操作 | 语法 | 注意 |
|---|---|---|
| 创建 | make(map[K]V) | 未 make 的 map 不能写入 |
| 读取 | v, ok := m[k] | 区分零值与不存在 |
| 写入 | m[k] = v | — |
| 删除 | delete(m, k) | 安全,不存在也不 panic |
| 遍历 | for k, v := range m | 顺序随机 |
| 长度 | len(m) | — |
4.5 商品库存 map 练习:扣减与合并
// internal/inventory/stock.go(续)
func (s *Stock) Deduct(slug string, n int) error {
if n <= 0 {
return fmt.Errorf("invalid deduct amount: %d", n)
}
qty, ok := s.Get(slug)
if !ok {
return fmt.Errorf("sku not found: %s", slug)
}
if qty < n {
return fmt.Errorf("insufficient stock for %s: have %d need %d", slug, qty, n)
}
s.items[slug] = qty - n
return nil
}
func MergeStock(base, delta map[string]int) map[string]int {
out := make(map[string]int, len(base)+len(delta))
for k, v := range base {
out[k] = v
}
for k, v := range delta {
out[k] += v
}
return out
}
| 步骤 | 业务 | 函数 |
|---|---|---|
| 初始化库存 | 上架 100 件 | Set("go-book", 100) |
| 下单扣减 | 买 2 件 | Deduct("go-book", 2) |
| 库存不足 | 买 999 件 | 返回 error |
| 合并快照 | 增量补货 | MergeStock |
与 gin-web 中 Redis hash 缓存库存概念类似(ch09 并发安全会展开)。
4.6 slice + struct 订单行实战
func UniqueSlugs(lines []model.LineItem) []string {
seen := make(map[string]struct{})
out := make([]string, 0, len(lines))
for _, ln := range lines {
if _, ok := seen[ln.Slug]; ok {
continue
}
seen[ln.Slug] = struct{}{}
out = append(out, ln.Slug)
}
return out
}
func FilterPositiveQty(lines []model.LineItem) []model.LineItem {
out := make([]model.LineItem, 0, len(lines))
for _, ln := range lines {
if ln.Quantity > 0 {
out = append(out, ln)
}
}
return out
}
4.7 排序预告(ch14)
slice 可用 sort.Slice 排序(ch14 详讲):
import "sort"
// 预习:按 slug 字典序
sort.Slice(lines, func(i, j int) bool {
return lines[i].Slug < lines[j].Slug
})