znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第06章 - 方法与接口

本章讲解 Go 面向对象编程的两大支柱:方法(method)和接口(interface)。Go 没有类和继承,而是通过方法、接口和组合实现灵活的多态与抽象。

6.1 方法

方法是绑定到特定类型的函数。定义方法时,在 func 和方法名之间增加一个接收者(receiver)

type Circle struct {
    Radius float64
}

// Area 是 Circle 类型的方法,c 是接收者
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

c := Circle{Radius: 5}
fmt.Println(c.Area()) // 调用方法

6.1.1 值接收者与指针接收者

接收者可以是值类型或指针类型,二者有重要区别:

// 值接收者:操作的是副本,无法修改原对象
func (c Circle) Scale(factor float64) {
    c.Radius *= factor // 修改无效
}

// 指针接收者:操作的是原对象,可以修改
func (c *Circle) ScaleP(factor float64) {
    c.Radius *= factor // 修改生效
}

选择原则

  • 如果方法需要修改接收者,必须使用指针接收者。
  • 如果结构体较大,使用指针接收者避免复制开销。
  • 为保持一致性,如果一个类型的某些方法使用了指针接收者,那么它的所有方法都应使用指针接收者
  • 对于小型不可变类型,值接收者更安全。

6.1.2 为任意类型定义方法

方法可以定义在任何自定义类型上,不限于结构体:

type MyInt int

func (m MyInt) IsPositive() bool {
    return m > 0
}

var n MyInt = 5
fmt.Println(n.IsPositive()) // true

限制:不能为其他包中的类型(包括内置类型如 int)直接定义方法,必须先用 type 定义新类型。

6.2 接口

接口定义了一组方法签名的集合,是 Go 实现多态和解耦的核心机制。

6.2.1 接口定义与实现

type Shape interface {
    Area() float64
    Perimeter() float64
}

Go 的接口实现是隐式的(鸭子类型)——只要一个类型实现了接口中所有的方法,就自动满足该接口,无需显式声明 implements

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

// Rectangle 自动实现了 Shape 接口
var s Shape = Rectangle{Width: 3, Height: 4}
fmt.Println(s.Area()) // 12

这种隐式实现使代码高度解耦:实现方不需要知道接口的存在,接口可以在使用方按需定义。

6.2.2 多态

接口变量可以持有任何实现了该接口的具体类型,从而实现多态:

shapes := []Shape{
    Rectangle{Width: 3, Height: 4},
    Circle{Radius: 5},
}

for _, shape := range shapes {
    fmt.Printf("面积: %.2f\n", shape.Area())
}

6.2.3 空接口 interface{} 与 any

不包含任何方法的接口称为空接口,任何类型都满足它。Go 1.18 起,any 成为 interface{} 的别名,是推荐写法:

func describe(i any) {
    fmt.Printf("值: %v, 类型: %T\n", i, i)
}

describe(42)
describe("hello")
describe([]int{1, 2, 3})

空接口常用于需要处理任意类型的场景(如 fmt.Println 的参数),但应谨慎使用,过度使用会丧失类型安全。

6.3 类型断言与类型选择

6.3.1 类型断言

从接口值中提取其底层的具体类型:

var i any = "hello"

// 安全断言(推荐):返回值和成功标志
s, ok := i.(string)
if ok {
    fmt.Println("是字符串:", s)
}

// 非安全断言:断言失败会 panic
s := i.(string)

6.3.2 类型选择 type switch

当需要根据接口的多种可能类型分别处理时,使用 type switch

func process(i any) {
    switch v := i.(type) {
    case int:
        fmt.Println("整数:", v*2)
    case string:
        fmt.Println("字符串长度:", len(v))
    case bool:
        fmt.Println("布尔值:", v)
    default:
        fmt.Printf("未知类型: %T\n", v)
    }
}

6.4 接口的内部表示

接口变量内部由两部分组成:动态类型动态值。理解这一点有助于避免常见陷阱。

6.4.1 nil 接口的陷阱

一个接口值只有在动态类型和动态值都为 nil 时才等于 nil。下面是经典陷阱:

func doSomething() error {
    var p *MyError = nil
    return p // 返回的接口不为 nil!因为动态类型是 *MyError
}

err := doSomething()
fmt.Println(err == nil) // false,容易出错

建议:当函数返回 error 时,直接返回 nil,不要返回一个值为 nil 的具体指针类型。

6.5 常用标准库接口

Go 标准库定义了许多重要接口,理解并实现它们能让自定义类型无缝融入生态:

6.5.1 Stringer

实现 String() 方法可自定义类型的打印格式:

type Color struct {
    R, G, B int
}

func (c Color) String() string {
    return fmt.Sprintf("RGB(%d, %d, %d)", c.R, c.G, c.B)
}

fmt.Println(Color{255, 0, 0}) // 输出: RGB(255, 0, 0)

6.5.2 error

error 本身就是一个接口(详见第 7 章):

type error interface {
    Error() string
}

6.5.3 io.Reader 与 io.Writer

这是 Go 中最重要的两个接口,是流式 I/O 的基石:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

文件、网络连接、缓冲区、HTTP 请求体等都实现了这些接口,使得各种 I/O 操作可以统一处理和组合。

6.6 接口组合

接口可以嵌入其他接口,组合成更大的接口:

type ReadWriter interface {
    Reader // 嵌入 Reader
    Writer // 嵌入 Writer
}

标准库的 io.ReadWriterio.ReadCloser 等都是这样组合而来。

6.7 面向对象设计哲学

Go 的面向对象与传统语言有显著不同:

特性 传统 OOP(Java/C++) Go
继承 有类继承 无,用组合代替
接口实现 显式 implements 隐式(鸭子类型)
多态 基于继承层次 基于接口
封装 private/public 首字母大小写控制可见性

Go 推崇”组合优于继承“和”面向接口编程,而非面向实现“。接口应该小而专一——Go 标准库中很多接口只有一两个方法,越小的接口越容易被实现和复用。

Rob Pike 的名言:”The bigger the interface, the weaker the abstraction.”(接口越大,抽象越弱。)

6.8 本章小结

本章讲解了 Go 的方法与接口。方法通过接收者绑定到类型,需根据是否修改对象、性能等因素选择值接收者或指针接收者。接口通过隐式实现提供了强大而灵活的多态能力,配合类型断言、类型选择处理动态类型。理解空接口、nil 接口陷阱以及标准库核心接口(Stringer、io.Reader/Writer),并遵循”小接口 + 组合”的设计哲学,是写出地道 Go 代码的关键。

下一章我们将系统学习 Go 的错误处理机制。