Go Map 与结构体

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

切片管顺序,map 管查找,结构体管建模——这三个是 Go 里最常用的组合。这一篇讲 map 与结构体,以及它们各自的边界。

map 的声明与使用

ages := map[string]int{
    "ann": 30,
    "bob": 25,
}

ages["cindy"] = 28       // 写入
fmt.Println(ages["ann"]) // 30

用 make 声明空 map:

m := make(map[string]int)   // 常用,可立即写入
var m2 map[string]int       // nil map,只能读,写入会 panic
m2["a"] = 1   // panic: assignment to entry in nil map

nil map 可以读、可以 len、可以 range,只是不能写。函数返回值是 map 时,调用方要确认它已初始化。

comma-ok:区分「不存在」与「值为零」

v := ages["dave"]        // 0,无法区分"没有这个人"和"年龄是 0"
v, ok := ages["dave"]    // 0, false

需要区分时必须用 comma-ok 形式。同样的写法也用于类型断言和 channel 接收。

判断键是否存在:

if _, ok := ages["ann"]; ok {
    fmt.Println("存在")
}

go vet 会提示「只用 ok 不用值时改用 _, ok :=」。

删除与遍历

delete(ages, "bob")     // 删除不存在的键不会报错
len(ages)               // 元素个数

遍历顺序随机(Go 有意设计),需要有序结果先排序键:

keys := make([]string, 0, len(ages))
for k := range ages {
    keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
    fmt.Println(k, ages[k])
}

遍历时删除元素是安全的(Go 规范允许),但新增元素的行为不确定,不要在遍历中写入。

map 是引用语义

func addAge(m map[string]int, name string) {
    m[name] = 40        // 修改会影响调用方
}

ages := map[string]int{}
addAge(ages, "ann")
fmt.Println(ages["ann"])   // 40

map 变量本身是指向内部结构的指针,传参不复制内容。但 map 不是并发安全的:多个 goroutine 同时写会直接 panic(concurrent map writes)。并发场景用 sync.Mutex 或 sync.Map。

另一个限制:不能取 map 元素的地址,因为元素可能在扩容时被移动:

p := &ages["ann"]   // 编译错误:cannot take address of ages["ann"]

需要存指针时,map 的值直接放指针类型:map[string]*User。

结构体定义

type User struct {
    ID    int
    Name  string
    Email string
    Tags  []string
}

创建与赋值:

u := User{ID: 1, Name: "ann"}          // 具名字段,推荐
u2 := User{1, "bob", "b@x.com", nil}   // 按位置,字段一改就编译失败,不推荐

u.Name = "anna"
fmt.Println(u.ID, u.Name)              // 1 anna

指针与结构体:

p := &u
p.Name = "cindy"          // 自动解引用,等价于 (*p).Name = ...
fmt.Println(u.Name)       // cindy

结构体是值类型:赋值和传参都会完整复制。

a := User{Name: "ann", Tags: []string{"go"}}
b := a
b.Name = "bob"
fmt.Println(a.Name)       // ann,Name 没变

b.Tags[0] = "rust"
fmt.Println(a.Tags[0])    // rust,切片共享底层数组

这就是「结构体是浅复制」的含义:字段本身复制,字段指向的数据共享。

字段导出规则

首字母大写 = 导出(可被其他包访问),小写 = 包内私有。这条规则同时决定了 JSON 序列化、反射可见性:

type Article struct {
    Title   string `json:"title"`        // 导出,参与 JSON
    Content string `json:"content"`
    views   int    `json:"-"`            // 未导出,JSON 也不会输出
}

结构体标签(反引号里的 json:"...")是给 encoding/json、yaml 等库读的元数据。

嵌入:组合而非继承

type Profile struct {
    Bio string
}

func (p Profile) Describe() string {
    return "bio: " + p.Bio
}

type Account struct {
    Profile          // 嵌入,字段名就是类型名
    Username string
}

a := Account{Profile: Profile{Bio: "gopher"}, Username: "ann"}
fmt.Println(a.Bio)          // 提升字段,等价于 a.Profile.Bio
fmt.Println(a.Describe())   // 提升方法

嵌入是组合:Account 拥有一个 Profile,方法被「提升」到外层,但不存在虚函数与重写。外层定义同名方法会遮蔽内层,而不是多态。

常见错误与注意事项

  1. 写 nil map:var m map[string]int 后直接赋值会 panic,用 make。
  2. 用 m[k] 判断键是否存在:值恰好是零值时误判,用 comma-ok。
  3. 依赖 map 遍历顺序:顺序随机,需要排序键。
  4. 并发读写 map:会 panic,加锁或用 sync.Map。
  5. 取 map 元素地址:编译错误,值改成指针类型。
  6. 按位置初始化结构体:新增字段即编译失败,一律用具名字段。
  7. 以为结构体赋值是深拷贝:slice / map 字段仍共享。

小结

  • map 用 make 初始化;取值判断存在用 v, ok := m[k]。
  • 遍历顺序随机,需要顺序就排序键;删除用 delete。
  • map 是引用语义且非并发安全,也不能取元素地址。
  • 结构体是值类型,赋值浅复制;首字母大写才导出。
  • 嵌入实现组合与方法提升,不是继承与重写。

下一步