编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
        • 目录介绍
        • 9.1 本章学习目标
        • 9.2 没有 class,只有 struct + 方法
        • 9.3 方法定义:接收者
          • 9.3.1 值接收者 func (s S)
          • 9.3.2 指针接收者 func (s *S)
          • 9.3.3 接收者选择决策表
          • 9.3.4 方法集与接口实现
        • 9.4 嵌入字段:组合优于继承
          • 9.4.1 字段提升
          • 9.4.2 方法提升
          • 9.4.3 同名字段冲突解决
          • 9.4.4 嵌入不是继承
        • 9.5 struct Tag:反射的元数据
          • 9.5.1 Tag 语法约定
          • 9.5.2 常见 Tag:json/yaml/gorm
          • 9.5.3 Tag 的解析与校验
        • 9.6 struct 的 == 可比较性
        • 9.7 空 struct struct{} 的妙用
        • 9.8 方法表达式与方法值
        • 9.9 综合示例:构建 User-Account 模型
        • 9.10 本章底层原理(简介)
        • 9.11 Go 新手陷阱 Top 5
          • ❌ 陷阱 1:同一类型混用值/指针接收者
          • ❌ 陷阱 2:值接收者 + nil 指针调用
          • ❌ 陷阱 3:嵌入 Mutex 后被复制
          • ❌ 陷阱 4:含 slice/map 字段还想 ==
          • ❌ 陷阱 5:嵌入字段同名导致歧义
        • 9.12 思考题
        • 9.13 推荐阅读
          • 卷内
          • 跨卷
          • 外部资料
      • 接口与多态
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

结构体与方法

# 第 9 章 结构体与方法

Go 没有 class,OOP 通过 struct + 方法 + 接口 实现。本章讲清"接收者怎么选"——这是 Go 工程师的第一关。 关键词:值接收者 vs 指针接收者、方法集、嵌入字段、struct Tag、== 可比较性、空 struct


# 目录介绍

  • 9.1 本章学习目标
  • 9.2 没有 class,只有 struct + 方法
  • 9.3 方法定义:接收者
    • 9.3.1 值接收者 func (s S)
    • 9.3.2 指针接收者 func (s *S)
    • 9.3.3 接收者选择决策表
    • 9.3.4 方法集与接口实现
  • 9.4 嵌入字段:组合优于继承
    • 9.4.1 字段提升
    • 9.4.2 方法提升
    • 9.4.3 同名字段冲突解决
    • 9.4.4 嵌入不是继承
  • 9.5 struct Tag:反射的元数据
    • 9.5.1 Tag 语法约定
    • 9.5.2 常见 Tag:json/yaml/gorm
    • 9.5.3 Tag 的解析与校验
  • 9.6 struct 的 == 可比较性
  • 9.7 空 struct struct{} 的妙用
  • 9.8 方法表达式与方法值
  • 9.9 综合示例:构建 User-Account 模型
  • 9.10 本章底层原理(简介)
  • 9.11 Go 新手陷阱 Top 5
  • 9.12 思考题
  • 9.13 推荐阅读

# 9.1 本章学习目标

  • ✅ 用"是否修改 / 大小 / 一致性 / 含 Mutex / 接口需求"五条规则决定值 vs 指针接收者
  • ✅ 解释方法集规则:T 的方法集 vs *T 的方法集,以及它对接口实现的影响
  • ✅ 用嵌入字段实现"组合优于继承",并能解释嵌入不是继承的本质区别
  • ✅ 写 struct Tag 配合 encoding/json、gorm、validator
  • ✅ 知道哪些 struct 可以 ==,哪些不行(含 slice/map/func 字段不可比)
  • ✅ 列举 5 个空 struct struct{} 的工程用法(信号、Set、零字节占位)

# 9.2 没有 class,只有 struct + 方法

Go 故意没有 class 关键字。它的"面向对象"由三件套实现:

OOP 特性 Java/C++/Python Go 的实现
数据封装 class { ... } struct { ... }
行为绑定 类内方法 func (recv T) Method() 顶层定义
继承 extends 没有——用嵌入字段 + 组合替代
多态 虚函数 / 抽象类 接口(鸭子类型,第 10 章)
构造函数 new ClassName() func New(...) *T 工厂函数(约定)
析构函数 ~ClassName() 没有——用 defer + 显式 Close()
访问控制 public/private/protected 大写导出 / 小写私有(包级别)

