Skip to content

第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).Age

2.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    // 改的是原对象
}

🌰 生活化比喻:

  • 值接收者 = 复印一份身份证给你修改 → 原件不变
  • 指针接收者 = 把身份证原件给你 → 改完真的变了

📊 配套图解:diagrams/value-vs-pointer-receiver.svg

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"

📊 配套图解:diagrams/struct-embedding.svg


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 的结构体不能用 == 比较


📂 配套资源