学 Go 的时候我最容易踩坑的一块就是结构体:看起来就"一个数据集合",结果一写项目发现------初始化方式一多就乱、方法接收者选错就改不了值、组合(嵌套)和"字段提升"又容易名冲突、Tag 还经常写了但解析不到......这篇就按我的节奏,把结构体真正用顺。
[1. 结构体是什么:Go 的"类"替代品](#1. 结构体是什么:Go 的“类”替代品)
[2. 定义与字段可见性(封装的核心)](#2. 定义与字段可见性(封装的核心))
[2.1 基本定义](#2.1 基本定义)
[2.2 字段命名决定"能不能被外部包访问"](#2.2 字段命名决定“能不能被外部包访问”)
[2.3 结构体零值](#2.3 结构体零值)
[3. 4 种初始化方式:怎么选不踩坑](#3. 4 种初始化方式:怎么选不踩坑)
[3.1 键值对初始化(最推荐)](#3.1 键值对初始化(最推荐))
[3.2 顺序初始化(我一般不用)](#3.2 顺序初始化(我一般不用))
[3.3 new(T) 初始化(返回指针)](#3.3 new(T) 初始化(返回指针))
[3.4 &T{} 初始化(最常见的"指针创建")](#3.4 &T{} 初始化(最常见的“指针创建”))
[4. 字段访问:值 vs 指针(语法糖别误会)](#4. 字段访问:值 vs 指针(语法糖别误会))
[4.1 值类型访问](#4.1 值类型访问)
[4.2 指针访问:Go 的语法糖很舒服](#4.2 指针访问:Go 的语法糖很舒服)
[5. 方法(Method):值接收者 vs 指针接收者(重点)](#5. 方法(Method):值接收者 vs 指针接收者(重点))
[5.1 核心区别(建议直接背表)](#5.1 核心区别(建议直接背表))
[5.2 一个"踩坑级"对比](#5.2 一个“踩坑级”对比)
[6. 组合/嵌套:Go 的"继承"替代方案](#6. 组合/嵌套:Go 的“继承”替代方案)
[6.1 具名嵌套(更清晰)](#6.1 具名嵌套(更清晰))
[6.2 匿名嵌套(字段/方法提升,像"继承")](#6.2 匿名嵌套(字段/方法提升,像“继承”))
[6.3 名冲突怎么处理](#6.3 名冲突怎么处理)
[7. 结构体 Tag:JSON/DB 映射的关键](#7. 结构体 Tag:JSON/DB 映射的关键)
[7.1 JSON 序列化示例](#7.1 JSON 序列化示例)
[8. 可比较性:能不能用 ==?能不能当 Map Key?](#8. 可比较性:能不能用 ==?能不能当 Map Key?)
[9. 内存布局与对齐:字段顺序能省内存(实用)](#9. 内存布局与对齐:字段顺序能省内存(实用))
[10. Tips & 最佳实践(写项目真的省事)](#10. Tips & 最佳实践(写项目真的省事))
[11. 总结](#11. 总结)
1. 结构体是什么:Go 的"类"替代品
Go 没有 class,但结构体是 Go 唯一的自定义复合类型。我们通常用:
-
字段封装数据
-
方法封装行为
-
组合(嵌套)做复用和"类似继承"的效果
这也是 Go 做 OOP 的主路线。
2. 定义与字段可见性(封装的核心)
2.1 基本定义
type Person struct {
Name string
Age int
}
2.2 字段命名决定"能不能被外部包访问"
这点真的太重要了,很多库就是靠这个做封装:
type User struct {
ID int // 导出字段:包外可访问(Public)
name string // 未导出字段:仅包内可访问(Private)
}
一句话记忆:首字母大写=对外暴露,小写=包内私有。
2.3 结构体零值
var p Person // Name="" Age=0
零值规律:
-
数值:0
-
string:
"" -
bool:false
-
指针/slice/map/chan:nil
3. 4 种初始化方式:怎么选不踩坑
我按推荐程度排个序。
| 初始化方式 | 写法 | 推荐度 | 适用场景 |
|---|---|---|---|
| 键值对初始化 | Person{Name:"A", Age:1} |
5 | 最常用,字段多也不怕 |
&T{} 指针字面量 |
&Person{Name:"A"} |
5 | 需要指针、避免拷贝 |
new(T) |
new(Person) |
3 | 想要零值对象指针 |
| 顺序初始化 | Person{"A",1} |
1 | 字段少且稳定,否则易翻车 |
3.1 键值对初始化(最推荐)
p1 := Person{
Name: "Alice",
Age: 25,
}
p2 := Person{
Name: "Bob", // Age 自动是 0
}
3.2 顺序初始化(我一般不用)
p := Person{"Charlie", 30}
坑点:字段顺序一改,全项目都要跟着改;字段一多更容易写错。
3.3 new(T) 初始化(返回指针)
p := new(Person) // *Person,字段全是零值
p.Name = "David"
p.Age = 28
3.4 &T{} 初始化(最常见的"指针创建")
p := &Person{
Name: "Eve",
Age: 22,
}
4. 字段访问:值 vs 指针(语法糖别误会)
4.1 值类型访问
var p Person
p.Name = "Frank"
fmt.Println(p.Name)
4.2 指针访问:Go 的语法糖很舒服
p := &Person{Name: "Grace", Age: 26}
p.Name = "Grace Liu" // 等价于 (*p).Name = ...
fmt.Println(p.Age)
5. 方法(Method):值接收者 vs 指针接收者(重点)
Go 方法就是"带接收者的函数"。
func (p Person) SayHello() {}
func (p *Person) Birthday() {}
5.1 核心区别(建议直接背表)
| 特性 | 值接收者 (p Person) |
指针接收者 (p *Person) |
|---|---|---|
| 传递方式 | 拷贝整个结构体 | 拷贝指针地址 |
| 能否修改原对象 | ❌ 不行(改副本) | ✅ 可以(改原对象) |
| 性能(大结构体) | 开销大 | 开销小 |
| 调用体验 | 值/指针都能调 | 值/指针都能调(自动取地址) |
5.2 一个"踩坑级"对比
type Person struct {
Name string
Age int
}
func (p Person) WrongBirthday() {
p.Age++ // 改的是副本
}
func (p *Person) RightBirthday() {
p.Age++ // 改的是原对象
}
func main() {
p := Person{Name: "Henry", Age: 30}
p.WrongBirthday()
fmt.Println(p.Age) // 30
p.RightBirthday()
fmt.Println(p.Age) // 31
}
结论
-
需要修改结构体:用指针接收者
-
结构体很大:也优先指针接收者
-
小结构体且只读:值接收者更"安全"
6. 组合/嵌套:Go 的"继承"替代方案
Go 不搞继承,我们用组合。
6.1 具名嵌套(更清晰)
type Address struct {
Province string
City string
}
type User struct {
Name string
Age int
Address Address
}
u := User{Address: Address{City: "Shenzhen"}}
fmt.Println(u.Address.City)
6.2 匿名嵌套
type User struct {
Name string
Age int
Address // 直接写类型名
}
u := User{Address: Address{City: "Hangzhou"}}
fmt.Println(u.City) // 被提升了
6.3 名冲突怎么处理
外层同名字段会"盖住"内层的,想访问内层就显式写出来:
type A struct{ Name string }
type B struct {
A
Name string
}
b := B{A: A{Name:"Inner"}, Name:"Outer"}
fmt.Println(b.Name) // Outer
fmt.Println(b.A.Name) // Inner
7. 结构体 Tag:JSON/DB 映射的关键
Tag 是写在字段后面的"元数据",常用于 JSON、数据库映射、校验等。
type User struct {
ID int `json:"user_id" db:"id"`
Name string `json:"user_name"`
Age int `json:"-"` // 忽略
}
7.1 JSON 序列化示例
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"user_id"`
Name string `json:"user_name"`
Age int `json:"-"`
}
func main() {
u := User{ID: 1, Name: "Kate", Age: 25}
data, _ := json.Marshal(u)
fmt.Println(string(data)) // {"user_id":1,"user_name":"Kate"}
}
Tag 常见坑(我踩过)
-
反引号必须是 `(不是引号)
-
格式必须
key:"value",引号必须英文双引号 -
多个 tag 用空格分隔
8. 可比较性:能不能用 ==?能不能当 Map Key?
规则很简单:结构体能不能比较,取决于它所有字段能不能比较。
type Comparable struct {
A int
B string
}
type Uncomparable struct {
A int
B []int // slice 不可比较
}
-
Comparable可以用== -
Uncomparable不能用==,也 不能作为 map 的 key
9. 内存布局与对齐:字段顺序能省内存(实用)
Go 会做内存对齐(padding),字段顺序不合理会浪费空间。
type BadOrder struct {
A bool
B int64
C bool
} // 可能更大
type GoodOrder struct {
A bool
C bool
B int64
} // 可能更小
想看真实大小可以用:
import "unsafe"
fmt.Println(unsafe.Sizeof(BadOrder{}))
fmt.Println(unsafe.Sizeof(GoodOrder{}))
10. Tips & 最佳实践
-
结构体是值类型:赋值/传参会拷贝,大结构体优先传指针。
-
方法接收者怎么选:
-
要修改对象 → 指针接收者
-
结构体大 → 指针接收者
-
小结构体只读 → 值接收者更稳
-
-
组合优先于"模拟继承":具名嵌套更清晰,匿名嵌套更方便但冲突风险更高。
-
Tag 写规范:反引号 +
key:"value"+ 英文双引号,别用中文引号。
11. 总结
结构体在 Go 里就是"数据 + 行为":
-
用字段组织数据
-
用方法定义行为(值/指针接收者决定是否能改原对象与性能)
-
用组合(嵌套)实现复用与"类似继承"
-
用 Tag 做序列化/映射
-
通过字段顺序优化内存布局