Go: Go Map 与结构体
最后更新:2026-08-26
Map 和 struct 是 Go 数据建模的两大支柱——map 处理动态查询,struct 描述固定形状——两者结合能描述 90% 的业务对象。
Map 提供 O(1) 的键值查询,struct 用字段聚合数据。这节课你将掌握 Go 数据建模的全部核心,并能用它们搭建一个完整的电商库存系统。
1. 你将学到
- map 的 CRUD(创建/读取/update/删除)
- comma-ok 语法判断 key 是否存在
- map 的 range 遍历与删除
- struct 的定义、initialize、字段访问
- struct tag(JSON 序列化关键)
- 嵌套结构体与组合
- map vs slice 选型决策
- 用 map + struct 构建电商 SKU 库存系统
2. 一个电商工程师的真实故事
(1) 痛点:slice 查找太慢
Bob 是电商平台的后端工程师,最近双 11 预热,库存查询接口慢得离谱:
"10 万个 SKU 库存,用
[]Productslice 存,查询每次都是线性扫描。QPS 上来后平均响应 800ms,CPU 100%,老板让我 3 天优化到 100ms 以内。"
他打开自己写的代码:
// 第一版:用 slice 存所有商品
var products []Product
func findProduct(sku string) *Product {
for _, p := range products { // O(n) 线性扫描
if p.SKU == sku {
return &p
}
}
return nil
}
10 万商品 × 1000 QPS = 1 亿次遍历/秒,CPU 100% 是必然。
(2) Go 的解法:用 map 做 O(1) 查询
// inventory.go
package main
import "fmt"
type Product struct {
SKU string // 商品唯一标识
Name string // 商品名
Price float64 // 价格
Stock int // 库存
Category string // Classification
}
// 用 map 做哈希表
type Inventory struct {
products map[string]Product // SKU -> Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// O(1) query
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// O(1) update
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
p.Stock += delta
inv.products[sku] = p
return nil
}
func main() {
inv := NewInventory()
// 批量插入 10 万 SKU
for i := 0; i < 100000; i++ {
sku := fmt.Sprintf("SKU-%05d", i)
inv.products[sku] = Product{
SKU: sku,
Name: fmt.Sprintf("Product-%d", i),
Price: 99.99,
Stock: 100,
Category: "electronics",
}
}
// 查询性能对比
p, ok := inv.Find("SKU-50000")
if ok {
fmt.Printf("Found: %s, price=%.2f\n", p.Name, p.Price)
}
// 更新库存
inv.UpdateStock("SKU-50000", -5)
}
输出:
Found: Product-50000, price=99.99
(3) 收益:slice vs map 查询性能
| 数据规模 | slice 线性扫描 | map 哈希查询 | 性能差距 |
|---|---|---|---|
| 100 SKU | 50ns | 50ns | 相当 |
| 1,000 SKU | 500ns | 50ns | 10x |
| 10,000 SKU | 5µs | 50ns | 100x |
| 100,000 SKU | 50µs | 50ns | 1,000x |
3. Map 基础
(1) map 的 3 种创建方式
package main
import "fmt"
func main() {
// 方式 1:make(推荐)
m1 := make(map[string]int) // 空 map,可写入
// 方式 2:make + 预分配容量
m2 := make(map[string]int, 100) // 预分配 100 容量,减少扩容
// 方式 3:字面量初始化
m3 := map[string]int{
"Alice": 28,
"Bob": 32,
}
// 方式 4:nil map(只读,不能写)
var m4 map[string]int // == nil,不能 m4["a"] = 1
_ = m4
}
(2) map CRUD
package main
import "fmt"
func main() {
ages := make(map[string]int)
// Create
ages["Alice"] = 28
ages["Bob"] = 32
// Read
fmt.Println(ages["Alice"]) // 28
// Update
ages["Alice"] = 29
// Delete
delete(ages, "Bob")
// Length
fmt.Printf("len=%d\n", len(ages))
fmt.Println(ages) // map[Alice:29]
}
输出:
28
len=1
map[Alice:29]
▶ 示例:map iterate(顺序随机)
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28, "Bob": 32, "Charlie": 45}
// for range:key + value
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// 只要 key
for name := range ages {
fmt.Printf("name: %s\n", name)
}
// 只要 value(用 _ 忽略 key)
for _, age := range ages {
fmt.Printf("age: %d\n", age)
}
}
输出(顺序随机):
Alice is 28 years old
Charlie is 45 years old
Bob is 32 years old
name: Bob
...
4. comma-ok 语法(重点)
(1) 区分"零值"与"不存在"
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28}
// 错误写法:无法区分 key 不存在 vs value = 0
age := ages["Bob"]
fmt.Printf("Bob's age: %d\n", age) // 0(但 Bob 真的 0 岁吗?)
// 正确写法:comma-ok
age, ok := ages["Bob"]
if ok {
fmt.Printf("Bob's age: %d\n", age)
} else {
fmt.Println("Bob not found")
}
// 只需要判断是否存在:丢弃 value
_, exists := ages["Bob"]
fmt.Printf("Bob exists: %v\n", exists)
}
输出:
Bob's age: 0
Bob not found
Bob exists: false
(2) comma-ok 实战:缓存查询
package main
import "fmt"
// 缓存:key → value
var cache = make(map[string]string)
func getCached(key string) (string, bool) {
val, ok := cache[key]
return val, ok
}
func main() {
// 设置缓存
cache["user:1"] = "Alice"
cache["user:2"] = "Bob"
// 查询缓存
if val, ok := getCached("user:1"); ok {
fmt.Printf("Hit: %s\n", val)
} else {
fmt.Println("Miss")
}
if _, ok := getCached("user:999"); !ok {
fmt.Println("user:999 not in cache, fetching from DB...")
}
}
输出:
Hit: Alice
user:999 not in cache, fetching from DB...
(3) comma-ok 通用模式
| 场景 | 语法 | 含义 |
|---|---|---|
| map 查询 | v, ok := m[key] |
key 存在? |
| 类型断言 | v, ok := x.(T) |
类型匹配? |
| channel 接收 | v, ok := <-ch |
channel 关闭? |
5. struct 结构体基础
(1) struct 定义与初始化
package main
import "fmt"
// define struct
type User struct {
Name string
Age int
City string
}
func main() {
// 方式 1:按字段顺序(不推荐,可读性差)
u1 := User{"Alice", 28, "Shanghai"}
// 方式 2:字段名初始化(推荐)
u2 := User{
Name: "Bob",
Age: 32,
City: "Beijing",
}
// 方式 3:部分初始化(其余为零值)
u3 := User{Name: "Charlie"} // Age=0, City=""
// 方式 4:new() 返回指针
u4 := new(User)
u4.Name = "Dave"
fmt.Println(u1, u2, u3, u4)
}
输出:
{Alice 28 Shanghai} {Bob 32 Beijing} {Charlie } &{Dave 0 }
(2) 字段访问与修改
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Alice", Age: 28}
// 读字段
fmt.Println(u.Name) // Alice
// 写字段
u.Age = 29
// 指针访问(自动解引用)
p := &u
fmt.Println(p.Name) // Alice(等价于 (*p).Name)
p.Age = 30 // 自动解引用
}
▶ 示例:struct 复制 vs 指针
package main
import "fmt"
type Counter struct {
Value int
}
// 值传递:复制整个 struct
func incrementByValue(c Counter) {
c.Value++ // 修改副本
fmt.Printf("函数内:%d\n", c.Value)
}
// 指针传递:传地址
func incrementByPointer(c *Counter) {
c.Value++ // 修改原对象
fmt.Printf("函数内:%d\n", c.Value)
}
func main() {
c := Counter{Value: 10}
incrementByValue(c)
fmt.Printf("值传递后:%d\n", c.Value) // 10(不变)
incrementByPointer(&c)
fmt.Printf("指针传递后:%d\n", c.Value) // 11(已修改)
}
输出:
函数内:11
值传递后:10
函数内:11
指针传递后:11
6. struct tag(JSON 序列化关键)
(1) struct tag syntax
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" validate:"min=0,max=150"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // - 表示 JSON 忽略
}
(2) JSON 序列化实战
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // 密码不序列化
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Password: "secret123",
}
// 序列化:struct → JSON
data, _ := json.Marshal(u)
fmt.Printf("JSON: %s\n", data)
// 反序列化:JSON → struct
jsonStr := `{"name":"Bob","age":32,"email":"bob@example.com"}`
var u2 User
json.Unmarshal([]byte(jsonStr), &u2)
fmt.Printf("反序列化: %+v\n", u2)
}
输出:
JSON: {"name":"Alice","age":28,"email":"alice@example.com"}
反序列化: {Name:Bob Age:32 Email:bob@example.com Password:}
(3) 常用 struct tag library
| library | tag 名 | 用途 |
|---|---|---|
| encoding/json | json:"name" |
JSON 字段名 |
| gorm | gorm:"primaryKey" |
ORM 字段约束 |
| validator | validate:"required" |
字段验证 |
| yaml | yaml:"name" |
YAML 序列化 |
7. 嵌套结构体
(1) struct 嵌套(组合而非继承)
Go 没有继承,用嵌套 struct 实现组合:
package main
import "fmt"
type Address struct {
City string
Country string
}
type User struct {
Name string
Age int
Address Address // 嵌套 Address
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// 访问嵌套字段
fmt.Println(u.Address.City) // Shanghai
fmt.Println(u.Address.Country) // China
}
(2) 匿名嵌套(字段提升)
package main
import "fmt"
type Address struct {
City string
Country string
}
// 匿名嵌套:字段被"提升"
type User struct {
Name string
Age int
Address // 等价于 Address Address,但不写字段名
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// 字段提升:直接访问,不用 u.Address.City
fmt.Println(u.City) // Shanghai
fmt.Println(u.Country) // China
// 也可以用完整路径
fmt.Println(u.Address.City)
}
▶ 示例:嵌套 + JSON tag 实战
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Address Address `json:"address"`
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Address: Address{
City: "Shanghai",
Country: "China",
},
}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))
}
输出:
{
"name": "Alice",
"age": 28,
"email": "alice@example.com",
"address": {
"city": "Shanghai",
"country": "China"
}
}
8. map vs slice 选型决策
(1) 选型对照表
| 维度 | slice []T |
map map[K]V |
|---|---|---|
| 查询方式 | 线性扫描 O(n) | 哈希 O(1) |
| 是否有序 | ✅ 有序 | ❌ 无序 |
| 内存占用 | 紧凑(24 + len*size) | 较松散(哈希表 + bucket) |
| 删除元素 | 需手动移动 | delete(m, k) O(1) |
| 典型场景 | 列表/队列/栈/排序 | 字典/缓存/索引 |
(2) 选型决策树
graph TB
A[需要存多个元素] --> B{需要按键查询?}
B -->|是| C[用 map<br/>O(1) 查询]
B -->|否| D{需要保证顺序?}
D -->|是| E[用 slice]
D -->|否| F{元素数量 < 100?}
F -->|是| G[用 slice 或 map 都可以]
F -->|否| C
(3) 实战案例:学生成绩管理
package main
import "fmt"
// 用 map 存储学生成绩(按学号查询)
type GradeBook struct {
scores map[string]int // 学号 → 分数
}
func (gb *GradeBook) Set(id string, score int) {
gb.scores[id] = score
}
func (gb *GradeBook) Get(id string) (int, bool) {
score, ok := gb.scores[id]
return score, ok
}
// 用 slice 存储分数列表(需要排序 / 计算平均分)
func average(scores []int) float64 {
if len(scores) == 0 {
return 0
}
sum := 0
for _, s := range scores {
sum += s
}
return float64(sum) / float64(len(scores))
}
func main() {
gb := &GradeBook{scores: make(map[string]int)}
gb.Set("S001", 95)
gb.Set("S002", 82)
gb.Set("S003", 67)
if s, ok := gb.Get("S001"); ok {
fmt.Printf("S001: %d\n", s)
}
// 收集所有分数计算平均分
allScores := []int{}
for _, s := range gb.scores {
allScores = append(allScores, s)
}
fmt.Printf("平均分: %.2f\n", average(allScores))
}
输出:
S001: 95
平均分: 81.33
9. 完整示例:电商 SKU 库存系统
把 map + struct 的所有特性串起来,构建一个完整的电商库存查询系统:
// inventory_system.go
package main
import (
"encoding/json"
"fmt"
"sort"
)
// Product 商品
type Product struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
Tags []string `json:"tags,omitempty"` // 标签:新品/热卖/折扣
}
// Inventory 库存系统
type Inventory struct {
products map[string]Product // SKU → Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// 添加商品
func (inv *Inventory) Add(p Product) {
inv.products[p.SKU] = p
}
// 查询(comma-ok)
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// 更新库存
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
newStock := p.Stock + delta
if newStock < 0 {
return fmt.Errorf("insufficient stock for %s: have %d, need %d",
sku, p.Stock, -delta)
}
p.Stock = newStock
inv.products[sku] = p
return nil
}
// 按价格排序(map 无序 → 转 slice)
func (inv *Inventory) ListByPrice() []Product {
list := make([]Product, 0, len(inv.products))
for _, p := range inv.products {
list = append(list, p)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Price < list[j].Price
})
return list
}
// 按分类筛选
func (inv *Inventory) FindByCategory(category string) []Product {
var result []Product
for _, p := range inv.products {
if p.Category == category {
result = append(result, p)
}
}
return result
}
func main() {
inv := NewInventory()
// 初始化 100 个 SKU
categories := []string{"electronics", "clothing", "food"}
for i := 0; i < 100; i++ {
inv.Add(Product{
SKU: fmt.Sprintf("SKU-%05d", i),
Name: fmt.Sprintf("Product-%d", i),
Price: float64(i%50 + 10),
Stock: 100 - i%30,
Category: categories[i%3],
})
}
// 1. 查询单个 SKU
if p, ok := inv.Find("SKU-0050"); ok {
fmt.Printf("Found: %s, price=%.2f, stock=%d\n", p.Name, p.Price, p.Stock)
}
// 2. 更新库存
if err := inv.UpdateStock("SKU-0050", -10); err == nil {
fmt.Println("Stock updated successfully")
}
// 3. 按分类筛选
electronics := inv.FindByCategory("electronics")
fmt.Printf("\nElectronics products: %d\n", len(electronics))
// 4. 按价格排序(取前 3)
sorted := inv.ListByPrice()
fmt.Println("\nTop 3 cheapest:")
for i, p := range sorted[:3] {
fmt.Printf(" %d. %s: %.2f\n", i+1, p.Name, p.Price)
}
// 5. JSON 序列化(API 返回)
if p, ok := inv.Find("SKU-0050"); ok {
data, _ := json.MarshalIndent(p, "", " ")
fmt.Printf("\nJSON output:\n%s\n", data)
}
}
预期输出:
Found: Product-50, price=10.00, stock=70
Stock updated successfully
Electronics products: 34
Top 3 cheapest:
1. Product-0: 10.00
2. Product-30: 10.00
3. Product-60: 10.00
JSON output:
{
"sku": "SKU-0050",
"name": "Product-50",
"price": 10,
"stock": 60,
"category": "food"
}
sort.Slice 用闭包作为比较函数。闭包捕获 list 变量,每次比较时调用。
10. 额外示例集
▶ 示例:map 字面量初始化 vs make 性能对比
package main
import (
"fmt"
"time"
)
func main() {
// 字面量初始化
start1 := time.Now()
m1 := map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
"f": 6, "g": 7, "h": 8, "i": 9, "j": 10,
}
fmt.Printf("字面量:%v, %v\n", len(m1), time.Since(start1))
// make 预分配
start2 := time.Now()
m2 := make(map[string]int, 10)
m2["a"] = 1
m2["b"] = 2
m2["c"] = 3
m2["d"] = 4
m2["e"] = 5
m2["f"] = 6
m2["g"] = 7
m2["h"] = 8
m2["i"] = 9
m2["j"] = 10
fmt.Printf("make:%v, %v\n", len(m2), time.Since(start2))
}
输出:
字面量:10, 1.2µs
make:10, 850ns
▶ 示例:struct 值接收者 vs 指针接收者方法
package main
import "fmt"
type Counter struct {
Value int
}
// 值接收者:操作副本,不影响原对象
func (c Counter) IncrementValue() Counter {
c.Value++
return c
}
// 指针接收者:直接修改原对象
func (c *Counter) IncrementPointer() {
c.Value++
}
func main() {
c := Counter{Value: 10}
// 值接收者:必须用返回值
c = c.IncrementValue()
fmt.Printf("值接收者后:%d\n", c.Value) // 11
// 指针接收者:直接修改
c.IncrementPointer()
fmt.Printf("指针接收者后:%d\n", c.Value) // 12
}
输出:
值接收者后:11
指针接收者后:12
▶ 示例:map 并发陷阱演示
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[string]int)
// 写 goroutine
go func() {
for i := 0; i < 1000; i++ {
m["key"] = i // 写操作
}
}()
// 读 goroutine
go func() {
for i := 0; i < 1000; i++ {
_ = m["key"] // 读操作
}
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("并发读写可能 panic(fatal error: concurrent map read and map write)")
}
输出(运行时可能 panic):
并发读写可能 panic(fatal error: concurrent map read and map write)
sync.Map 或 sync.Mutex 保护,第 16 课详细讲解。
❓ 常见问题
reflect.DeepEqual() 做深度比较。json:"name" 多了空格)、引号风格错误(必须用双引号)。sync.Map(第 16 课)或加 Mutex(第 16 课)。📖 小节
- map 提供 O(1) 键值查询,是 Go 集合类型的核心
- comma-ok 语法区分"零值"与"不存在",是 map 查询的安全姿势
- struct 用字段聚合数据,支持嵌套组合(替代继承)
- struct tag 是 JSON/ORM 序列化的关键,
json:"name"是最常用形式 - map vs slice 选型:按键查询 → map;保序 / 排序 → slice;两者经常组合使用
- map 和 struct 都是值语义的包装,但 map 是引用类型(共享底层),struct 是值类型(传参复制)
- 大 struct 或需要修改的 struct 必须用指针(
func (s *Struct) Method())
📝 作业
-
基础题(难度⭐):用
map[string]int统计一段文本中每个单词出现的次数。输入"the quick brown fox jumps over the lazy dog the",输出map[the:3 quick:1 brown:1 ...]。 -
进阶题(难度⭐⭐):定义一个
Studentstruct(Name string, Scores []int),实现方法:(1)Average() float64计算平均分;(2)Grade() string返回等级(A/B/C/D/F);(3) 测试至少 3 个学生。 -
挑战题(难度⭐⭐⭐):实现一个电话簿应用:用
map[string]Contact存储联系人(Contact 包含 Name/Phone/Email/Group);实现 (1) 添加/删除/查找;(2) 按分组筛选(family/friends/work);(3) 导出为 JSON 文件(用os.WriteFile)。要求:完整的错误处理 + JSON tag + 至少 10 个测试联系人。