Go 的 struct 与方法

用 struct 聚合数据、用方法附着行为:字段可见性、零值可用、值/指针接收者选择,以及与方法集、接口的衔接。

#type / concept #status / growing #tech / dev #resource / go

[!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

结合场景再看三个关注点

  1. struct 聚数据,方法挂行为
    没有 class,但 Member + 方法已经够表达会员领域对象。

  2. 值接收者 vs 指针接收者
    查询用值;改昵称/年龄用指针,否则状态丢在副本里。

  3. 调用处的自动取址
    m.Renamem 可寻址时会变成 (&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*TT 不能是指针)
  • 方法必须声明在与类型同一包
  • 语法糖:u.Rename 在需要时自动取址/解引用用于调用,但方法集规则更严

规范:Method declarations

值接收者 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 可变性之外的魔法;接收者类型写在签名上
  • 与接口衔接:方法集决定隐式满足

边界情况

  1. 值方法里改字段不影响调用方原值。
  2. nil 指针调用指针方法:可能合法(未解引用)或 panic。
  3. 不可导出字段在包外无法直接访问,但同包方法可封装。
  4. 结构体含 slice/map 字段时,值复制是浅拷贝描述符。
  5. 在方法值 u.Rename 中绑定接收者,注意生命周期。

常见误区

[!warning] 常见误区:把 struct 当 class 错误:期待构造继承多态关键字。
正确:组合 + 接口 + 方法集。

[!warning] 常见误区:值方法里修改并期望生效 错误:(u User) Set(x)u.x
正确:用指针接收者或返回新值。

[!warning] 常见误区:混用接收者导致接口困惑 错误:有的方法值、有的指针,却期望 T 满足接口。
正确:先查方法集。

工程实践

  1. 字段少而内聚;大对象拆分
  2. 可变性通过指针接收者集中表达
  3. 校验放在 New* 或显式 Validate
  4. 对导出 API 保持最小字段导出
  5. 测试表驱动覆盖零值与非法状态
  6. sync.Mutex 的结构体不要值复制

可验证实验

  1. 值接收者改字段,打印调用前后。
  2. 指针接收者改字段对比。
  3. 将仅指针方法的类型赋给接口:值与指针谁能编译。
  4. 复制含 map 字段的 struct,改 map 看是否共享。

本节总结

自测题

概念题

  1. 方法与普通函数的本质区别?
  2. 何时必须用指针接收者才能改状态?
  3. 小写字段包外能直接读吗?

代码推理题

type C struct{ n int }
func (c C) Inc() { c.n++ }
func main() {
    var c C
    c.Inc()
    fmt.Println(c.n)
}

输出?如何修正?

工程思考题

Configmap[string]stringmu sync.Mutex。应值传递还是指针?New 返回什么?

参考答案

展开
  1. 多了接收者,归属类型方法集。
  2. 要修改调用方持有的那份状态时。
  3. 不能。
    代码输出 0;改为 *C 接收者。
    工程:指针传递;New 返回 *Config 并初始化 map;避免复制锁。

延伸阅读

资料支撑
Spec — Struct types结构体
Spec — Method declarations方法
Effective Go — Methods风格
Go Code Review Comments接收者等约定

笔记元信息

  • 文件名:go-structs-and-methods.md
  • 状态:已深化
创建于 2026/6/20 更新于 2026/7/15