主题
第06章 结构体与方法 —— 用代码描述世界
本章目标:用
struct把现实世界的"事物"建模到代码里,用method给它们加行为。这是 Go 面向对象的全部精髓。
1. 结构体是什么?
🌰 生活化比喻:结构体像身份证——把一个人的多个属性(姓名、年龄、性别、出生地)打包成一个整体。
go
type Person struct {
Name string
Age int
Email string
}📊 配套图解:diagrams/struct-anatomy.svg
2. 创建与使用
2.1 三种创建方式
go
// 方式1:按字段名(推荐!)
p1 := Person{Name: "Alice", Age: 30}
// 方式2:按顺序(脆弱!增加字段就崩)
p2 := Person{"Bob", 25, "bob@x.com"}
// 方式3:先建零值再赋值
var p3 Person
p3.Name = "Charlie"2.2 指针创建
go
p := &Person{Name: "Tom"} // *Person
p.Age = 18 // Go 自动解引用,等同于 (*p).Age2.3 字段访问
go
p.Name // 读
p.Age = 20 // 写
// 通过指针访问也是 .
pp := &p
pp.Name // Go 自动解引用3. 方法(Method):给类型加行为
go
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50(r Rectangle) 叫做接收者(receiver)。r 是接收到的实例。
3.1 值接收者 vs 指针接收者
go
// 值接收者:方法内修改不影响外部
func (r Rectangle) ScaleByVal(factor float64) {
r.Width *= factor // 改的是副本
}
// 指针接收者:方法内修改影响外部
func (r *Rectangle) ScaleByPtr(factor float64) {
r.Width *= factor // 改的是原对象
}🌰 生活化比喻:
- 值接收者 = 复印一份身份证给你修改 → 原件不变
- 指针接收者 = 把身份证原件给你 → 改完真的变了
3.2 怎么选?
| 场景 | 选择 |
|---|---|
| 需要修改字段 | 指针接收者 |
| 结构体很大(避免拷贝) | 指针接收者 |
实现 sync.Mutex 等带锁字段 | 必须指针 |
| 小结构、纯读取 | 值接收者 |
| 整个类型一致性 | 统一用指针(推荐) |
💡 黄金法则:拿不准就用指针接收者。
4. 结构体嵌入:Go 的"继承"
Go 没有继承,但有嵌入(Embedding)——把一个类型嵌进另一个,自动获得它的字段和方法。
go
type Animal struct {
Name string
}
func (a Animal) Speak() {
fmt.Println(a.Name, "在叫")
}
type Dog struct {
Animal // 匿名嵌入
Breed string
}
d := Dog{Animal: Animal{Name: "旺财"}, Breed: "金毛"}
d.Speak() // "旺财 在叫",自动获得 Animal 的方法
fmt.Println(d.Name) // "旺财",自动获得 Animal 的字段🌰 生活化比喻:嵌入像继承家产,但不是"我是你的子类",而是"你的东西也归我用"。
4.1 字段名冲突?外层覆盖
go
type A struct { Name string }
type B struct { A; Name string }
b := B{}
b.Name = "Outer"
b.A.Name = "Inner"
fmt.Println(b.Name) // "Outer"
fmt.Println(b.A.Name) // "Inner"5. 标签(Tag):元数据
go
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"username"`
}Tag 是字符串元数据,编译器不解析它,但 encoding/json、ORM 库会用反射读取。
最常见的用法:
json:"name"—— 控制 JSON 序列化字段名db:"col_name"—— 控制数据库列名映射validate:"required"—— 字段校验
6. 构造函数:约定俗成的 New
Go 没有构造函数语法,但社区约定用 NewXxx 命名:
go
type Server struct {
addr string
debug bool
}
func NewServer(addr string) *Server {
return &Server{addr: addr, debug: false}
}
s := NewServer("0.0.0.0:80")7. 结构体比较
go
p1 := Person{Name: "Tom", Age: 18}
p2 := Person{Name: "Tom", Age: 18}
fmt.Println(p1 == p2) // true⚠️ 但含 slice/map/func 字段的结构体不能用 ==:
go
type S struct { Data []int }
s1 := S{Data: []int{1}}
s2 := S{Data: []int{1}}
// fmt.Println(s1 == s2) // 编译错误8. 案例:实现一个简单的"账户"
go
type Account struct {
ID string
Owner string
Balance float64
}
func NewAccount(id, owner string) *Account {
return &Account{ID: id, Owner: owner}
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a *Account) Withdraw(amount float64) error {
if amount > a.Balance {
return errors.New("余额不足")
}
a.Balance -= amount
return nil
}
func (a Account) Info() string {
return fmt.Sprintf("账户 %s 持有人 %s 余额 %.2f", a.ID, a.Owner, a.Balance)
}9. 本章小结
✅ struct 用于打包多个相关字段
✅ func (r Type) Method() 给类型加方法
✅ 值接收者拷贝,指针接收者修改原对象
✅ Go 没有继承,用嵌入实现复用
✅ Tag 是字段元数据,配合 reflect 用
✅ 用 NewXxx 函数模拟构造函数
✅ 含 slice/map 的结构体不能用 == 比较