Skip to content

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 的面向对象,本质是“组合能力而不是继承层级”。

你可以按这条路线实践:

  1. 先用 struct 建模数据。
  2. 再用方法维护状态与行为。
  3. 把可替换能力抽象成小接口。
  4. 用组合组织模块,而不是追求复杂继承树。

这样写出来的 Go 代码,通常更简单、更稳定,也更容易在团队中长期维护。

上次更新于: