This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/GO/多态.md
T

212 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 工程模块化]]