6.1 KiB
6.1 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-05-02 14:00 |
Go 多态
概述
梳理 Go 中的多态机制:通过接口实现"同一份代码处理多种类型",涵盖接口定义与实现、类型断言、type switch、空接口与泛型的取舍等核心用法。
正文
一、什么是多态
[!question] 思考:为什么需要多态?
假设你写了一个打印函数
Print(),只能接受User类型。后来来了一个Admin类型,你又得写一个PrintAdmin()。如果有十个不同的类型呢?多态就是为了解决这种对不同类型做相同操作时,代码不断膨胀的问题——让同一份代码处理多种具体类型。
Go 的多态通过接口(Interface)来实现,与其他语言有本质区别:
// 其他语言(如 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 基础用法
var w io.Writer = file // 结构体 → 接口(隐式转换)
w.Write([]byte("hello")) // 运行时动态调用具体类型的 Write 方法
[!note] 指针接收者的小陷阱
方法接收者 什么变量可以赋值给接口 func (t T) Method()T和*T都可以func (t *T) Method()只有 *T可以如果接口要求指针接收者方法,传值类型会编译报错。这是初学者最容易踩的坑之一。
2.2 零值接口的特性
var w io.Writer // 值为 nil,不会 panic —— 直到你真正调用它
w.Write([]byte("test")) // 这里才 panic: nil pointer dereference
零值接口不等于空字符串或 false,它就是 nil,只有在被调用时才会触发 panic。这意味着你可以安全地把未赋值的接口当作参数传递(比如依赖注入中可选的接口)。
三、类型断言与 Type Switch
3.1 类型断言
从接口值中提取底层具体类型:
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
同时处理多种可能类型:
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)
}
}
两种方式的选用建议:
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
// 能存储任意类型 —— JSON 反序列化正是依赖这一特性
var x any = "hello" // 等价于: var x interface{} = "hello"
典型应用:
// 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 泛型
// ❌ 泛型引入前:空接口做容器,取用时需反复断言
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) 普通接口
五、实战:策略模式的接口化
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 的安全用法
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] 经验法则
- 优先在函数入口做一次类型断言校验,后续逻辑不再重复断言。
- 尽量避免裸类型断言
v.(Type)而不用comma ok——除非你确信类型必然正确,且愿意用 panic 表达"程序 BUG"。- 空接口作为返回值时做好文档说明预期类型,否则调用方容易踩坑。