Go 的 struct 与方法
用 struct 聚合数据、用方法附着行为:字段可见性、零值可用、值/指针接收者选择,以及与方法集、接口的衔接。
[!info] 关联笔记
Go 的 struct 与方法
这个概念为什么会出现
Go 没有 class/继承主线,但仍需要:
- 把相关字段聚合成一个值
- 把操作绑定到类型上,方便调用与接口抽象
struct 解决聚合;方法(带接收者的函数)解决行为附着。二者共同构成 Go 最常见的“面向对象风格”表达,却保持“就是数据 + 函数”的透明模型。
[!abstract] 一句话理解
struct是字段的复合值类型;方法是带接收者参数的函数。值接收者在副本上工作,指针接收者可共享并修改原对象——接口满足则看方法集。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:会员中心改昵称与过生日
用户资料用 Member 聚合:对外展示名 Name,对内年龄 age。
读展示名用值接收者(不改动原对象);
改昵称、过生日用指针接收者(必须改到原对象上)。
package main
import "fmt"
// Member 是会员资料聚合。
// 业务意图:公开昵称,隐藏年龄实现细节。
// 教学点:字段首字母大小写 = 是否导出。
type Member struct {
Name string
age int // 包内可见
}
// DisplayName 只读展示名。
// 教学点:值接收者拿到的是副本,适合不修改状态的查询。
func (m Member) DisplayName() string {
return m.Name
}
// Rename 修改昵称。
// 业务意图:用户在设置页提交新名字。
// 教学点:指针接收者才能写回原 Member,值接收者改的是副本。
func (m *Member) Rename(name string) {
m.Name = name
}
// CelebrateBirthday 年龄 +1。
// 业务意图:生日任务触发时更新内部年龄。
func (m *Member) CelebrateBirthday() {
m.age++
}
func main() {
// 字面量初始化:导出字段用 Name:,未导出 age 同包可写。
m := Member{Name: "Ann", age: 20}
fmt.Println(m.DisplayName()) // Ann
// 语法糖:m 是值,调用指针方法时编译器自动取址。
m.Rename("Bob")
m.CelebrateBirthday()
fmt.Println(m.Name, m.age) // Bob 21
}
建议运行:
go run .
期望输出:
Ann
Bob 21
结合场景再看三个关注点
-
struct 聚数据,方法挂行为
没有 class,但Member+ 方法已经够表达会员领域对象。 -
值接收者 vs 指针接收者
查询用值;改昵称/年龄用指针,否则状态丢在副本里。 -
调用处的自动取址
m.Rename在m可寻址时会变成(&m).Rename;接口赋值时方法集规则更严(见方法集笔记)。
核心概念
结构体类型
type Point struct {
X, Y int
}
- 字段名大写导出,小写包内
- 零值是字段零值的组合,常可直接使用
- 字面量:
Point{1, 2}或Point{X: 1, Y: 2} - 可比较当且仅当所有字段可比较
规范:Struct types
方法声明
func (r ReceiverType) MethodName(params) results { }
- 接收者可以是类型
T或*T(T不能是指针) - 方法必须声明在与类型同一包
- 语法糖:
u.Rename在需要时自动取址/解引用用于调用,但方法集规则更严
值接收者 vs 指针接收者
值接收者 (T) | 指针接收者 (*T) | |
|---|---|---|
| 收到什么 | 副本 | 指向原值的指针 |
| 修改原值 | 否(改副本) | 可以 |
| 大结构体 | 可能复制成本高 | 常更合适 |
| 接口 | 仅值方法进 T 的方法集 | 见方法集笔记 |
选择经验(非硬性):
- 需要改接收者 → 指针
- 接收者是大结构体/含锁 → 指针
- 小不变语义、类似内置值 → 值
- 同一类型方法集保持一致(通常整组都用指针或明确文档)
精确接口规则 → go-method-sets-and-receivers。
构造与零值
Go 无官方构造器关键字;惯用:
func NewUser(name string) *User {
return &User{Name: name}
}
零值可用是设计目标:var sb strings.Builder 即可用。
嵌入预告
匿名字段实现组合,见 go-embedding-and-composition。标签见 go-struct-tags。
设计动机
- 组合优于继承:数据平面简单,行为通过方法与接口扩展
- 显式:没有隐式 this 可变性之外的魔法;接收者类型写在签名上
- 与接口衔接:方法集决定隐式满足
边界情况
- 值方法里改字段不影响调用方原值。
nil指针调用指针方法:可能合法(未解引用)或 panic。- 不可导出字段在包外无法直接访问,但同包方法可封装。
- 结构体含 slice/map 字段时,值复制是浅拷贝描述符。
- 在方法值
u.Rename中绑定接收者,注意生命周期。
常见误区
[!warning] 常见误区:把 struct 当 class 错误:期待构造继承多态关键字。
正确:组合 + 接口 + 方法集。
[!warning] 常见误区:值方法里修改并期望生效 错误:
(u User) Set(x)改u.x。
正确:用指针接收者或返回新值。
[!warning] 常见误区:混用接收者导致接口困惑 错误:有的方法值、有的指针,却期望
T满足接口。
正确:先查方法集。
工程实践
- 字段少而内聚;大对象拆分
- 可变性通过指针接收者集中表达
- 校验放在
New*或显式Validate - 对导出 API 保持最小字段导出
- 测试表驱动覆盖零值与非法状态
- 含
sync.Mutex的结构体不要值复制
可验证实验
- 值接收者改字段,打印调用前后。
- 指针接收者改字段对比。
- 将仅指针方法的类型赋给接口:值与指针谁能编译。
- 复制含 map 字段的 struct,改 map 看是否共享。
本节总结
- 本质:聚合数据 + 带接收者的函数。
- 关键:导出规则、零值、接收者复制 vs 共享、方法集连接接口。
- 下一步:go-method-sets-and-receivers 与 go-interfaces。
自测题
概念题
- 方法与普通函数的本质区别?
- 何时必须用指针接收者才能改状态?
- 小写字段包外能直接读吗?
代码推理题
type C struct{ n int }
func (c C) Inc() { c.n++ }
func main() {
var c C
c.Inc()
fmt.Println(c.n)
}
输出?如何修正?
工程思考题
Config 含 map[string]string 与 mu sync.Mutex。应值传递还是指针?New 返回什么?
参考答案
展开
- 多了接收者,归属类型方法集。
- 要修改调用方持有的那份状态时。
- 不能。
代码输出 0;改为*C接收者。
工程:指针传递;New返回*Config并初始化 map;避免复制锁。
延伸阅读
| 资料 | 支撑 |
|---|---|
| Spec — Struct types | 结构体 |
| Spec — Method declarations | 方法 |
| Effective Go — Methods | 风格 |
| Go Code Review Comments | 接收者等约定 |
笔记元信息
- 文件名:
go-structs-and-methods.md - 状态:已深化