结构体与方法
# 第 9 章 结构体与方法
Go 没有 class,OOP 通过 struct + 方法 + 接口 实现。本章讲清"接收者怎么选"——这是 Go 工程师的第一关。 关键词:值接收者 vs 指针接收者、方法集、嵌入字段、struct Tag、
==可比较性、空 struct
# 目录介绍
- 9.1 本章学习目标
- 9.2 没有 class,只有 struct + 方法
- 9.3 方法定义:接收者
- 9.4 嵌入字段:组合优于继承
- 9.5 struct 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 (接收者名 接收者类型) 方法名(参数) 返回值 { ... }
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
重要约束:
- 接收者类型必须是当前包内定义的类型(不能给
int、string直接加方法,但可以type MyInt int后给MyInt加) - 接收者类型不能是接口类型,也不能是指针类型自身(
*int不行,但*MyInt可以——因为MyInt才是基础类型) - 方法名在类型 + 接收者层级唯一即可,不同类型可以有同名方法
# 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 —— 没改!
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
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()
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 是变量,可寻址
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"]
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 // ✅
}
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() // ❌ 不可寻址
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) // 旺财 ← 字段被"提升"
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) // 显式路径
// 二者等价
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 // 显式三层
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") // ✅ 调用提升来的方法
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 // ✅ 显式选
2
3
4
5
6
7
8
9
10
11
冲突规则:
- 同一深度有多个同名字段 → 编译错(除非显式选路径)
- 不同深度同名字段 → 最浅层胜出
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
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!
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"`
}
2
3
4
Tag 本身对编译期、运行期行为没有任何影响——它只是字段附加的字符串。是各种库(json/yaml/gorm/validator)通过反射读取它来决定行为。
# 9.5.1 Tag 语法约定
`key1:"value1" key2:"value2,opt1,opt2"`
- 整个 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"`
}
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
2
3
4
5
6
注意:
- Tag 内拼写错误(如
josn:"name")编译不报错、运行时也不报错——库直接当作"没设置",按字段名小写处理 - 推荐配置
golangci-lint启用tagliatelle/gofmt -s等检查器 - 复杂 Tag 写完用
go vet跑一遍,能查出格式错误
go vet ./... # 会检查常见 Tag 格式
# 9.6 struct 的 == 可比较性
规则:所有字段都是可比较类型时,struct 才可比较。
type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true ✅
2
3
4
含 slice / map / func 字段的 struct 不可比较:
type Bad struct {
Tags []string
}
b1, b2 := Bad{}, Bad{}
fmt.Println(b1 == b2) // ❌ 编译错:cannot compare Bad
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
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",
}
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
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 { /* 命中 */ }
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")
2
3
4
5
6
7
用法 4:实现接口的"零字节标记类型"
type ReadOnly struct{}
func (ReadOnly) IsReadOnly() {}
type WriteOnly struct{}
func (WriteOnly) IsWriteOnly() {}
2
3
4
5
用法 5:占位
// 比 chan bool 略省,但更重要的是表达"我不带数据"
type Event chan struct{}
2
底层小知识:所有 struct{} 实例共享同一个地址(runtime.zerobase)——你不能依赖它们的指针唯一性。
a, b := struct{}{}, struct{}{}
fmt.Println(&a == &b) // 在某些场景下 true(取决于编译器优化)
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
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
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)
}
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 }
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 指针
2
3
4
5
如果接收者是值(不需要 nil 安全),永远不要让外部拿到 nil 指针的 Logger。要支持 nil-safe,必须改成指针接收者并做 nil 判断:
func (l *Logger) Log(s string) {
if l == nil {
return
}
fmt.Println(s)
}
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]
}
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
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" // ✅ 显式选
2
3
4
5
6
7
8
9
10
最佳实践:嵌入两个含同名字段的类型时,要么显式给字段名(R Reader),要么避免嵌入。
# 9.12 思考题
- 解释 Go 为什么不支持继承。如果让你给 Go 加一个最小的"继承"能力,你会怎么设计?需要解决什么 trade-off?
- 写一段代码:定义类型
Stack[T](Go 1.18+ 泛型),含Push、Pop、Peek、Len四个方法。请说明你为什么选值接收者还是指针接收者,并保证一致性。 - 嵌入字段与组合(普通字段)相比,什么时候该用嵌入、什么时候该用普通字段?请举一个真实业务场景。
- 如果一个 struct 含
*sync.Mutex(指针)而不是sync.Mutex(值),它能值传递吗?这种写法的 trade-off 是什么? struct{}与interface{}各占多少字节?举一个例子说明用哪个更合适。- 假设接口
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 - 写一个
OrderedSet[T comparable],要求:保留插入顺序、O(1) 查找、可迭代。请用嵌入 + 组合 + 空 struct 至少各一处。
# 9.13 推荐阅读
# 卷内
# 跨卷
# 外部资料
- 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)