Day 4|struct 设计、组合与嵌入(Go 的建模方式)[Go 语言 30 天系统学习计划]

学习目标
掌握 struct 在 Go 中的真实定位(数据 + 行为载体)
学会用 组合(composition) 代替继承
正确理解 嵌入(embedding) 的语义与使用边界
在工程中设计“边界清晰、不易失控”的 struct

一、struct 在 Go 中是什么

在 Go 中:

struct 是最基础、最重要的建模工具

它同时承担两种角色:
数据结构(字段)
行为承载体(方法)

Go 没有 class,但 struct + method + interface = class 能力

二、struct 的基本定义与使用

1. 定义 struct

type User struct {
    ID   int
    Name string
    Age  int
}

2. 创建 struct 实例

u := User{
    ID:   1,
    Name: "Tom",
    Age:  18,
}

说明:

  • 推荐使用 字段名初始化
  • 避免依赖字段顺序

3. 零值 struct

var u User
fmt.Println(u) // {0 "" 0}

结论:

Go 的零值是“可用状态”,不是半成品


三、struct 作为参数与返回值

1. struct 作为参数(值拷贝)

func printUser(u User) {
    u.Name = "Jack"
}

func main() {
    u := User{Name: "Tom"}
    printUser(u)
    fmt.Println(u.Name) // Tom
}

原因:

  • struct 默认按值传递

2. struct 指针作为参数(推荐)

func printUser(u *User) {
    u.Name = "Jack"
}

func main() {
    u := User{Name: "Tom"}
    printUser(&u)
    fmt.Println(u.Name) // Jack
}

工程结论:

参与修改的 struct,必须使用指针


四、struct 的方法设计

1. 方法属于 struct,而不是函数

func (u *User) Rename(name string) {
    u.Name = name
}

调用:

u.Rename("Jack")

说明:

  • Go 会自动处理 &u / *u
  • 调用方无需关心指针细节

2. 方法接收者选择原则(非常重要)

使用 指针接收者 的场景:

  • 方法需要修改 struct
  • struct 较大(避免拷贝)
  • struct 将被接口实现

使用 值接收者 的场景:

  • struct 很小
  • 不涉及修改
  • 明确表达“不可变行为”

五、组合(Composition)是 Go 的核心设计方式

1. 为什么 Go 不支持继承

继承的问题:

  • 强耦合
  • 层级膨胀
  • 行为不可控

Go 的解决方案:

组合能力,而不是继承类型


2. 普通组合(has-a)

type Logger struct{}

func (l *Logger) Log(msg string) {
    fmt.Println("log:", msg)
}

type UserService struct {
    logger *Logger
}

调用:

s := UserService{
    logger: &Logger{},
}
s.logger.Log("hello")

特点:

  • 明确依赖关系
  • 行为来源清晰

六、嵌入(Embedding):Go 的“能力提升器”

1. 什么是嵌入

type Logger struct{}

func (l *Logger) Log(msg string) {
    fmt.Println("log:", msg)
}

type UserService struct {
    Logger
}

调用:

s := UserService{}
s.Log("hello")

说明:

  • UserService 并不是 Logger
  • 但“表现得像”有 Log 方法

2. 嵌入的本质

嵌入 = 字段 + 方法提升(promotion)

等价于:

type UserService struct {
    Logger Logger
}

只是语法更简洁。


七、嵌入 struct vs 嵌入接口

1. 嵌入 struct

type Service struct {
    Logger
}

特点:

  • 复用具体实现
  • 耦合较强

2. 嵌入接口(更推荐)

type Logger interface {
    Log(msg string)
}

type Service struct {
    Logger
}

优点:

  • 面向接口
  • 易替换
  • 易测试

八、字段与方法冲突(必须知道)

示例:字段覆盖

type A struct{}

func (A) Hello() {
    fmt.Println("A")
}

type B struct {
    A
}

func (B) Hello() {
    fmt.Println("B")
}
b := B{}
b.Hello() // B
b.A.Hello() // A

结论:

外层优先,内层可显式访问


九、struct 设计的工程原则(非常重要)

1. struct 不要过大

反例:

type User struct {
    ID int
    Name string
    Age int
    Address string
    OrderCount int
    // 一堆无关字段
}

建议:

  • 拆分领域对象
  • 一个 struct 只做一件事

2. 不要把“行为无关的字段”塞进 struct

struct 应该代表:

  • 一个明确的概念
  • 一组内聚的数据和行为

3. struct 是稳定边界

  • struct 变化 = API 变化
  • 对外暴露的 struct 要极度谨慎

十、自检问题(进入 Day 5 前必须能回答)

  1. struct 和 class 的本质区别是什么?
  2. 为什么 Go 强调组合而不是继承?
  3. 嵌入到底“多”了什么能力?
  4. 什么时候该嵌入接口?
  5. struct 设计中最容易犯的错误是什么?

预告:Day 5

Day 5|interface 设计原则与工程实践

你将真正掌握:

  • Go 接口的设计哲学
  • 小接口原则
  • 面向接口编程的正确姿势
  • 为什么“接口是为使用者设计的”

发表评论