第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.ReadWriter、io.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 的错误处理机制。