13 KiB
第 1 章:Go 语言的面向对象编程思想
它没有传统 OOP 语言(如 Java, C++)中的 class、extends 等关键字,但通过 struct、method 和 interface 的组合,为开发者提供了强大而灵活的面向对象编程能力。
在软件开发中,代码的组织遵循着一个自然的进化路径:
- 函数(Function):最初,功能代码被聚合在独立的函数中。
- 结构体(Struct):随着复杂度的增加,相关的变量和状态被组合成结构体,用于封装数据。
- 方法(Method):将操作这些数据的函数(行为)与结构体(数据)绑定,就形成了方法,这已非常接近于传统 OOP 中“类”的概念。
第 2 章:核心构建块:类型、结构体与方法
2.1 type 关键字:定义新类型与类型别名
type 关键字是 Go 中构建自定义数据结构的基础。
- 定义新类型:
type MyInt int- 创建了一个全新的、独立的类型
MyInt。 - 它与
int在类型系统上不兼容,需要显式转换才能混合运算。
- 创建了一个全新的、独立的类型
- 定义类型别名:
type AliasInt = intAliasInt只是int的一个别名,它们是完全相同的类型。- 主要用于提高代码可读性,在 Go 1.9 版本引入。
any就是interface{}的类型别名。
package main
import "fmt"
// 1. 定义一个全新的类型 MyInt
type MyInt int
// 2. 为 int 类型定义一个别名 Number
// 在 Go 1.18 之后,any 就是 interface{} 的类型别名
type Number = int
func main() {
// --- 新类型示例 ---
var a MyInt = 10
var b int = 20
// a 和 b 是不同类型,直接相加会编译错误
// fmt.Println(a + b) // a + b (mismatched types MyInt and int)
// 必须将 MyInt 类型的 a 转换为 int 类型
fmt.Printf("新类型 MyInt 转换为 int 后与 int 相加: %d\n", int(a)+b)
fmt.Printf("类型 a: %T, 类型 b: %T\n", a, b)
fmt.Println("--------------------")
// --- 类型别名示例 ---
var x Number = 30
var y int = 40
// x 和 y 本质上都是 int 类型,可以直接运算
fmt.Printf("类型别名 Number 与 int 直接相加: %d\n", x+y)
fmt.Printf("类型 x: %T, 类型 y: %T\n", x, y)
}
2.2 struct:数据聚合的基石
struct (结构体)是 Go 中聚合零个或多个任意类型字段的数据集合,它是构建复杂数据结构和实现面向对象编程中“数据”部分的基础。
package main
import "fmt"
// 定义一个 Person 结构体,用于封装人的属性
type Person struct {
Name string
Age int
}
func main() {
// 创建并初始化一个 Person 实例
p1 := Person{
Name: "Alice",
Age: 30,
}
fmt.Printf("创建的 Person 实例: %+v\n", p1)
// 访问结构体的字段
fmt.Printf("姓名: %s, 年龄: %d\n", p1.Name, p1.Age)
}
2.3 method:为类型绑定行为
方法(Method)是绑定到特定类型的函数。它将数据(struct)和操作数据的行为(function)结合起来。
- 函数 (Function):
func DoSomething(),独立调用。 - 方法 (Method):
func (t MyType) DoSomething(),通过类型的实例instance.DoSomething()调用。
方法的接收者可以是值类型或指针类型。
package main
import "fmt"
// 定义一个矩形结构体
type Rectangle struct {
width, height float64
}
// 1. 值接收者方法 (Value Receiver)
// 接收者 r 是 Rectangle 的一个副本。
// 方法内部对 r 的修改不会影响原始的 Rectangle 实例。
// 适用于不需要修改原始数据,并且结构体较小的场景。
func (r Rectangle) Area() float64 {
// r.width = 100 // 这里的修改是无效的,因为 r 只是一个副本
return r.width * r.height
}
// 2. 指针接收者方法 (Pointer Receiver)
// 接收者 r 是一个指向 Rectangle 实例的指针。
// 方法内部对 *r 的修改会直接影响原始的 Rectangle 实例。
// 适用于需要修改原始数据,或结构体较大以避免复制开销的场景。
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func main() {
rect := Rectangle{width: 10, height: 5}
// 调用值接收者方法
fmt.Printf("原始矩形: %+v\n", rect)
fmt.Printf("面积: %.2f\n", rect.Area())
// 调用指针接收者方法
// Go 语言会自动进行转换,rect.Scale(2) 等效于 (&rect).Scale(2)
rect.Scale(2)
fmt.Printf("缩放后的矩形: %+v\n", rect)
fmt.Printf("缩放后的面积: %.2f\n", rect.Area())
}
值接收者 vs. 指针接收者选择指南
- 要修改接收者的状态吗?
- 是 -> 必须使用指针接收者 (
*T)。 - 否 -> 可以使用值接收者 (
T)。
- 是 -> 必须使用指针接收者 (
- 性能考虑:
- 如果接收者是一个大型结构体或数组,使用指针接收者可以避免昂贵的内存拷贝。
- 一致性:
- 如果一个类型已经有了指针接收者的方法,那么其他方法也建议使用指针接收者以保持一致。
- 特殊类型:
- 如果类型包含
sync.Mutex或其他不应被复制的同步字段,必须使用指针接收者。
- 如果类型包含
第 3 章:代码复用:组合与内嵌
Go 语言通过结构体内嵌(Embedding)和组合(Composition)实现代码复用,而不是传统的类继承。
3.1 结构体内嵌(模拟继承 - is-a 关系)
通过在结构体中匿名地嵌入另一个结构体类型,可以获得被嵌入结构体的所有字段和方法,这在形式上模拟了“继承”。
package main
import "fmt"
// "父类":动物
type Animal struct {
name string
}
// Animal 的方法
func (a *Animal) Move() {
fmt.Printf("%s is moving.\n", a.name)
}
// "子类":狗
type Dog struct {
Animal // 匿名内嵌 Animal,Dog is an Animal
breed string
}
// Dog 重写了 Move 方法
func (d *Dog) Move() {
fmt.Printf("%s the %s is running happily!\n", d.name, d.breed)
}
// Dog 特有的方法
func (d *Dog) Bark() {
fmt.Printf("%s says: Woof! Woof!\n", d.name)
}
func main() {
// 创建一个 Dog 实例
dog := &Dog{
Animal: Animal{name: "Buddy"},
breed: "Golden Retriever",
}
// 1. 可以直接访问内嵌结构体的字段
fmt.Printf("Dog's name is: %s\n", dog.name)
// 2. 调用 Dog 自己重写的方法 (就近原则)
dog.Move()
// 3. 调用内嵌结构体的原始方法 (需要显式指定)
dog.Animal.Move()
// 4. 调用 Dog 特有的方法
dog.Bark()
}
3.2 组合(has-a 关系)
组合是通过在结构体中包含一个具名的字段来实现的。这表示一个类型“拥有”另一个类型的实例。 Go 语言哲学:优先使用组合而不是继承。
package main
import "fmt"
// 定义引擎结构体
type Engine struct {
horsepower int
}
// 引擎的方法
func (e *Engine) Start() {
fmt.Printf("Engine with %d HP starts. Vroom!\n", e.horsepower)
}
// 定义汽车结构体
type Car struct {
brand string
engine Engine // 具名字段,Car has an Engine
}
func main() {
// 创建一个 Car 实例,它“拥有”一个 Engine
myCar := Car{
brand: "Ferrari",
engine: Engine{
horsepower: 710,
},
}
fmt.Printf("My car is a %s.\n", myCar.brand)
// 访问组合部件的方法需要通过字段名
myCar.engine.Start()
}
第 4 章:多态的基石:接口
接口(Interface)是 Go 实现多态的核心。它定义了一组方法的集合(契约),任何类型只要实现了接口中所有的方法,就被认为是该接口的实现,这个过程是隐式的。
4.1 接口的定义与实现(多态)
多态即“多种形态”。一个变量可以持有实现了同一接口的不同类型的值,并在运行时调用相应类型的方法。
package main
import "fmt"
// 1. 定义一个接口 (契约)
// Shaper 接口定义了一个行为:计算面积
type Shaper interface {
Area() float64
}
// 2. 定义实现该接口的多个结构体
type Circle struct {
radius float64
}
// Circle 实现了 Shaper 接口
func (c Circle) Area() float64 {
return 3.14159 * c.radius * c.radius
}
type Square struct {
side float64
}
// Square 实现了 Shaper 接口
func (s Square) Area() float64 {
return s.side * s.side
}
// 3. 定义一个接收接口类型参数的函数,实现多态
func PrintArea(s Shaper) {
fmt.Printf("This shape is of type %T and its area is %.2f\n", s, s.Area())
}
func main() {
// 创建不同类型的实例
circle := Circle{radius: 5}
square := Square{side: 4}
// 使用多态函数
// 尽管 circle 和 square 是不同类型
// 但它们都满足 Shaper 接口,所以可以被 PrintArea 函数接收
PrintArea(circle)
PrintArea(square)
// 接口变量可以持有任何实现了该接口的类型的值
var shaper Shaper
shaper = circle
fmt.Printf("Shaper variable holding a Circle: Area=%.2f\n", shaper.Area())
shaper = square
fmt.Printf("Shaper variable holding a Square: Area=%.2f\n", shaper.Area())
}
4.2 空接口 (interface{} 或 any)
空接口不包含任何方法,因此所有类型都默认实现了空接口。它可以用来存储任意类型的值。自 Go 1.18 起,any 成为了 interface{} 的官方别名。
package main
import "fmt"
// 这个函数可以接收并打印任何类型的值
func printAnything(v any) { // 使用 any 代替 interface{}
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
printAnything(42)
printAnything("Hello, Go!")
printAnything(true)
printAnything(3.14)
// 在集合类型中非常有用
heterogeneousSlice := []any{"text", 100, false, 2.718}
fmt.Println("\nA slice with mixed types:", heterogeneousSlice)
hetegeneousMap := map[string]any{
"name": "Excalicode",
"version": 1.0,
"active": true,
}
fmt.Println("A map with mixed value types:", hetegeneousMap)
}
4.3 接口断言(Type Assertion)
接口断言用于在运行时检查一个接口类型变量所持有的具体类型,并获取其底层值。
- 安全形式:
value, ok := i.(Type)。如果断言成功,ok为true,value为具体类型的值;如果失败,ok为false,value为该类型的零值。这是推荐的方式。 - 非安全形式:
value := i.(Type)。如果断言失败,程序会发生panic。 - Type Switch:
switch v := i.(type)。用于判断多种可能的类型。
package main
import "fmt"
func process(v any) {
fmt.Printf("\nProcessing value: %v\n", v)
// 1. 安全的类型断言
str, ok := v.(string)
if ok {
fmt.Printf("It's a string! Value: \"%s\"\n", str)
return
}
// 2. 使用 Type Switch 处理多种类型
switch t := v.(type) {
case int:
fmt.Printf("It's an int! Value: %d\n", t)
case bool:
fmt.Printf("It's a bool! Value: %v\n", t)
case float64:
fmt.Printf("It's a float64! Value: %.2f\n", t)
default:
fmt.Printf("It's an unknown type: %T\n", t)
}
}
func main() {
process("Go is awesome")
process(2024)
process(true)
process(3.14159)
process([]int{1, 2, 3})
}
4.4 接口嵌套(Interface Embedding)
一个接口可以嵌入其他接口,从而组合它们的方法集。一个类型必须实现所有被嵌入接口的方法,才能满足这个组合后的大接口。
package main
import "fmt"
// 定义基础接口
type Reader interface {
Read() string
}
type Writer interface {
Write(data string)
}
// 接口嵌套:ReadWriter 组合了 Reader 和 Writer
type ReadWriter interface {
Reader
Writer
}
// 定义一个实现 ReadWriter 接口的结构体
type MemoryBuffer struct {
data string
}
// 实现 Reader 接口
func (mb *MemoryBuffer) Read() string {
return mb.data
}
// 实现 Writer 接口
func (mb *MemoryBuffer) Write(data string) {
mb.data = data
fmt.Printf("Wrote '%s' to buffer.\n", data)
}
func main() {
// 创建实例
buffer := &MemoryBuffer{}
// buffer 既是 Reader, 也是 Writer, 也是 ReadWriter
// 1. 使用 ReadWriter 接口
var rw ReadWriter = buffer
rw.Write("Hello, nested interfaces!")
fmt.Printf("Read from ReadWriter: %s\n", rw.Read())
// 2. 也可以作为 Reader 接口使用
var r Reader = buffer
fmt.Printf("Read from Reader: %s\n", r.Read())
// 3. 也可以作为 Writer 接口使用
var w Writer = buffer
w.Write("New data")
fmt.Printf("Read again after writing via Writer interface: %s\n", rw.Read())
}
接口使用的最佳实践
- 小接口,大作为:倾向于定义小而专一的接口(例如
io.Reader),而不是一个包含几十个方法的大而全的接口。Go 的名言是:“The bigger the interface, the weaker the abstraction.”(接口越大,抽象能力越弱)。 er命名约定:对于只包含一个方法的接口,通常在方法名后加上er后缀来命名,如Reader,Writer,Stringer。- 接收接口,返回结构体 (Accept Interfaces, Return Structs):这是一个重要的 Go 设计模式。函数参数应尽量使用接口类型,增加灵活性和可测试性;而返回值应尽量是具体的结构体类型,让调用者清楚地知道他们得到了什么。