这种设计的好处:

  • 学习曲线短——没有继承层级、虚函数表、抽象类、最终类等概念
  • 依赖更扁——方法不强制写在类内,可以在任何文件给类型加方法(仅限本包定义的类型)
  • 接口即契约——多态完全由"实现了哪些方法"决定,不需要 implements

代价是:复杂的层次化抽象(如 GUI 控件树)写起来不如 Java 顺手——但这正是 Go 的设计选择。


# 9.3 方法定义:接收者

方法 = 带"接收者"的函数。语法:

func (接收者名 接收者类型) 方法名(参数) 返回值 { ... }
1
type Counter struct{ n int }

// 值接收者
func (c Counter) Get() int { return c.n }

// 指针接收者
func (c *Counter) Inc() { c.n++ }

func main() {
    c := Counter{}
    c.Inc()       // 自动取址:(&c).Inc()
    c.Inc()
    fmt.Println(c.Get()) // 2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

重要约束:

  1. 接收者类型必须是当前包内定义的类型(不能给 int、string 直接加方法,但可以 type MyInt int 后给 MyInt 加)
  2. 接收者类型不能是接口类型,也不能是指针类型自身(*int 不行,但 *MyInt 可以——因为 MyInt 才是基础类型)
  3. 方法名在类型 + 接收者层级唯一即可,不同类型可以有同名方法

# 9.3.1 值接收者 func (s S)

值接收者收到的是 struct 的副本——方法内修改不影响外部:

type Counter struct{ n int }

func (c Counter) Inc() { c.n++ } // ❌ 改的是副本

c := Counter{}
c.Inc()
c.Inc()
fmt.Println(c.n) // 0 —— 没改!
1
2
3
4
5
6
7
8

值接收者适用场景:

  • 不修改接收者状态(纯读 / 纯计算)
  • 接收者是小结构体(参考第 8 章 §8.8.2 的阈值经验)
  • 接收者是基础类型(type Celsius float64 这种)
  • 接收者本身就是引用语义(map、chan)

# 9.3.2 指针接收者 func (s *S)

指针接收者收到的是指针副本——但因为指向同一块内存,写就改原值:

func (c *Counter) Inc() { c.n++ } // ✅ 改原值

c := Counter{}
c.Inc()
c.Inc()
fmt.Println(c.n) // 2
1
2
3
4
5
6

指针接收者适用场景:

  • 需要修改接收者
  • 接收者是大结构体(避免复制成本)
  • 接收者含 sync.Mutex 等"不可复制"字段
  • 需要表达"nil 接收者也是合法状态"(nil-safe 方法)

自动取址 / 自动解引用——Go 的语法糖:

var c Counter = Counter{}     // 值类型变量
var p *Counter = &Counter{}   // 指针类型变量

c.Inc()  // ✅ 自动取址:(&c).Inc()
p.Get()  // ✅ 自动解引用:(*p).Get()
1
2
3
4
5

但有一个例外——不可寻址的值不能直接调用指针接收者方法:

// 假设 NewCounter 返回 Counter(非指针)
type Counter struct{ n int }
func NewCounter() Counter { return Counter{} }
func (c *Counter) Inc()   { c.n++ }

NewCounter().Inc() // ❌ 编译错:cannot call pointer method on NewCounter()
                   //    (函数返回值不可寻址,无法 &NewCounter())

c := NewCounter()
c.Inc() // ✅ c 是变量,可寻址
1
2
3
4
5
6
7
8
9
10

map 元素同理:

m := map[string]Counter{"a": {}}
m["a"].Inc() // ❌ 编译错:cannot take the address of m["a"]
1
2

修复方法见 §8.3.2(取出来改完放回,或改为 map[string]*Counter)。

# 9.3.3 接收者选择决策表

工程上按这条优先级链做决策(自上而下,命中即用):

# 条件 选择
1 方法需要修改接收者 指针
2 接收者含 sync.Mutex / sync.WaitGroup / atomic.Value 等不可复制字段 指针
3 接收者大(≥ 4 字段或 ≥ 64 B) 指针
4 同一个类型上其他方法已经用了指针接收者(一致性原则) 指针
5 接收者是基础类型(type Email string、type Age int) 值
6 接收者是 map / chan / func 值(它们本身已是引用)
7 以上都不命中 值(默认)

Go 官方 FAQ 在 Should I define methods on values or pointers? (opens new window) 给的总结:

If in doubt, use a pointer receiver.

但这条 FAQ 之后还有一段:

A type's method set should be consistent. If some methods of the type must have pointer receivers, the rest should too.

一致性原则比"是否修改"更重要——同一类型混用值/指针接收者会让接口实现规则混乱(见下节)。

# 9.3.4 方法集与接口实现

这是 Go 类型系统里最容易翻车的一节。先记结论:

表达式 拥有的方法
T 所有值接收者方法 func (T)
*T 所有方法(值接收者 + 指针接收者)

也就是说:

  • 值接收者方法,T 和 *T 都能调
  • 指针接收者方法,只有 *T 能调(编译器的自动取址只在变量可寻址时生效)

对接口实现的影响:

type Stringer interface{ String() string }

type Animal struct{ Name string }

// 指针接收者
func (a *Animal) String() string { return a.Name }

func main() {
    var s Stringer

    a := Animal{Name: "cat"}
    s = a   // ❌ 编译错:Animal does not implement Stringer
            //    (String method has pointer receiver)
    s = &a  // ✅
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

核心规则一句话:

如果接口的方法是指针接收者实现的,那么只有指针类型满足这个接口;值类型不满足。

工程后果——这是为什么 §9.3.3 的"一致性原则"重要:

// ❌ 混用——使用方很困惑
func (a Animal) Eat()    {}
func (a *Animal) Sleep() {}

// 使用方:
var a Animal
a.Eat()    // ✅
a.Sleep()  // ✅ 编译器自动取址(a 可寻址)

var as []Animal = ...
as[0].Sleep()  // ✅ 可寻址
m["x"].Sleep() // ❌ 不可寻址
1
2
3
4
5
6
7
8
9
10
11
12

统一选指针接收者或统一选值接收者,让使用方有可预测的行为。


# 9.4 嵌入字段:组合优于继承

Go 用嵌入(embedding)模拟"继承"的部分能力——但它不是继承,本质是组合的语法糖。

type Animal struct{ Name string }

func (a Animal) Eat() { fmt.Println(a.Name, "eating") }

type Dog struct {
    Animal // 嵌入字段:没有字段名,类型名即字段名
    Breed string
}

d := Dog{
    Animal: Animal{Name: "旺财"},
    Breed:  "金毛",
}
d.Eat()           // 旺财 eating  ← 方法被"提升"
fmt.Println(d.Name) // 旺财       ← 字段被"提升"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9.4.1 字段提升

嵌入字段的字段会被"提升"到外层 struct,看起来像外层自己的字段:

fmt.Println(d.Name)        // 提升路径
fmt.Println(d.Animal.Name) // 显式路径
// 二者等价
1
2
3

多层嵌入也可提升:

type A struct{ X int }
type B struct{ A }
type C struct{ B }

c := C{}
c.X = 1                    // 提升两层
c.B.A.X = 1                // 显式三层
1
2
3
4
5
6
7

冲突时最浅层胜出(详见 9.4.3)。

# 9.4.2 方法提升

嵌入字段的方法也会被提升——这是 Go 实现"代码复用"的核心机制:

type Logger struct{}
func (l Logger) Log(s string) { fmt.Println("[LOG]", s) }

type Server struct {
    Logger // 嵌入
    addr string
}

s := Server{addr: ":8080"}
s.Log("server started") // ✅ 调用提升来的方法
1
2
3
4
5
6
7
8
9
10

接收者类型的提升规则:

嵌入字段类型 Server 拥有的方法(外层是值)
Logger(值嵌入) Logger 的所有值/指针接收者方法
*Logger(指针嵌入) Logger 的所有值/指针接收者方法

写 Server * 时拥有更多方法集——具体规则参考 Go 规范 (opens new window)。实务上记住:嵌入指针类型可以让外层的方法集更全。

# 9.4.3 同名字段冲突解决

type A struct{ X int }
type B struct{ X int }
type C struct {
    A
    B
}

c := C{}
// c.X = 1   // ❌ 编译错:ambiguous selector c.X
c.A.X = 1    // ✅ 显式选
c.B.X = 2    // ✅ 显式选
1
2
3
4
5
6
7
8
9
10
11

冲突规则:

  1. 同一深度有多个同名字段 → 编译错(除非显式选路径)
  2. 不同深度同名字段 → 最浅层胜出
type A struct{ X int }    // 深度 2
type B struct{ A }        // 深度 1: B.X 提升自 A.X
type C struct {
    B
    X int                 // 深度 0
}

c := C{}
c.X = 1     // ✅ 命中 C 自己的 X
c.B.A.X = 2 // ✅ 显式访问深层 X
1
2
3
4
5
6
7
8
9
10

# 9.4.4 嵌入不是继承

这是初学者最容易踩的认知坑。嵌入只是"把方法/字段借过来",没有继承的多态语义:

type Animal struct{ Name string }
func (a Animal) Hello() { fmt.Println("Animal:", a.Name) }

type Dog struct{ Animal }
func (d Dog) Hello() { fmt.Println("Dog:", d.Name) }

// 关键:Animal 上的方法看不到 Dog
func (a Animal) Greet() { a.Hello() } // 这里调的永远是 Animal.Hello

d := Dog{Animal: Animal{Name: "旺财"}}
d.Hello()  // Dog: 旺财
d.Greet()  // Animal: 旺财  ← 不是 Dog 的 Hello!
1
2
3
4
5
6
7
8
9
10
11
12

在 Java/C++ 里,Greet 内部 this.Hello() 会动态分发到子类的覆盖方法。Go 没有这种动态分发——Animal.Greet 里的 a 类型就是 Animal,调到的就是 Animal.Hello。

含义:

  • ❌ 不要用嵌入模拟"模板方法模式"(父类调子类覆盖的钩子)——Go 里这个模式实现不了
  • ✅ 用接口 + 组合实现多态(详见第 10 章)

Rob Pike 在 Go at Google (opens new window) 强调: Object-oriented programming, at least in the best-known languages, involves too much discussion of the relationships between types... Go takes an unusual approach and lets the type relationships fall out naturally.


# 9.5 struct Tag:反射的元数据

Tag 是写在字段后面的字符串字面量,在反射时可读取:

type User struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age,omitempty" validate:"gte=0,lte=150"`
}
1
2
3
4

Tag 本身对编译期、运行期行为没有任何影响——它只是字段附加的字符串。是各种库(json/yaml/gorm/validator)通过反射读取它来决定行为。

# 9.5.1 Tag 语法约定

`key1:"value1" key2:"value2,opt1,opt2"`
1
  • 整个 tag 用反引号 ` 包裹(否则 " 要转义,可读性差)
  • 多个 key 用空格分隔
  • value 用双引号包裹
  • value 内部用逗号分隔多个选项

强约定但非语法:每个 key 由对应的库定义。所以同一字段可以有多个 key 给不同库用。

# 9.5.2 常见 Tag:json/yaml/gorm

type User struct {
    ID        uint64    `json:"id" gorm:"primaryKey"`
    Name      string    `json:"name" gorm:"size:64;not null" validate:"required,min=2"`
    Email     string    `json:"email,omitempty" gorm:"uniqueIndex" validate:"email"`
    Age       int       `json:"age" validate:"gte=0,lte=150"`
    Password  string    `json:"-"` // - 表示永远不输出
    CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
}
1
2
3
4
5
6
7
8
库 Tag key 常见选项
encoding/json json omitempty、-、自定义名
gopkg.in/yaml.v3 yaml omitempty、flow、inline
gorm.io/gorm gorm primaryKey、size:N、uniqueIndex、not null
go-playground/validator validate required、min、max、email、oneof=a b c
mapstructure mapstructure squash、remain

# 9.5.3 Tag 的解析与校验

import "reflect"

t := reflect.TypeOf(User{})
f, _ := t.FieldByName("Name")
fmt.Println(f.Tag.Get("json"))     // name
fmt.Println(f.Tag.Get("validate")) // required,min=2
1
2
3
4
5
6

注意:

  • Tag 内拼写错误(如 josn:"name")编译不报错、运行时也不报错——库直接当作"没设置",按字段名小写处理
  • 推荐配置 golangci-lint 启用 tagliatelle / gofmt -s 等检查器
  • 复杂 Tag 写完用 go vet 跑一遍,能查出格式错误
go vet ./...  # 会检查常见 Tag 格式
1

# 9.6 struct 的 == 可比较性

规则:所有字段都是可比较类型时,struct 才可比较。

type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true ✅
1
2
3
4

含 slice / map / func 字段的 struct 不可比较:

type Bad struct {
    Tags []string
}
b1, b2 := Bad{}, Bad{}
fmt.Println(b1 == b2) // ❌ 编译错:cannot compare Bad
1
2
3
4
5

完整可比较性表(来自 Go 规范):

类型 可比较
数字 / 布尔 / 字符串 / 指针 / channel ✅
接口(动态类型可比较时) ✅(运行时 panic 风险)
数组(元素可比较时) ✅
struct(所有字段可比较时) ✅
slice / map / func ❌

接口值比较的运行时风险:

var a, b interface{} = []int{1, 2}, []int{1, 2}
fmt.Println(a == b) // ❌ panic: comparing uncomparable type []int
1
2

不可比较的 struct 想比较内容,用:

  • reflect.DeepEqual(a, b)——通用但慢
  • slices.Equal / maps.Equal——Go 1.21+,快且类型安全
  • 手写 Equal 方法——最快、最可控

可比较 struct 还能作 map key:

type Coord struct{ X, Y int }
m := map[Coord]string{
    {0, 0}: "origin",
    {1, 1}: "diag",
}
1
2
3
4
5

不可比较 struct 不能作 map key——尝试就编译错。


# 9.7 空 struct struct{} 的妙用

struct{} 是大小为 0 字节的类型——它不占内存,但是个完整的类型。在 Go 工程里有 5 个高频用法:

用法 1:信号 channel

done := make(chan struct{})
go func() {
    doWork()
    close(done) // 或 done <- struct{}{}
}()
<-done
1
2
3
4
5
6

只关心"事件发生"不关心数据时,用 struct{} 比 bool 更省内存(虽然单个 bool 也只 1B,但意图更清晰——"我不带数据")。

用法 2:Set 数据结构

type StringSet map[string]struct{}

s := StringSet{}
s["a"] = struct{}{}
s["b"] = struct{}{}

if _, ok := s["a"]; ok { /* 命中 */ }
1
2
3
4
5
6
7

map[string]struct{} 比 map[string]bool 省内存——Go 1.20 后差异不大(map value 实际占用都按对齐算),但 struct{} 明确表达"value 无意义" 的语义。

用法 3:纯方法集类型(无状态服务)

type UserService struct{}

func (UserService) Create(name string) (*User, error) { ... }
func (UserService) Find(id uint64) (*User, error)     { ... }

var svc = UserService{}
svc.Create("alice")
1
2
3
4
5
6
7

用法 4:实现接口的"零字节标记类型"

type ReadOnly struct{}
func (ReadOnly) IsReadOnly() {}

type WriteOnly struct{}
func (WriteOnly) IsWriteOnly() {}
1
2
3
4
5

用法 5:占位

// 比 chan bool 略省,但更重要的是表达"我不带数据"
type Event chan struct{}
1
2

底层小知识:所有 struct{} 实例共享同一个地址(runtime.zerobase)——你不能依赖它们的指针唯一性。

a, b := struct{}{}, struct{}{}
fmt.Println(&a == &b) // 在某些场景下 true(取决于编译器优化)
1
2

# 9.8 方法表达式与方法值

Go 允许把方法当函数值用,分两种形式:

方法值(Method Value)——绑定接收者:

type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }

c := &Counter{}
inc := c.Inc        // 类型: func(),已绑定 c
inc()
inc()
fmt.Println(c.n)    // 2
1
2
3
4
5
6
7
8

c.Inc 是一个闭包:捕获了 c,每次调用都作用在那个 c 上。

方法表达式(Method Expression)——未绑定接收者:

inc2 := (*Counter).Inc  // 类型: func(*Counter),接收者作为首参数
c2 := &Counter{}
inc2(c2)
inc2(c2)
fmt.Println(c2.n)  // 2
1
2
3
4
5

适用场景:

  • 回调注册:http.HandleFunc 接收 func,可直接传方法值
    h := &Handler{}
    http.HandleFunc("/", h.ServeHTTP)
    
    1
    2
  • 批量调用:for _, c := range counters { c.Inc }(用方法表达式 + 显式传接收者)
  • 降低耦合:把"动作"传给下层,不暴露完整对象

# 9.9 综合示例:构建 User-Account 模型

把本章学到的全部组合起来:嵌入、Tag、接收者选型、空 struct 标记、方法值。

package model

import (
    "errors"
    "fmt"
    "sync"
    "time"
)

// 1. 基础 audit 字段,多个模型共享 ─────────────
type Audit struct {
    CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
    UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}

func (a *Audit) Touch() { a.UpdatedAt = time.Now() }

// 2. User 模型,嵌入 Audit + 含 Mutex(必须指针接收者) ─────
type User struct {
    Audit          // 嵌入 → 自动有 CreatedAt/UpdatedAt 与 Touch()
    ID    uint64   `json:"id" gorm:"primaryKey"`
    Name  string   `json:"name" gorm:"size:64;not null" validate:"required,min=2"`
    Email string   `json:"email" gorm:"uniqueIndex" validate:"email"`
    mu    sync.Mutex // 私有字段;含 Mutex 强制 *User 接收者
}

// 一致性:所有方法都用指针接收者
func (u *User) Rename(name string) error {
    if len(name) < 2 {
        return errors.New("name too short")
    }
    u.mu.Lock()
    defer u.mu.Unlock()
    u.Name = name
    u.Touch() // 嵌入方法
    return nil
}

func (u *User) String() string {
    return fmt.Sprintf("User{ID=%d, Name=%s}", u.ID, u.Name)
}

// 3. Account 模型,与 User 是组合关系 ─────────────
type Account struct {
    Audit
    ID      uint64 `json:"id" gorm:"primaryKey"`
    UserID  uint64 `json:"user_id" gorm:"index"`
    Balance int64  `json:"balance"`
}

func (a *Account) Deposit(amount int64) error {
    if amount <= 0 {
        return errors.New("amount must be positive")
    }
    a.Balance += amount
    a.Touch()
    return nil
}

// 4. UserService 是无状态服务,用空 struct ─────
type UserService struct{}

func (UserService) New(name, email string) *User {
    return &User{
        ID:    nextID(),
        Name:  name,
        Email: email,
        Audit: Audit{CreatedAt: time.Now(), UpdatedAt: time.Now()},
    }
}

// 5. 工厂 + 方法值用法 ─────────────────────
func nextID() uint64 { /* ... */ return 1 }

func main() {
    svc := UserService{}
    u := svc.New("alice", "a@x.com")

    // 方法值:把方法作为回调传递
    rename := u.Rename
    if err := rename("alicia"); err != nil {
        fmt.Println(err)
    }
    fmt.Println(u)

    acc := &Account{UserID: u.ID}
    _ = acc.Deposit(100)
    fmt.Printf("balance: %d, updated: %v\n", acc.Balance, acc.UpdatedAt)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

这个例子覆盖了:

  • 嵌入 Audit 让多个模型共享审计字段(字段提升 + 方法提升)
  • User 含 sync.Mutex 字段 → 必须指针接收者
  • 一致性原则:*User 上的方法全部用指针接收者
  • Tag 多库共存:json + gorm + validate
  • 空 struct UserService 作为无状态服务的容器
  • 方法值 rename := u.Rename 当回调

# 9.10 本章底层原理(简介)

主题 简介 详解
struct 内存布局 字段顺序 = 内存顺序,会按字段类型对齐插 padding 卷三第 3 章
字段对齐与 padding int8 + int64 + int8 比 int8 + int8 + int64 多浪费 6 B 卷三第 3 章
方法的实现 编译期把 (t T).M() 重写成 M(t T) 普通函数 卷三第 5 章
方法表达式底层 方法值是闭包对象(含接收者),方法表达式是普通函数指针 卷三第 5 章
嵌入字段实现 编译器在选择器解析阶段做"提升" 卷三第 5 章
接口实现判定 编译期看类型是否包含接口要求的方法集 卷三第 5 章
空 struct 优化 所有 struct{} 实例共享 runtime.zerobase,零分配 卷三第 1 章

# 9.11 Go 新手陷阱 Top 5

# ❌ 陷阱 1:同一类型混用值/指针接收者

type Cache struct{ data map[string]string }

func (c Cache)  Get(k string) string { return c.data[k] }
func (c *Cache) Set(k, v string)     { c.data[k] = v }
1
2
3
4

这种混用让"是否实现接口"难判断、文档要写一堆注意事项。统一指针接收者或统一值接收者。

# ❌ 陷阱 2:值接收者 + nil 指针调用

type Logger struct{}
func (l Logger) Log(s string) { fmt.Println(s) }

var p *Logger // nil
p.Log("hi") // ❌ panic:值接收者会自动解引用 nil 指针
1
2
3
4
5

如果接收者是值(不需要 nil 安全),永远不要让外部拿到 nil 指针的 Logger。要支持 nil-safe,必须改成指针接收者并做 nil 判断:

func (l *Logger) Log(s string) {
    if l == nil {
        return
    }
    fmt.Println(s)
}
1
2
3
4
5
6

# ❌ 陷阱 3:嵌入 Mutex 后被复制

type Cache struct {
    sync.Mutex
    data map[string]string
}

func (c Cache) Get(k string) string { // ❌ 值接收者复制了 Mutex
    c.Lock()
    defer c.Unlock()
    return c.data[k]
}
1
2
3
4
5
6
7
8
9
10

go vet 会报:Get passes lock by value。修复:改指针接收者。

含 sync.Mutex / sync.WaitGroup / sync.Once / atomic.Value 的 struct,永远只用指针——不要值传递、不要值接收者、不要嵌套到其他 struct 的值字段里。

# ❌ 陷阱 4:含 slice/map 字段还想 ==

type Item struct {
    ID   int
    Tags []string
}

a, b := Item{}, Item{}
fmt.Println(a == b) // ❌ 编译错:cannot compare Item
1
2
3
4
5
6
7

修复:

  • 用 reflect.DeepEqual(a, b)——通用但慢且失类型安全
  • 写 func (i Item) Equal(o Item) bool 手实现
  • 改类型不含 slice/map(如把 Tags 改 [K]string 数组——很罕见)

# ❌ 陷阱 5:嵌入字段同名导致歧义

type Reader struct{ Name string }
type Writer struct{ Name string }
type ReadWriter struct {
    Reader
    Writer
}

rw := ReadWriter{}
// rw.Name = "x" // ❌ 编译错:ambiguous selector
rw.Reader.Name = "x" // ✅ 显式选
1
2
3
4
5
6
7
8
9
10

最佳实践:嵌入两个含同名字段的类型时,要么显式给字段名(R Reader),要么避免嵌入。


# 9.12 思考题

  1. 解释 Go 为什么不支持继承。如果让你给 Go 加一个最小的"继承"能力,你会怎么设计?需要解决什么 trade-off?
  2. 写一段代码:定义类型 Stack[T](Go 1.18+ 泛型),含 Push、Pop、Peek、Len 四个方法。请说明你为什么选值接收者还是指针接收者,并保证一致性。
  3. 嵌入字段与组合(普通字段)相比,什么时候该用嵌入、什么时候该用普通字段?请举一个真实业务场景。
  4. 如果一个 struct 含 *sync.Mutex(指针)而不是 sync.Mutex(值),它能值传递吗?这种写法的 trade-off 是什么?
  5. struct{} 与 interface{} 各占多少字节?举一个例子说明用哪个更合适。
  6. 假设接口 Stringer { String() string } 由 *MyType 实现。下列代码哪行编译错?
    var s Stringer
    s = MyType{}            // (a)
    s = &MyType{}           // (b)
    m := MyType{}; s = &m   // (c)
    m := MyType{}; s = m    // (d)
    
    1
    2
    3
    4
    5
  7. 写一个 OrderedSet[T comparable],要求:保留插入顺序、O(1) 查找、可迭代。请用嵌入 + 组合 + 空 struct 至少各一处。

# 9.13 推荐阅读

# 卷内

  • 第 5 章 复合类型(struct 作为复合类型的基础)
  • 第 8 章 指针与逃逸(接收者选型的性能依据)
  • 第 10 章 接口与多态(方法集 → 接口实现)

# 跨卷

  • 卷三第 3 章 结构体与对齐
  • 卷三第 5 章 接口与类型系统
  • 卷四第 3 章 模型设计模式

# 外部资料

  • Go FAQ - Should I define methods on values or pointers? (opens new window)
  • Go FAQ - Why is there no type inheritance? (opens new window)
  • Effective Go - Embedding (opens new window)
  • Go 规范 - Method sets (opens new window)
  • Go at Google: Language Design in the Service of Software Engineering - Rob Pike (opens new window)
上次更新: 2026/06/10, 11:13:41
指针与逃逸
接口与多态

← 指针与逃逸 接口与多态→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式