Go 语言没有 class,但它通过结构体(struct) + 方法(method) + 接口(interface)这套组合,实现了一种非常务实、轻量级的面向对象风格。
下面从最基础讲起,一步步说明 Go 是如何“面向对象”的(重点放在结构体与方法的部分)。
1. 结构体 ≈ 数据 + 状态(类似 OOP 的“字段/属性”)
type Person struct {
Name string
Age int
Gender string // 小写开头 → 包外不可见(封装)
}
- 这是最基本的“对象数据载体”
- Go 不强制要求字段一定要小写(private),但习惯上用小写开头实现封装
2. 方法 = 绑定到结构体的函数(Receiver)
Go 的方法写法非常独特:函数名前加一个“接收者”参数。
// 值接收者(最常见,拷贝一份数据)
func (p Person) Greet() string {
return "你好,我是 " + p.Name
}
// 指针接收者(能修改原对象)
func (p *Person) Birthday() {
p.Age++ // 真正修改了调用者的 Age
}
使用方式:
func main() {
alice := Person{Name: "Alice", Age: 25}
// 值接收者:两种写法都行,Go 会自动解引用
fmt.Println(alice.Greet()) // 你好,我是 Alice
fmt.Println((&alice).Greet()) // 也行
// 指针接收者:通常需要 & 取地址
alice.Birthday() // Age 变成 26
(&alice).Birthday() // 也行
fmt.Println(alice.Age) // 27
}
3. 值接收者 vs 指针接收者 —— 这是 Go 最容易踩坑的地方
| 特性 | 值接收者 (p Person) | 指针接收者 (p *Person) | 推荐场景 |
|---|---|---|---|
| 是否拷贝结构体 | 是(拷贝一份) | 否(只拷贝指针,8字节) | — |
| 能否修改原对象 | 不能(改的是副本) | 能 | 需要修改状态时必须用指针 |
| 方法调用方式 | 值 / 指针 都可以调用 | 只有指针能调用(值调用会编译错) | — |
| 并发安全性 | 天然安全(操作副本) | 需要自己加锁 | 小结构体、无状态变化 → 值接收者 |
| 性能(大结构体) | 拷贝开销大 | 几乎无拷贝开销 | 结构体 > 几十字节 → 优先考虑指针 |
| 一致性原则(官方建议) | — | — | 同一个类型的所有方法尽量统一用一种接收者 |
官方经典建议(Effective Go & Go Tour):
“对一个类型的所有方法,要么都用值接收者,要么都用指针接收者,不要混用。”
最常见的现实选择规律(2025–2026 社区共识):
- 小结构体(< 16–32 字节)、不可变、纯计算 → 值接收者
- 需要修改状态、包含 mutex、slice/map 等会增长的字段 → 指针接收者(占主流,约 70–80%)
- 混合类型(例如 String() 方法常用值接收者,其余用指针)→ 可以接受,但尽量避免
4. 完整的“类”风格示例(带构造函数 + 方法)
package main
import "fmt"
// "类":推荐首字母大写(导出)
type Rectangle struct {
width float64
height float64
}
// 构造函数(Go 没有 new 关键字强制要求,习惯用 NewXxx)
func NewRectangle(w, h float64) *Rectangle {
if w <= 0 || h <= 0 {
panic("宽度和高度必须 > 0")
}
return &Rectangle{width: w, height: h}
}
// 值接收者方法(只读)
func (r Rectangle) Area() float64 {
return r.width * r.height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.width + r.height)
}
// 指针接收者方法(修改状态)
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
// 实现 Stringer 接口(类似 toString)
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle{%.1f × %.1f}", r.width, r.height)
}
func main() {
rect := NewRectangle(10, 5)
fmt.Println(rect) // Rectangle{10.0 × 5.0}
fmt.Printf("面积: %.2f\n", rect.Area()) // 50.00
rect.Scale(2)
fmt.Println(rect) // Rectangle{20.0 × 10.0}
fmt.Printf("周长: %.2f\n", rect.Perimeter()) // 60.00
}
5. Go 的 OOP 与传统语言对比(快速记忆表)
| 特性 | Java/C#/C++ | Go | Go 的实现方式 |
|---|---|---|---|
| 类 | class | struct | — |
| 构造器 | constructor | 普通函数(NewXxx) | 习惯约定 |
| 方法 | member function | receiver function | (t T) 或 (t *T) |
| 继承 | extends | 不支持,改用嵌入 | 匿名嵌入结构体 |
| 多态 | override | 接口(duck typing) | 隐式实现接口 |
| 封装 | private/protected | 小写字段/方法 | 包级可见性 |
| this/self | this / self | 接收者名字(习惯用 p、r、this) | 随便起名 |
总结一句话:
Go 的“面向对象”本质是:用 struct 存数据,用方法(带 receiver)绑行为,用接口做多态,用嵌入做组合/“伪继承”。
它故意去掉了传统 OOP 最复杂、最容易滥用的部分(类继承、protected、super、构造器链、final 等),换来的是更简单、可预测、高性能的代码。
你现在是用值接收者多一些,还是指针接收者占主流?
或者你在项目里遇到过最纠结的 receiver 选择场景是什么?可以聊聊~