Go 的面向对象:结构体、方法、接口与组合
很多同学第一次写 Go,会问一个问题:Go 没有 class,它到底怎么做面向对象?
答案是:Go 的面向对象不是“类继承”那一套,而是用这四个核心工具完成建模:
struct:承载数据method:绑定行为interface:定义能力embedding(组合):复用能力
如果你把 OOP 理解为“组织数据和行为、隔离变化、降低耦合”,GO 则是遵从组合优于继承的思想。
1. 结构体:对象的载体
结构体是 Go 中最常见的建模方式,用来组织一组相关字段。
1.1 定义与初始化
go
type User struct {
ID int64
Name string
Email string
}
func main() {
u1 := User{ID: 1, Name: "alice", Email: "a@test.com"}
u2 := User{2, "bob", "b@test.com"} // 不推荐,字段顺序容易出错
_ = u2
fmt.Println(u1)
}推荐使用“带字段名”的初始化,可读性和可维护性都更好。
1.2 结构体是值类型
go
func rename(u User) {
u.Name = "changed"
}
func main() {
u := User{ID: 1, Name: "alice"}
rename(u)
fmt.Println(u.Name) // alice
}这里 rename 收到的是副本。想修改原对象,要传指针。
2. 方法:把行为绑定到类型
方法本质是“带接收者参数的函数”。
2.1 值接收者与指针接收者
go
type Counter struct {
N int
}
func (c Counter) Value() int { // 值接收者:通常用于只读行为
return c.N
}
func (c *Counter) Inc() { // 指针接收者:用于修改状态
c.N++
}经验法则:
- 方法需要修改状态,用指针接收者
- 结构体较大,优先指针接收者,避免频繁拷贝
- 同一类型的方法接收者风格尽量统一
2.2 方法集与接口实现
如果某接口要求的方法是由指针接收者实现的,那么只有 *T 实现该接口,T 不实现。
go
type Incer interface {
Inc()
}
func use(i Incer) {
i.Inc()
}
func main() {
c := Counter{}
use(&c) // 正确
// use(c) // 编译错误:Counter 没有满足 Incer 的方法集
}3. 接口:面向能力编程
Go 的接口是隐式实现,不需要 implements 关键字。
3.1 小接口优先
go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}接口越小,复用越高。这个设计在标准库里非常常见。
3.2 多态的实际效果
go
type Notifier interface {
Notify(msg string) error
}
type EmailSender struct{}
func (EmailSender) Notify(msg string) error {
fmt.Println("email:", msg)
return nil
}
type SmsSender struct{}
func (SmsSender) Notify(msg string) error {
fmt.Println("sms:", msg)
return nil
}
func Send(n Notifier, msg string) error {
return n.Notify(msg)
}调用方只依赖 Notifier,新增通知渠道时,不需要改调用逻辑。
3.3 常见坑:interface{} 里包着 nil 指针
go
var p *User = nil
var i interface{} = p
fmt.Println(i == nil) // false接口值由“动态类型 + 动态值”组成,只要动态类型不为空,接口就不等于 nil。
4. 组合优于继承
Go 没有传统继承,但可以通过嵌入(embedding)实现能力复用。
4.1 嵌入结构体
go
type Logger struct{}
func (Logger) Log(msg string) {
fmt.Println("[LOG]", msg)
}
type OrderService struct {
Logger
}
func (s OrderService) Create() {
s.Log("create order") // 直接提升调用
}OrderService 没有继承 Logger,但通过组合获得了日志能力。
4.2 组合的价值
- 避免继承层级过深
- 行为按需拼装,更灵活
- 更容易测试与替换依赖
5. 封装:用可见性控制边界
Go 没有 public/private 关键字,而是靠首字母大小写控制导出。
- 大写开头:包外可见(导出)
- 小写开头:包内可见(不导出)
go
type account struct { // 包内使用
balance int64
}
func NewAccount(init int64) *account {
if init < 0 {
init = 0
}
return &account{balance: init}
}
func (a *account) Deposit(v int64) {
if v > 0 {
a.balance += v
}
}这种写法能把“不变量”收敛在方法内部,避免外部随意破坏状态。
6. 一页速记:Go 风格 OOP
| 主题 | 结论 | 实战建议 |
|---|---|---|
| 建模 | 用 struct 表达数据 | 初始化优先字段名写法 |
| 行为 | 用 method 绑定类型 | 修改状态用指针接收者 |
| 抽象 | 用 interface 表达能力 | 保持小接口,按需定义 |
| 复用 | 用组合代替继承 | 通过 embedding 提升能力 |
| 封装 | 靠导出规则控制边界 | 对外暴露构造函数和方法 |
7. 小结
Go 的面向对象,本质是“组合能力而不是继承层级”。
你可以按这条路线实践:
- 先用
struct建模数据。 - 再用方法维护状态与行为。
- 把可替换能力抽象成小接口。
- 用组合组织模块,而不是追求复杂继承树。
这样写出来的 Go 代码,通常更简单、更稳定,也更容易在团队中长期维护。