go 语言不支持继承,仅通过嵌入实现字段和方法提升,但嵌入非继承、无多态、无动态覆盖,且接口满足隐式、组合暴露依赖、字段声明即契约。

Go 语言没有继承,不是你没写对,是它压根不支持——所有试图用 Base 和 Extender 构造循环依赖来模拟“父类调用子类方法”的写法,都会在运行时崩掉或测试时无法打桩。
嵌入不是继承:struct{ A } 只是字段和方法的自动提升
嵌入一个结构体,比如 type Dog struct { Animal },并不会让 Dog “成为” Animal 的子类型。它只是把 Animal 的公开字段和方法“提上来”,调用 dog.Run() 实际仍是 Animal.Run() 的副本,且无法被 Dog 动态覆盖。
- 如果
Dog自己定义了同名的Run()方法,会完全遮蔽嵌入版本,这是编译期静态遮蔽,不是多态 - 若同时嵌入两个含
Log()的结构体,编译报错:ambiguous selector d.Log,强制你显式写成d.Animal.Log()或d.Logger.Log() - 嵌入不传递生命周期控制权:你得自己确保
Animal字段已初始化,否则dog.Run()panic
接口满足是隐式的,但契约必须完整
Go 中类型是否满足 io.Reader,只看它有没有实现 Read(p []byte) (n int, err error),不需要 implements io.Reader 声明。这种隐式满足带来自由,也带来责任。
- 新增方法不破坏接口满足性:给
FileLogger加个Rotate(),它仍满足Logger接口 - 但删掉或改签名就会立刻不满足——没有运行时 fallback,编译直接失败
- 接口越小越好:
type Validator interface { Validate(string) bool }比type Service interface { Validate(...); Process(...); Close(...) }更易实现、替换和测试
组合暴露依赖,继承隐藏依赖
当你看到 type Server struct { DB *sql.DB; Cache cache.Interface },就知道它依赖什么、怎么初始化、哪里可以注入 mock。而继承关系里,DB 是藏在父类构造逻辑里的黑盒。
- 测试时可直接传入
&mockDB{},不用绕过父类初始化流程 - 运行时可动态换组件:把
redis.Client换成ristretto.Cache,只要都实现了cache.Interface,上层代码零改动 - 但别把组合当“省事”:嵌入
Logger后,Server就承担了它的初始化和关闭责任,漏掉logger.Close()就可能泄露资源
真正容易被忽略的不是语法怎么写,而是字段声明即契约——每个嵌入、每个接口字段,都在回答“谁负责初始化”“谁负责销毁”“谁保证线程安全”。组合不是更简单,是把隐含假设全摊开到结构体定义里,由你亲手确认。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











