212 lines
6.1 KiB
Markdown
212 lines
6.1 KiB
Markdown
---
|
||
tags: [go, polymorphism, interface]
|
||
create time: 2026-05-02 14:00
|
||
---
|
||
|
||
# Go 多态
|
||
|
||
## 概述
|
||
|
||
梳理 Go 中的多态机制:通过接口实现"同一份代码处理多种类型",涵盖接口定义与实现、类型断言、type switch、空接口与泛型的取舍等核心用法。
|
||
|
||
## 正文
|
||
|
||
### 一、什么是多态
|
||
|
||
> [!question] 思考:为什么需要多态?
|
||
>
|
||
> 假设你写了一个打印函数 `Print()`,只能接受 `User` 类型。后来来了一个 `Admin` 类型,你又得写一个 `PrintAdmin()`。如果有十个不同的类型呢?多态就是为了解决这种对不同类型做相同操作时,代码不断膨胀的问题——**让同一份代码处理多种具体类型**。
|
||
|
||
Go 的多态通过接口(Interface)来实现,与其他语言有本质区别:
|
||
|
||
```go
|
||
// 其他语言(如 Java/C++)需要显式声明实现某个接口
|
||
// Go:只要 struct 实现了接口所有方法,就自动满足该接口,无需 implements / : 关键字
|
||
|
||
type Writer interface {
|
||
Write([]byte) (int, error)
|
||
}
|
||
|
||
type File struct{}
|
||
func (f File) Write(p []byte) (int, error) { return len(p), nil }
|
||
// File 自动实现 io.Writer,零额外声明
|
||
```
|
||
|
||
核心原则:**接口定义行为,结构体提供实现,双方互不感知对方存在。**
|
||
|
||
### 二、接口赋值与方法接收者
|
||
|
||
#### 2.1 基础用法
|
||
|
||
```go
|
||
var w io.Writer = file // 结构体 → 接口(隐式转换)
|
||
w.Write([]byte("hello")) // 运行时动态调用具体类型的 Write 方法
|
||
```
|
||
|
||
> [!note] 指针接收者的小陷阱
|
||
>
|
||
> | 方法接收者 | 什么变量可以赋值给接口 |
|
||
> |-----------|---------------------|
|
||
> | `func (t T) Method()` | `T` 和 `*T` 都可以 |
|
||
> | `func (t *T) Method()` | 只有 `*T` 可以 |
|
||
>
|
||
> 如果接口要求指针接收者方法,传值类型会编译报错。这是初学者最容易踩的坑之一。
|
||
|
||
#### 2.2 零值接口的特性
|
||
|
||
```go
|
||
var w io.Writer // 值为 nil,不会 panic —— 直到你真正调用它
|
||
|
||
w.Write([]byte("test")) // 这里才 panic: nil pointer dereference
|
||
```
|
||
|
||
零值接口不等于空字符串或 false,它就是 `nil`,只有在被调用时才会触发 panic。这意味着你可以安全地把未赋值的接口当作参数传递(比如依赖注入中可选的接口)。
|
||
|
||
### 三、类型断言与 Type Switch
|
||
|
||
#### 3.1 类型断言
|
||
|
||
从接口值中提取底层具体类型:
|
||
|
||
```go
|
||
v := interface{}(42)
|
||
|
||
val, ok := v.(int) // 安全断言,ok=false 时 val=0,不会 panic
|
||
fmt.Println(val, ok) // 42 true
|
||
|
||
bad := v.(string) // 不安全断言,类型不对会 panic
|
||
fmt.Println(bad) // panic: interface conversion: interface {} is int, not string
|
||
```
|
||
|
||
> [!tip] 惯用法:"comma ok" 是 Go 中检查类型的标准模式,类似 map 读取时的写法。几乎所有标准库都遵循这一约定。
|
||
|
||
#### 3.2 Type Switch
|
||
|
||
同时处理多种可能类型:
|
||
|
||
```go
|
||
func describe(i any) {
|
||
switch v := i.(type) {
|
||
case int:
|
||
fmt.Printf("int, value=%d\n", v)
|
||
case string:
|
||
fmt.Printf("string, value=%s\n", v)
|
||
default:
|
||
fmt.Printf("unknown type %T\n", v)
|
||
}
|
||
}
|
||
```
|
||
|
||
两种方式的选用建议:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["interface{} 存储的值"] --> B{"确定只处理一种类型?"}
|
||
B -->|是| C["用类型断言<br/>v, ok := i.(int)"]
|
||
B -->|否,有多种可能| D["用 type switch<br/>switch i.(type)"]
|
||
|
||
style C fill:#bfb,stroke:#333
|
||
style D fill:#bbf,stroke:#333
|
||
```
|
||
|
||
### 四、空接口与泛型的取舍
|
||
|
||
#### 4.1 空接口 `any`
|
||
|
||
```go
|
||
// 能存储任意类型 —— JSON 反序列化正是依赖这一特性
|
||
var x any = "hello" // 等价于: var x interface{} = "hello"
|
||
```
|
||
|
||
典型应用:
|
||
|
||
```go
|
||
// encoding/json.Unmarshal
|
||
func Unmarshal(data []byte, v any) error
|
||
|
||
// sync.Map
|
||
func (*sync.Map) LoadOrStore(key, value any) (actual any, loaded bool)
|
||
```
|
||
|
||
#### 4.2 空接口 vs 泛型
|
||
|
||
```go
|
||
// ❌ 泛型引入前:空接口做容器,取用时需反复断言
|
||
type AnyList struct{ Items []any }
|
||
func (l *AnyList) Add(item any) { l.Items = append(l.Items, item) }
|
||
|
||
list := &AnyList{}
|
||
list.Add("hi")
|
||
list.Add(42) // 编译期不会报错,运行时类型断言会出问题
|
||
|
||
// ✅ Go 1.18+ 泛型方案,类型安全有保障
|
||
type List[T any] struct{ Items []T }
|
||
func (l *List[T]) Add(item T) { l.Items = append(l.Items, item) }
|
||
|
||
intList := &List[int]{}
|
||
intList.Add(1)
|
||
intList.Add("hi") // 编译期直接报错
|
||
```
|
||
|
||
> [!summary] 如何选择?
|
||
>
|
||
> | 场景 | 推荐方案 |
|
||
> |------|----------|
|
||
> 处理完全未知的类型(JSON 反序列化、日志、缓存) | 空接口 `any` |
|
||
> 容器类数据结构(切片、Map、列表) | 泛型 |
|
||
> 定义抽象行为接口(如 Reader、Writer) | 普通接口 |
|
||
|
||
### 五、实战:策略模式的接口化
|
||
|
||
```go
|
||
type Payer interface {
|
||
Pay(amount float64) error
|
||
}
|
||
|
||
type Alipay struct{}
|
||
func (a Alipay) Pay(amount float64) error { /* ... */ return nil }
|
||
|
||
type WechatPay struct{}
|
||
func (w WechatPay) Pay(amount float64) error { /* ... */ return nil }
|
||
|
||
type CreditCard struct{}
|
||
func (c CreditCard) Pay(amount float64) error { /* ... */ return nil }
|
||
|
||
// 多态体现:同一行代码处理三种支付方式
|
||
func Checkout(p Payer, amount float64) {
|
||
_ = p.Pay(amount) // 不关心具体是谁在付钱
|
||
}
|
||
|
||
Checkout(Alipay{}, 99.0)
|
||
Checkout(WechatPay{}, 99.0)
|
||
```
|
||
|
||
新增支付方式时无需修改 `Checkout` 本身——符合开闭原则(对扩展开放,对修改封闭)。这就是多态带来的解耦效果。
|
||
|
||
### 六、Type Assertion 的安全用法
|
||
|
||
```go
|
||
data := loadData() // returns interface{}
|
||
|
||
// ❌ 危险链:一旦类型不对,整条链路 panic
|
||
user := data.(*User)
|
||
name := user.Name
|
||
|
||
// ✅ 安全链:先校验再使用
|
||
if user, ok := data.(*User); ok {
|
||
name := user.Name
|
||
} else {
|
||
log.Printf("unexpected type: %T", data)
|
||
}
|
||
```
|
||
|
||
> [!warning] 经验法则
|
||
>
|
||
> 1. 优先在函数入口做一次类型断言校验,后续逻辑不再重复断言。
|
||
> 2. 尽量避免裸类型断言 `v.(Type)` 而不用 `comma ok`——除非你确信类型必然正确,且愿意用 panic 表达"程序 BUG"。
|
||
> 3. 空接口作为返回值时做好文档说明预期类型,否则调用方容易踩坑。
|
||
|
||
## 关联笔记
|
||
|
||
- [[Go 工程模块化]]
|