--- 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["用类型断言
v, ok := i.(int)"] B -->|否,有多种可能| D["用 type switch
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 工程模块化]]