如果你之前学过 Java、C++ 或 Python 等面向对象语言,可能会好奇:Go 语言有面向对象吗?答案是:有,但不太一样。
Go 语言没有 class 关键字,也没有 extends 继承语法,但它通过结构体(struct)、**方法(method)和接口(interface)**实现了面向对象编程的核心思想。
本文将从零开始,用大量示例带你理解 Go 语言中的面向对象编程。
在开始之前,我们先理解一下面向对象编程(Object-Oriented Programming,简称 OOP)到底是什么。
想象你在玩一个角色扮演游戏:
面向对象编程就是把现实世界的事物抽象成"对象",每个对象都有自己的属性和行为。
面向对象编程有三大核心特性,我们先用通俗的话理解一下:
| 特性 | 通俗解释 | 生活例子 |
|---|---|---|
| 封装 | 把东西包起来,只暴露必要的部分 | 手机的内部零件被外壳包住,你只能通过按钮操作 |
| 继承 | 子类拥有父类的特征,还可以扩展 | 儿子继承了父亲的姓氏,但有自己的名字 |
| 多态 | 同样的操作,不同的对象有不同的表现 | 按下遥控器,电视换台,空调调温 |
下面我们逐一学习 Go 语言如何实现这三大特性。
封装就是把数据(属性)和操作数据的方法包装在一起,并控制外部的访问权限。
为什么要封装?
Go 语言通过首字母大小写来控制访问权限:
| 访问权限 | 命名规则 | 可见范围 |
|---|---|---|
| 公开的 | 首字母大写 | 所有包都可以访问 |
| 私有的 | 首字母小写 | 只能在当前包内访问 |
注意:Go 只有公开和私有两种访问级别,没有 protected(受保护)级别。
假设我们要创建一个"人"的对象,包含姓名和年龄。姓名可以公开,但年龄需要保护(不能随意设置负数)。
第一步:定义结构体和构造函数
package person
type Person struct {
Name string // 公开字段:首字母大写,任何包都可以访问
age int // 私有字段:首字母小写,只能在 person 包内访问
}
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
age: age,
}
}
代码解释:
Person 是结构体名称,首字母大写表示公开Name 字段首字母大写,表示公开字段age 字段首字母小写,表示私有字段NewPerson 是一个"构造函数"(Go 没有真正的构造函数,这是一种约定俗成的写法)第二步:提供访问私有字段的方法
func (p *Person) GetAge() int {
return p.age
}
func (p *Person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
}
}
代码解释:
GetAge() 方法用于获取年龄(Getter)SetAge() 方法用于设置年龄(Setter),并添加了验证逻辑SetAge 中,我们限制了年龄必须在 0-150 之间,这就是封装的好处:可以在设置数据时进行验证第三步:使用 Person 结构体
package main
import (
"fmt"
"your-module/person"
)
func main() {
p := person.NewPerson("张三", 25)
fmt.Println(p.Name)
fmt.Println(p.GetAge())
p.SetAge(26)
fmt.Println(p.GetAge())
p.SetAge(-10)
fmt.Println(p.GetAge())
// fmt.Println(p.age)
}
输出结果:
张三
25
26
26
| 好处 | 说明 |
|---|---|
| 数据保护 | 通过 SetAge 方法,防止设置无效的年龄 |
| 隐藏实现 | 外部不知道 age 是如何存储的 |
| 灵活修改 | 未来可以改变 age 的存储方式,不影响外部代码 |
在传统面向对象语言中,继承是指一个类可以"继承"另一个类的属性和方法。比如"狗"继承"动物",就自动拥有了"动物"的所有特征。
Go 语言的设计者认为继承有很多问题:
所以 Go 语言选择用组合来代替继承。组合就是"把一个结构体嵌入到另一个结构体中"。
记住一句话:组合优于继承。这是 Go 语言的设计哲学。
让我们用组合的方式来实现"狗是一种动物"这个关系。
第一步:定义 Animal 结构体
package main
import "fmt"
type Animal struct {
Name string
Age int
}
func (a *Animal) Eat() {
fmt.Printf("%s 正在吃东西\n", a.Name)
}
func (a *Animal) Sleep() {
fmt.Printf("%s 正在睡觉\n", a.Name)
}
代码解释:
Animal 结构体,包含 Name 和 Age 两个属性Eat() 和 Sleep() 两个方法第二步:定义 Dog 结构体(嵌入 Animal)
type Dog struct {
Animal
Breed string
}
func (d *Dog) Bark() {
fmt.Printf("%s 正在汪汪叫\n", d.Name)
}
代码解释:
Dog 结构体中嵌入了 Animal(注意:没有字段名,只有类型)Breed 是狗特有的属性(品种)Bark() 是狗特有的方法(叫)第三步:使用 Dog 结构体
func main() {
dog := Dog{
Animal: Animal{
Name: "旺财",
Age: 3,
},
Breed: "金毛",
}
dog.Eat()
dog.Sleep()
dog.Bark()
fmt.Printf("名字: %s, 年龄: %d, 品种: %s\n", dog.Name, dog.Age, dog.Breed)
}
输出结果:
旺财 正在吃东西
旺财 正在睡觉
旺财 正在汪汪叫
名字: 旺财, 年龄: 3, 品种: 金毛
神奇的地方:
dog.Eat() - Dog 没有 Eat 方法,但可以调用 Animal 的 Eat 方法dog.Name - Dog 没有 Name 字段,但可以直接访问 Animal 的 Name 字段这就是 Go 语言的结构体嵌套带来的便利,看起来像继承,但本质是组合。
| 对比项 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a(是一种) | has-a(有一个) |
| 耦合度 | 高(子类依赖父类) | 低(可以灵活组合) |
| 灵活性 | 编译时确定 | 运行时可变 |
| Go 支持 | 不支持 | 支持 |
传统继承中,一个类只能继承一个父类(单继承)。但组合可以实现"多重继承"的效果。
type Flyable struct{}
func (f *Flyable) Fly() {
fmt.Println("正在飞翔")
}
type Swimmable struct{}
func (s *Swimmable) Swim() {
fmt.Println("正在游泳")
}
type Duck struct {
Animal
Flyable
Swimmable
}
func main() {
duck := Duck{
Animal: Animal{Name: "小鸭子"},
}
duck.Eat()
duck.Fly()
duck.Swim()
}
输出结果:
小鸭子 正在吃东西
正在飞翔
正在游泳
代码解释:
Duck 结构体嵌入了三个结构体:Animal、Flyable、Swimmable多态是指:同一个接口,不同的实现。
举个例子:按下"播放"按钮,MP3 播放器播放音乐,DVD 播放器播放视频。同样的"播放"操作,不同的设备有不同的表现。
接口是一组方法的集合,用来定义"对象应该做什么",而不是"对象是什么"。
第一步:定义接口
type Shape interface {
Area() float64
Perimeter() float64
}
代码解释:
Shape 是一个接口,定义了两个方法:Area()(计算面积)和 Perimeter()(计算周长)Shape 接口implements第二步:实现接口(矩形)
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
代码解释:
Rectangle 结构体实现了 Area() 和 Perimeter() 方法Rectangle 自动实现了 Shape 接口第三步:实现接口(圆形)
import "math"
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
代码解释:
Circle 结构体也实现了 Area() 和 Perimeter() 方法Circle 也自动实现了 Shape 接口第四步:使用多态
func PrintShapeInfo(s Shape) {
fmt.Printf("面积: %.2f\n", s.Area())
fmt.Printf("周长: %.2f\n", s.Perimeter())
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
circle := Circle{Radius: 2.5}
fmt.Println("矩形信息:")
PrintShapeInfo(rect)
fmt.Println("\n圆形信息:")
PrintShapeInfo(circle)
}
输出结果:
矩形信息:
面积: 15.00
周长: 16.00
圆形信息:
面积: 19.63
周长: 15.71
多态的威力:
PrintShapeInfo 函数接收 Shape 接口类型Rectangle 时,调用矩形的面积和周长计算方法Circle 时,调用圆形的面积和周长计算方法func main() {
shapes := []Shape{
Rectangle{Width: 5, Height: 3},
Circle{Radius: 2.5},
Rectangle{Width: 10, Height: 4},
}
for i, s := range shapes {
fmt.Printf("图形 %d - 面积: %.2f, 周长: %.2f\n", i+1, s.Area(), s.Perimeter())
}
}
输出结果:
图形 1 - 面积: 15.00, 周长: 16.00
图形 2 - 面积: 19.63, 周长: 15.71
图形 3 - 面积: 40.00, 周长: 28.00
代码解释:
[]Shape 是一个接口切片,可以存储任何实现了 Shape 接口的对象让我们用一个完整的案例来综合运用封装、组合和多态。
package main
import "fmt"
type Employee struct {
id int
name string
salary float64
}
func NewEmployee(id int, name string, salary float64) *Employee {
return &Employee{
id: id,
name: name,
salary: salary,
}
}
func (e *Employee) GetInfo() string {
return fmt.Sprintf("ID: %d, 姓名: %s, 薪资: %.2f", e.id, e.name, e.salary)
}
func (e *Employee) CalculateBonus() float64 {
return e.salary * 0.1
}
type Manager struct {
Employee
teamSize int
}
func NewManager(id int, name string, salary float64, teamSize int) *Manager {
return &Manager{
Employee: Employee{
id: id,
name: name,
salary: salary,
},
teamSize: teamSize,
}
}
func (m *Manager) CalculateBonus() float64 {
return m.salary*0.2 + float64(m.teamSize)*1000
}
func (m *Manager) GetInfo() string {
return fmt.Sprintf("%s, 团队人数: %d", m.Employee.GetInfo(), m.teamSize)
}
type BonusCalculator interface {
CalculateBonus() float64
}
func PrintBonus(b BonusCalculator) {
fmt.Printf("奖金: %.2f\n", b.CalculateBonus())
}
func main() {
emp := NewEmployee(1, "张三", 10000)
mgr := NewManager(2, "李四", 20000, 5)
fmt.Println(emp.GetInfo())
PrintBonus(emp)
fmt.Println()
fmt.Println(mgr.GetInfo())
PrintBonus(mgr)
}
输出结果:
ID: 1, 姓名: 张三, 薪资: 10000.00
奖金: 1000.00
ID: 2, 姓名: 李四, 薪资: 20000.00, 团队人数: 5
奖金: 4500.00
封装体现:
Employee 的字段都是小写(私有),外部无法直接访问NewEmployee 构造函数创建对象GetInfo() 方法获取信息组合体现:
Manager 嵌入了 Employee,自动拥有员工的所有属性和方法Manager 可以直接访问 Employee 的方法:m.Employee.GetInfo()多态体现:
BonusCalculator 接口定义了 CalculateBonus() 方法Employee 和 Manager 都实现了这个接口PrintBonus 函数可以接收任何实现了 BonusCalculator 的对象组合比继承更灵活,可以在运行时改变对象的行为。
type Writer interface {
Write([]byte) (int, error)
}
type FileWriter struct{}
func (fw *FileWriter) Write(data []byte) (int, error) {
fmt.Printf("写入文件: %s\n", string(data))
return len(data), nil
}
type NetworkWriter struct{}
func (nw *NetworkWriter) Write(data []byte) (int, error) {
fmt.Printf("写入网络: %s\n", string(data))
return len(data), nil
}
type Logger struct {
writer Writer
}
func (l *Logger) Log(message string) {
l.writer.Write([]byte(message))
}
func main() {
fileLogger := Logger{writer: &FileWriter{}}
fileLogger.Log("文件日志")
networkLogger := Logger{writer: &NetworkWriter{}}
networkLogger.Log("网络日志")
}
代码解释:
Logger 包含一个 Writer 接口类型的字段Logger 时决定使用 FileWriter 还是 NetworkWriter定义小接口,让代码更灵活。Go 标准库中很多接口只有 1-2 个方法。
type Reader interface {
Read() string
}
type Writer interface {
Write(string)
}
type ReadWriter interface {
Reader
Writer
}
代码解释:
Reader 只有一个方法,接口很小Writer 也只有一个方法ReadWriter 组合了 Reader 和 Writer每个结构体只负责一个功能。
type User struct {
ID int
Name string
}
type UserRepository struct{}
func (r *UserRepository) Save(user *User) error {
fmt.Printf("保存用户: %s\n", user.Name)
return nil
}
type UserService struct {
repo *UserRepository
}
func (s *UserService) CreateUser(name string) *User {
user := &User{Name: name}
s.repo.Save(user)
return user
}
代码解释:
User 只负责存储用户数据UserRepository 只负责数据库操作UserService 只负责业务逻辑没有。但我们可以约定使用 NewXxx() 函数作为构造函数:
func NewPerson(name string, age int) *Person {
return &Person{Name: name, age: age}
}
没有。Go 语言有垃圾回收机制,不需要手动释放内存。如果需要在对象销毁前执行一些操作,可以使用 defer。
type Manager struct {
Employee
}
func (m *Manager) GetInfo() string {
return m.Employee.GetInfo() + ", 是经理"
}
可以,接口嵌套是组合多个小接口的常用方式:
type ReadWriter interface {
Reader
Writer
}
| 特性 | 传统 OOP | Go 语言 |
|---|---|---|
| 类 | class 关键字 | struct 结构体 |
| 继承 | extends 关键字 | 结构体嵌套(组合) |
| 多态 | 继承 + 重写 | 接口实现 |
| 封装 | public/private/protected | 大小写控制 |
| 构造函数 | 类名同名方法 | NewXxx() 工厂函数 |
| this/self | 关键字 | 接收者变量(可自定义名称) |
| 接口实现 | 显式声明 implements | 隐式实现 |
Go 语言的设计理念是简单实用,通过组合和接口实现了面向对象的核心功能,同时避免了传统继承带来的复杂性和耦合问题。