Files
2026-05-24 11:42:38 +08:00

6.1 KiB
Raw Permalink Blame History

tags, create time
tags create time
go
polymorphism
interface
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] 经验法则

  1. 优先在函数入口做一次类型断言校验,后续逻辑不再重复断言。
  2. 尽量避免裸类型断言 v.(Type) 而不用 comma ok——除非你确信类型必然正确,且愿意用 panic 表达"程序 BUG"。
  3. 空接口作为返回值时做好文档说明预期类型,否则调用方容易踩坑。

关联笔记