第五章:Go的“面向对象”编程
上一章:《第四章:基础语法》
文章目录
- 1.核心概念:与Java的初步对比
- 2.结构体(Struct):替代类的数据载体
- 2.1 定义与初始化
- 2.2 访问与修改字段
- 3.方法(Method):为类型添加行为
- 4.封装与可见性:命名的大小写规则
- 5.“继承”与组合:类型嵌入
- 6.接口(Interface):多态的灵魂
- 6.1 接口的定义与实现
- 6.2 空接口与类型断言
- 7.实战案例:一个简单的图形计算程序
对于来自Java等传统OOP语言的开发者来说,这一章将是思维转换的关键。
Go语言的OOP没有“类”(class)的概念,而是通过结构体(struct)、方法(method) 和接口(interface) 以一种更简洁、更灵活的方式来实现。让我们来深入探索。
1.核心概念:与Java的初步对比
在开始具体语法之前,了解Go语言OOP的设计哲学至关重要。下表清晰地展示了Go与Java在OOP核心概念上的主要区别:
特性 | Java (传统OOP) | Go (简化OOP) |
---|---|---|
基本构建块 | 类(Class) | 结构体(Struct) |
封装 | public, private, protected等访问修饰符 | 字母大小写(大写导出,小写私有) |
方法定义 | 在类内部定义 | 与类型关联的接收者(Receiver) |
继承 | extends关键字(单继承) | 通过组合(Composition)和类型嵌入 |
多态 | 通过implements显式实现接口 | 通过接口隐式实现(鸭子类型) |
核心原则 | 一切皆对象,围绕类构建 | 组合优于继承,面向接口编程 |
核心思维转换:在Go中,请暂时忘记“类”和“继承”。思考的重点将变为“组合” 和“行为”。一个类型只要实现了接口的所有方法,它就自动实现了该接口,无需显式声明
2.结构体(Struct):替代类的数据载体
结构体是Go中组合不同类型数据以形成新类型的核心方式,相当于类的数据部分
2.1 定义与初始化
// 定义一个Person结构体(类似一个简单的类)
type Person struct {Name string // 字段(属性)Age int
}func main() {// 多种初始化方式// 1. 声明后赋值var p1 Personp1.Name = "Alice"p1.Age = 30// 2. 使用结构体字面量(推荐)p2 := Person{Name: "Bob", // 显式指定字段名,顺序可变Age: 25,}// 3. 按字段顺序初始化(不推荐,易错)p3 := Person{"Charlie", 28}fmt.Println(p1, p2, p3)
}
2.2 访问与修改字段
使用点号(.)操作符
p2.Name = "Bobby" // 修改字段值
fmt.Println(p2.Name) // 访问字段值
3.方法(Method):为类型添加行为
在Go中,方法不是定义在结构体内部,而是通过与特定类型绑定来实现的。这个绑定通过接收者(Receiver) 完成
func sayHello(p Person) {fmt.Printf("Hello, my name is %s,I am %d years old\n", p.name, p.age)
}
func addAge(p *Person) {p.age++
}func (p *Person) addAge1() {p.age++
}func main() {person := Person{name: "张三",age: 20,}sayHello(person)addAge(&person)sayHello(person)person.addAge1()sayHello(person)if person.age > 18 {fmt.Println("adult")}
}
- 值接收者(如 (p Person)):当方法不需要修改接收者的字段,或者操作结构体的副本即可时使用。适用于小型结构体或基础类型。
- 指针接收者(如 (p *Person)):当方法需要修改接收者的字段,或者结构体很大为避免复制的开销时使用。在需要修改接收者状态时,必须使用指针接收者
4.封装与可见性:命名的大小写规则
Go没有访问修饰符关键字。它通过标识符(类型名、字段名、方法名)的首字母大小写来控制可见性。
- 首字母大写:公开的(Public),可以被其他包中的代码访问。
- 首字母小写:包内私有的(Private),只能在当前包内访问。
// person.go (在包mylib中)
type Person struct { // 类型名大写,其他包可访问Name string // 字段名大写,其他包可访问和修改age int // 字段名小写,仅在mylib包内可访问
}// 方法名大写,其他包可调用
func (p *Person) SetAge(newAge int) {if newAge > 0 && newAge < 150 { // 可在此添加验证逻辑p.age = newAge}
}// 方法名小写,仅在mylib包内可调用
func (p *Person) validateAge(a int) bool {return a > 0
}
这种基于命名规则的封装鼓励你设计清晰的公开API,并将实现细节隐藏起来
上面只是一个简单的书写示例,下面我们新增一个包person,并添加person.go文件,同时在person包外部新增test_visibility.go来验证可见性
包结构:
go_ai_agent/ # 项目根目录
├── person/ # person包目录
│ └── person.go # 包名:package person
├── test_visibility.go # 主程序文件,包名:package main
package person
// Person 结构体演示真正的包外可见性
type Person struct {Name string // 公开字段age int // 私有字段
}// NewPerson 构造函数
func NewPerson(name string, age int) *Person {return &Person{Name: name,age: age,}
}// GetAge 获取年龄的公开方法
func (p *Person) GetAge() int {return p.age
}// SetAge 设置年龄的公开方法
func (p *Person) SetAge(age int) {p.age = age
}// UpdateName 更新姓名的公开方法
func (p *Person) UpdateName(name string) {p.Name = name
}// updateName 更新姓名的私有方法
func (p *Person) updateName(name string) {p.Name = name
}
- 目录名:person
- 包名:package person(通常与目录名相同)
这里其实已经有面向对象的概念了,通过公开的方法设置对象的私有属性值是我们Java中常做的事情
package mainimport ("fmt""go_ai_agent/person" // 导入person包
)func main() {fmt.Println("=== 真正的包外可见性测试 ===")// 创建Person实例p := person.NewPerson("张三", 25)// 测试公开字段和方法fmt.Println("姓名:", p.Name) // ✅ 可以访问p.UpdateName("李四") // ✅ 可以调用fmt.Println("更新后姓名:", p.Name)// 测试私有字段和方法// fmt.Println("年龄:", p.age) // ❌ 编译错误:cannot refer to unexported field 'age'// p.updateName("王五") // ❌ 编译错误:cannot refer to unexported method 'updateName'// 通过公开方法访问私有字段fmt.Println("年龄:", p.GetAge()) // ✅ 通过公开方法访问p.SetAge(30) // ✅ 通过公开方法修改fmt.Println("更新后年龄:", p.GetAge())fmt.Println("\n=== 总结 ===")fmt.Println("✅ 大写开头的字段和方法:包外可见")fmt.Println("❌ 小写开头的字段和方法:包外不可见")fmt.Println("💡 通过公开方法可以间接访问私有字段")
}
5.“继承”与组合:类型嵌入
Go推崇组合优于继承。它通过类型嵌入(在结构体中嵌入匿名结构体)来实现代码复用,这有时被称为“匿名组合”
// 基础结构体
type Animal struct {Name string
}// 为Animal定义方法
func (a Animal) Speak() {fmt.Println(a.Name, "发出声音")
}// Dog 组合了 Animal(类似于Dog继承自Animal)
type Dog struct {Animal // 类型嵌入(匿名字段)。Dog“拥有”Animal的所有字段和方法Breed string
}// 可以重写(Override)嵌入类型的方法
func (d Dog) Speak() {fmt.Println(d.Name, "在汪汪叫!")
}func main() {dog := Dog{Animal: Animal{Name: "旺财"}, // 初始化嵌入的结构体Breed: "柯基",}dog.Speak() // 输出:旺财 在汪汪叫! (调用Dog自己的方法)dog.Animal.Speak() // 输出:旺财 发出声音 (显式调用嵌入Animal的方法)// Dog可以直接访问Animal的字段fmt.Println("狗的名字是:", dog.Name)
}
通过上面这个简单的例子我们可以发现,所谓类型嵌入(匿名组合),就是:
- 在结构体中嵌入另一个结构体,但不给字段名
- 嵌入的结构体称为"匿名字段"
- 外层结构体自动获得内层结构体的所有字段和方法
方法提升(Method Promotion):
- 嵌入结构体的方法自动提升到外层结构体
- 可以直接调用:dog.Eat() 而不是 dog.Animal.Eat()
- 也可以显式调用:dog.Animal.Eat()
多重嵌入:
- 可以嵌入多个结构体
- 支持链式嵌入:A嵌入B,B嵌入C
6.接口(Interface):多态的灵魂
接口是Go语言实现多态的核心,也是Go最强大的特性之一。它的设计哲学是鸭子类型(Duck Typing):“如果某个东西走起来像鸭子,叫起来像鸭子,那么它就可以被当作鸭子”
6.1 接口的定义与实现
// 定义一个Speaker接口
type Speaker interface {Speak() string // 只声明方法签名,不实现
}// Person 类型实现了 Speaker 接口(隐式实现)
func (p Person) Speak() string {return "我是" + p.Name
}// Cat 类型也实现了 Speaker 接口
type Cat struct {Name string
}func (c Cat) Speak() string {return c.Name + "说:喵喵喵"
}// 多态的体现:一个函数可以处理任何实现了Speaker接口的类型
func Introduce(s Speaker) {fmt.Println(s.Speak())
}func main() {p := Person{Name: "小明"}c := Cat{Name: "咪咪"}Introduce(p) // 输出:我是小明Introduce(c) // 输出:咪咪说:喵喵喵// 接口变量可以持有任何实现该接口的值var sp Speakersp = pfmt.Println(sp.Speak())sp = cfmt.Println(sp.Speak())
}
按照我们正常的书写习惯,我们应该先定一个service层,专用用于存放接口,并针对person和cat实现Speak()方法
目录结构如下:
go_ai_agent/ # 项目根目录
├── go.mod # Go模块文件 (module go_ai_agent)
├── dto/ # dto包目录
│ ├── cat.go # 包名:package dto
│ └── person.go # 包名:package dto
├── service/ # service包目录
│ └── speak.go # 包名:package service
├── interface_demo.go # 主程序文件,包名:package main
- speakService.go
package service// Speaker 接口定义了说话的能力
type Speaker interface {Speak() string // 说话方法,返回说话内容
}
- person.go
package dtoimport "fmt"type Person struct {Name stringage int
}// 构造函数
func NewPerson(name string, age int) *Person {return &Person{Name: name,age: age,}
}func (p *Person) SetAge(age int) {p.age = age
}func (p *Person) GetAge() int {return p.age
}func (p *Person) SetName(name string) {p.Name = name
}func (p *Person) GetName() string {return p.Name
}// 实现Speaker接口的Speak方法
func (p *Person) Speak() string {return fmt.Sprintf("你好,我是:%s", p.Name)
}
- cat.go
package dtoimport "fmt"type Cat struct {Name string
}func NewCat(name string) *Cat {return &Cat{Name: name,}
}func (cat *Cat) GetName() string {return cat.Name
}func (cat *Cat) SetName(name string) {cat.Name = name
}// 实现Speaker接口的Speak方法
func (cat *Cat) Speak() string {return fmt.Sprintf("%s 说:喵喵喵!", cat.Name)
}
下面新增一个测试类:
- interface_demo.go
package mainimport ("fmt""go_ai_agent/dto""go_ai_agent/service"
)func introduce(s service.Speaker) {fmt.Println(s.Speak())
}
func main() {p := dto.NewPerson("Alice", 25)c := dto.NewCat("Whiskers")introduce(p)introduce(c)
}
在Go中,实现接口是隐式的。一个类型不需要像Java那样用implements关键字声明它要实现某个接口。它只需要实现了接口中规定的所有方法,那么就自动实现了该接口。这种设计极大地降低了耦合度,使得代码非常灵活
6.2 空接口与类型断言
空接口interface{}不包含任何方法,因此所有类型都实现了空接口。这常常用于需要处理未知类型数据的场景,类似于Java中的Object
// 可以接受任何类型的参数
func describe(i interface{}) {fmt.Printf("值: %v, 类型: %T\n", i, i)// 使用类型断言来判断接口变量的具体类型if str, ok := i.(string); ok {fmt.Println("这是个字符串,长度是:", len(str))} else if num, ok := i.(int); ok {fmt.Println("这是个整数,乘以2是:", num*2)} else {fmt.Println("是其他类型")}// 更简洁的写法:类型选择(type switch)switch v := i.(type) {case string:fmt.Printf("是字符串: %s\n", v)case int:fmt.Printf("是整数: %d\n", v)default:fmt.Printf("未知类型: %v\n", v)}
}func main() {describe("Hello")describe(42)describe(Person{Name: "Test"})
}
7.实战案例:一个简单的图形计算程序
让我们用一个综合案例来巩固本章知识,实现一个可以计算不同图形面积的程序
大家可以先自己尝试一下,然后在对比一下我实现的差异
目录结构:
geometry/ # 几何图形包(比calArea更专业)
├── shapes/ # 图形定义包
│ ├── circle.go # 圆形
│ ├── rectangle.go # 矩形
│ └── shape.go # 基础图形接口
├── calculator/ # 计算器包
│ └── area.go # 面积计算逻辑
└── examples/ # 使用示例└── main.go
- 基础图形接口(shape.go)
- 每个图形都有自己的名称(三角形,圆,长方形等)
package shapestype Shape struct {Name string
}func NewShape(name string) *Shape {return &Shape{Name: name,}
}
- 面积计算接口(area.go)
package calculatortype ShapeService interface {Name() stringArea() float64Perimeter() float64
}
里面有三个方法,分别是:图形信息,面积以及周长
下面就是每个图形的实现
- 圆(circle.go)
package shapesimport ("math"
)type Circle struct {Shaperadius float64
}func NewCircle(radius float64) *Circle {return &Circle{radius: radius,Shape: *NewShape("圆"),}
}func (c *Circle) GetRadius() float64 {return c.radius
}func (c *Circle) SetRadius(radius float64) {c.radius = radius
}func (c Circle) Area() float64 {return math.Pi * c.radius * c.radius
}func (c Circle) Perimeter() float64 {return math.Pi * c.radius * 2
}// 实现calculator.ShapeService接口
func (c Circle) Name() string {return c.Shape.Name
}
- 矩形(rectangle.go)
package shapestype Rectangle struct {ShapeWidth float64Height float64
}func NewRectangle(width float64, height float64) *Rectangle {return &Rectangle{Width: width,Height: height,Shape: *NewShape("矩形"),}
}func (r Rectangle) Area() float64 {return r.Width * r.Height
}func (r Rectangle) Perimeter() float64 {return 2 * (r.Width + r.Height)
}// 实现calculator.ShapeService接口
func (r Rectangle) Name() string {return r.Shape.Name
}
- 测试工具(main.go)
package mainimport ("fmt""go_ai_agent/geometry/calculator""go_ai_agent/geometry/shapes"
)func getShapeAreaInfo(shape calculator.ShapeService) string {return fmt.Sprintf("面积:%.2f,周长:%.2f", shape.Area(), shape.Perimeter())
}func printShapeAreaInfo(shape calculator.ShapeService) {fmt.Printf("图形:%s,%s\n", shape.Name(), getShapeAreaInfo(shape))
}func main() {circle := shapes.NewCircle(10)printShapeAreaInfo(circle)rectangle := shapes.NewRectangle(10, 20)printShapeAreaInfo(rectangle)
}
PS:大脑被Java已经污染,一时半会写的还有些Java面向对象的设计风格,后面会逐渐纠正
通过上面的这一套设计,我们来感受一下Go语言的接口设计思想以及和Java设计的差异
1.隐式实现 (Implicit Implementation)
// 定义接口
type ShapeService interface {Name() stringArea() float64Perimeter() float64
}// 类型自动实现接口(无需显式声明)
type Circle struct {Shaperadius float64
}func (c Circle) Name() string { return c.Shape.Name }
func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.radius }
// Circle 自动实现了 ShapeService 接口
Java语言:
// 必须显式声明实现接口
public class Circle implements ShapeService {// 必须实现所有接口方法
}
2.“鸭子类型” (Duck Typing)
Go遵循"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子"的原则:
// 只要有这些方法,就是ShapeService
type ShapeService interface {Name() stringArea() float64Perimeter() float64
}// 任何类型只要有这三个方法,就自动实现了接口
Go vs Java 接口设计对比
方面 | Go语言 | Java语言 |
---|---|---|
实现方式 | 隐式实现 | 显式实现 (implements) |
设计原则 | 接口发现 | 接口契约 |
依赖方向 | 使用者定义接口 | 提供者定义接口 |
接口大小 | 小接口(1-3个方法) | 大接口(多个方法) |
组合方式 | 接口嵌入 | 接口继承 |