Go Map 与结构体
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
切片管顺序,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,方法被「提升」到外层,但不存在虚函数与重写。外层定义同名方法会遮蔽内层,而不是多态。
常见错误与注意事项
- 写 nil map:
var m map[string]int后直接赋值会 panic,用make。 - 用
m[k]判断键是否存在:值恰好是零值时误判,用 comma-ok。 - 依赖 map 遍历顺序:顺序随机,需要排序键。
- 并发读写 map:会 panic,加锁或用
sync.Map。 - 取 map 元素地址:编译错误,值改成指针类型。
- 按位置初始化结构体:新增字段即编译失败,一律用具名字段。
- 以为结构体赋值是深拷贝:slice / map 字段仍共享。
小结
- map 用
make初始化;取值判断存在用v, ok := m[k]。 - 遍历顺序随机,需要顺序就排序键;删除用
delete。 - map 是引用语义且非并发安全,也不能取元素地址。
- 结构体是值类型,赋值浅复制;首字母大写才导出。
- 嵌入实现组合与方法提升,不是继承与重写。