编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
        • 目录介绍
        • 10.1 本章学习目标
        • 10.2 接口定义与隐式实现
          • 10.2.1 没有 implements 关键字
          • 为什么这么设计?——结构化子类型 vs 名义子类型
          • 10.2.2 接口隔离原则:小接口
        • 10.3 空接口 interface{} / any(Go 1.18+)
          • Go 1.18 之后用 any 还是 interface{}?
          • 何时该用 any,何时不该用?
        • 10.4 接口值的内部结构
          • 10.4.1 (itab, data) 双指针
          • *T 实现 vs T 实现的方法集差异
          • 10.4.2 nil 接口 vs nil 指针的接口
          • 正确写法:直接返回 nil
          • 验证一下
        • 10.5 类型断言
          • 10.5.1 单返回值 v := i.(T)(panic 风险)
          • 10.5.2 双返回值 v, ok := i.(T)(安全版)
          • 断言到接口类型
        • 10.6 类型 switch
          • 实战:写一个通用 JSON 序列化器
        • 10.7 接口嵌入
          • 实战:自己定义组合接口
          • 接口嵌入的同名方法冲突(Go 1.14+ 才允许)
        • 10.8 经典接口:Stringer / error / io.Reader/Writer
          • 10.8.1 fmt.Stringer
          • 10.8.2 error
          • 10.8.3 io.Reader / io.Writer
        • 10.9 多态范式:策略模式、模板方法、依赖倒置
          • 10.9.1 策略模式
          • 10.9.2 模板方法(Go 没有继承,但有组合 + 接口)
          • 10.9.3 依赖倒置(DIP)
        • 10.10 综合示例:实现一个插件式日志框架
        • 10.11 本章底层原理(简介)
          • 10.11.1 接口调用比直接调用慢多少?
          • 10.11.2 itab 缓存
          • 10.11.3 编译期检查 vs 运行时检查
        • 10.12 Go 新手陷阱 Top 5
          • 陷阱 1:nil 接口 vs nil 指针 (最经典)
          • 陷阱 2:把 any 当 Java Object 滥用
          • 陷阱 3:在大接口上做 mock
          • 陷阱 4:方法集不一致导致接口不实现
          • 陷阱 5:类型断言不带 ok,panic 没 recover
        • 10.13 思考题
        • 10.14 推荐阅读
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

接口与多态

# 第 10 章 接口与多态

Go 的接口是结构化子类型:不需要 implements 关键字,只要方法集匹配就自动实现。这是 Go 最有"灵魂"的设计。 关键词:duck typing、空接口 any、类型断言、类型 switch、接口值的双指针、nil 接口 vs nil 值


# 目录介绍

  • 10.1 本章学习目标
  • 10.2 接口定义与隐式实现
    • 10.2.1 没有 implements 关键字
    • 10.2.2 接口隔离原则:小接口
  • 10.3 空接口 interface{} / any(Go 1.18+)
  • 10.4 接口值的内部结构
    • 10.4.1 (itab, data) 双指针
    • 10.4.2 nil 接口 vs nil 指针的接口
  • 10.5 类型断言
    • 10.5.1 单返回值 v := i.(T)(panic 风险)
    • 10.5.2 双返回值 v, ok := i.(T)(安全版)
  • 10.6 类型 switch
  • 10.7 接口嵌入
  • 10.8 经典接口:Stringer / error / io.Reader/Writer
  • 10.9 多态范式:策略模式、模板方法、依赖倒置
  • 10.10 综合示例:实现一个插件式日志框架
  • 10.11 本章底层原理(简介)
  • 10.12 Go 新手陷阱 Top 5
  • 10.13 思考题
  • 10.14 推荐阅读

# 10.1 本章学习目标

学完本章你应当能够:

  • ✅ 能解释接口的"结构化子类型 / 隐式实现"哲学,并说清它与 Java/C# 的差异
  • ✅ 能识别一个类型是否实现了某个接口,能解释为什么 *T 实现的接口 T 不一定实现
  • ✅ 能画出接口值的 (itab, data) 双指针结构图
  • ✅ 能识别"nil 接口 vs nil 值"陷阱,并写出正确的 nil 判断
  • ✅ 能熟练使用类型断言(v, ok := i.(T))和类型 switch
  • ✅ 能用接口嵌入组合出大接口,能识别 io.ReadWriter 这类组合接口
  • ✅ 能熟练实现 Stringer 与 error 接口
  • ✅ 能用接口实现策略模式 / 模板方法 / 依赖倒置
  • ✅ 知道接口的动态调度成本(itab 缓存 + 间接调用)

本章是 Go 工程化能力的分水岭。理解了接口,才能理解 Go 标准库的设计哲学(io.Reader 一统天下),才能写出可测试、可扩展的工程代码。


# 10.2 接口定义与隐式实现

# 10.2.1 没有 implements 关键字

接口在 Go 里是一组方法集的声明:

type Animal interface {
    Name() string
    Sound() string
}
1
2
3
4

这就是全部。它说:"任何拥有 Name() string 和 Sound() string 这两个方法的类型,都自动是 Animal"。

type Dog struct{ name string }

func (d Dog) Name() string  { return d.name }
func (d Dog) Sound() string { return "汪汪" }

type Cat struct{ name string }

func (c Cat) Name() string  { return c.name }
func (c Cat) Sound() string { return "喵喵" }

func main() {
    var a Animal = Dog{name: "旺财"}  // 自动实现,无需声明
    fmt.Println(a.Name(), a.Sound())  // 旺财 汪汪

    a = Cat{name: "咪咪"}             // 同一个变量装不同类型
    fmt.Println(a.Name(), a.Sound())  // 咪咪 喵喵
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键观察:

  1. Dog 和 Cat 的定义里完全没有 Animal 这个名字。
  2. 它们"是 Animal"这件事,不是 Dog/Cat 声明出来的,而是编译器算出来的——只要方法集匹配,就自动满足。

与 Java 对比:

// Java:必须显式 implements
class Dog implements Animal { ... }
1
2

Go 不需要这一步。

# 为什么这么设计?——结构化子类型 vs 名义子类型

维度 名义子类型(Java/C#) 结构化子类型(Go)
关系建立 显式声明 implements 编译器算方法集
接口与实现的耦合 实现类必须知道接口 实现类不必知道接口
跨包重构 改接口名要改所有实现 改接口名不影响实现
后加接口 必须能改实现类源码 不必改实现类源码

最关键的好处:你可以给别人的库里的类型实现接口。比如标准库里的 os.File 不知道 io.Reader,但它就是 io.Reader——只要它有 Read([]byte)(int, error) 方法。

# 10.2.2 接口隔离原则:小接口

Go 标准库有一句金句:

The bigger the interface, the weaker the abstraction. ——Rob Pike

Go 提倡小接口、能组合。io 包里最常用的接口都只有 1 个方法:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}
1
2
3
4
5
6
7
8
9
10
11

需要"既能读又能写"?组合一下(见 §10.7 接口嵌入):

type ReadWriter interface {
    Reader
    Writer
}
1
2
3
4

新手常犯错:定义 10+ 方法的"上帝接口"。结果改一个方法所有 mock 都要改,测试代码爆炸。


# 10.3 空接口 interface{} / any(Go 1.18+)

空接口没有任何方法要求:

type any = interface{}  // Go 1.18 内建别名
1

由于"任何类型都拥有 0 个方法"这个集合,任何类型都满足空接口:

func main() {
    var a any
    a = 42
    a = "hello"
    a = []int{1, 2, 3}
    a = struct{ X int }{X: 1}
    fmt.Println(a)
}
1
2
3
4
5
6
7
8

# Go 1.18 之后用 any 还是 interface{}?

// 旧写法(仍合法)
func Println(v interface{})

// 新写法(推荐)
func Println(v any)
1
2
3
4
5

any 是 interface{} 的内建类型别名——完全等价,只是好读。Go 团队官方推荐用 any,标准库从 1.18 开始也在迁移。

# 何时该用 any,何时不该用?

场景 该不该用 any
真的需要装载任意类型(如 fmt.Println、JSON 解析) ✅ 该用
容器型(如 map[string]any 装 JSON 任意值) ✅ 该用
写"通用工具函数"图省事 ❌ 优先用泛型(卷一第 16 章)
函数参数你心里其实只有 2-3 种类型 ❌ 用接口或类型 switch
把 any 当 Java 的 Object 万能用 ❌ 强烈不推荐

黑话:any 是 Go 类型系统的"逃生舱"。能不开就不开。


# 10.4 接口值的内部结构

# 10.4.1 (itab, data) 双指针

这是 Go 接口最重要的底层知识。理解它,"nil 接口陷阱"就一通百通。

接口值在内存里是两个指针:

┌─────────────┬─────────────┐
│    itab     │    data     │   ← 接口值(16 字节,64 位机)
└─────────────┴─────────────┘
       │             │
       ▼             ▼
   类型信息       实际数据
   + 方法表        指针
1
2
3
4
5
6
7
  • itab(interface table):指向"接口类型 + 具体类型 + 方法函数指针表"的元数据
  • data:指向具体值的指针(小值会内联,但概念上是指针)

让我们看个例子:

type Animal interface {
    Sound() string
}

type Dog struct{ name string }

func (d Dog) Sound() string { return "汪" }

func main() {
    d := Dog{name: "旺财"}
    var a Animal = d  // 装箱

    // 此时 a 在内存里:
    //   itab:  指向 (Animal接口, Dog类型, [Sound函数地址])
    //   data:  指向 Dog{name: "旺财"} 的副本
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

几个关键事实:

  1. 接口值不是指针——它本身是一个 16 字节的结构体(在 64 位机上)
  2. 当你 var a Animal = d 时,d 会被复制(如果 d 是值类型)
  3. 调用 a.Sound() 实际是:先查 itab 找到 Sound 函数地址,再用 data 作为接收者去调用

这也解释了为什么接口调用比直接调用慢:多了一次间接跳转。但 Go 的 itab 是缓存的(runtime 维护一个 itab 哈希表),所以摊销成本很低。

# *T 实现 vs T 实现的方法集差异

回顾第 9 章 §9.4 的方法集铁律:

表达式 可调方法集 可赋值给的接口
T 类型变量 仅 func (t T) 定义的方法 仅这部分方法构成的接口
*T 类型变量 func (t T) + func (t *T) 这两部分方法构成的接口

所以:如果 Animal.Sound() 用指针接收者实现,那么 var a Animal = Dog{} 会编译失败:

func (d *Dog) Sound() string { return "汪" }  // 指针接收者

func main() {
    var a Animal = Dog{}    // ❌ Dog 没有 Sound 方法(只有 *Dog 有)
    var a Animal = &Dog{}   // ✅ *Dog 满足 Animal
}
1
2
3
4
5
6

这是 Go 接口最常见的"为什么编译不过"原因。记住口诀:指针接收者,必须传指针。

# 10.4.2 nil 接口 vs nil 指针的接口

这是 Go 最阴险的陷阱之一,几乎每个 Go 程序员都踩过:

type MyError struct{ msg string }

func (e *MyError) Error() string { return e.msg }

func doWork() error {
    var p *MyError = nil   // p 是 nil 指针
    return p               // 把 nil 指针装进 error 接口
}

func main() {
    err := doWork()
    if err != nil {        // ⚠️ 永远是 true!
        fmt.Println("出错了:", err)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出:

出错了: <nil>
1

为什么? 看接口的双指针:

return p 时:
  itab: 指向 (error接口, *MyError类型, [Error函数地址])  ← 非 nil!
  data: nil                                              ← 这里才是 nil
1
2
3

接口的 nil 判断是 itab == nil && data == nil。 只要 itab 非 nil("我装的是一个 *MyError 类型"这件事是有意义的),整个接口值就不是 nil。

# 正确写法:直接返回 nil

func doWork() error {
    var p *MyError = nil
    if p == nil {
        return nil  // ✅ 显式返回真正的 nil 接口
    }
    return p
}
1
2
3
4
5
6
7

或者从源头就不要用 *MyError 当返回类型中转:

func doWork() error {
    if 没问题 {
        return nil  // ✅ 直接返回 nil
    }
    return &MyError{msg: "oops"}
}
1
2
3
4
5
6

# 验证一下

func main() {
    var i any = nil
    var p *int = nil
    var i2 any = p

    fmt.Println(i == nil)   // true
    fmt.Println(p == nil)   // true
    fmt.Println(i2 == nil)  // false ← 注意!
}
1
2
3
4
5
6
7
8
9

这个陷阱在 §10.12 还会作为 Top 1 出现。现在记住一条规则:永远不要把可能为 nil 的具体类型变量返回给接口类型。要 nil 就直接 return nil。


# 10.5 类型断言

类型断言(type assertion)是从接口值"取出"具体类型的语法。

# 10.5.1 单返回值 v := i.(T)(panic 风险)

func main() {
    var i any = "hello"

    s := i.(string)         // 断言成功:s = "hello"
    fmt.Println(s)

    n := i.(int)            // ❌ panic: interface conversion: interface {} is string, not int
    fmt.Println(n)
}
1
2
3
4
5
6
7
8
9

单返回值形式在断言失败时会 panic。仅在你100% 确定类型时才用。

# 10.5.2 双返回值 v, ok := i.(T)(安全版)

func main() {
    var i any = "hello"

    if s, ok := i.(string); ok {
        fmt.Println("是字符串:", s)
    } else {
        fmt.Println("不是字符串")
    }

    if n, ok := i.(int); ok {
        fmt.Println("是整数:", n)
    } else {
        fmt.Println("不是整数")  // 走这条
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

生产代码请永远用双返回值形式——除非你愿意为一次类型不匹配让整个服务崩溃。

# 断言到接口类型

类型断言不只能断到具体类型,也能断到另一个接口类型:

type Closer interface {
    Close() error
}

func tryClose(v any) {
    if c, ok := v.(Closer); ok {  // 断言"是不是实现了 Closer 接口"
        c.Close()
    }
}
1
2
3
4
5
6
7
8
9

这是标准库里大量使用的模式。比如 io.Copy 内部会断言源是不是 WriterTo、目标是不是 ReaderFrom,从而走快速路径。


# 10.6 类型 switch

当你要对一个接口值做"按类型分派"时,链式 if-else 太丑:

// ❌ 笨重写法
func describe(i any) {
    if s, ok := i.(string); ok {
        fmt.Println("string:", s)
    } else if n, ok := i.(int); ok {
        fmt.Println("int:", n)
    } else if b, ok := i.(bool); ok {
        fmt.Println("bool:", b)
    } else {
        fmt.Println("unknown")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

Go 提供了专用语法 type switch:

// ✅ 推荐写法
func describe(i any) {
    switch v := i.(type) {
    case nil:
        fmt.Println("nil")
    case string:
        fmt.Println("string:", v, "长度", len(v))
    case int, int32, int64:
        // 注意:多类型 case 时,v 的类型是 any(取交集)
        fmt.Println("整数:", v)
    case bool:
        fmt.Println("bool:", v)
    case []byte:
        fmt.Println("bytes:", v)
    case error:
        // 也能断到接口
        fmt.Println("error:", v.Error())
    default:
        fmt.Printf("未知类型: %T\n", v)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

几个要点:

  1. i.(type) 只能在 switch 里用,别的地方都不行
  2. v 在每个 case 里自动是对应的具体类型——这是编译期魔法
  3. case 多类型并列时,v 类型退化为 any(取交集)
  4. case nil 用来匹配"接口值本身是 nil"
  5. 没有 fallthrough——和普通 switch 一样

# 实战:写一个通用 JSON 序列化器

func toJSON(v any) string {
    switch x := v.(type) {
    case nil:
        return "null"
    case bool:
        if x {
            return "true"
        }
        return "false"
    case string:
        return fmt.Sprintf("%q", x)
    case int, int32, int64, float32, float64:
        return fmt.Sprintf("%v", x)
    case []any:
        parts := make([]string, len(x))
        for i, e := range x {
            parts[i] = toJSON(e)
        }
        return "[" + strings.Join(parts, ",") + "]"
    case map[string]any:
        parts := make([]string, 0, len(x))
        for k, val := range x {
            parts = append(parts, fmt.Sprintf("%q:%s", k, toJSON(val)))
        }
        return "{" + strings.Join(parts, ",") + "}"
    default:
        return fmt.Sprintf("%q", fmt.Sprint(x))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

真实的 encoding/json 比这复杂得多(要处理 struct tag、反射、循环引用),但骨架就是这个 type switch。


# 10.7 接口嵌入

接口可以嵌入其他接口,方法集自动合并:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

ReadWriteCloser 的方法集 = Read + Write + Close 三个方法。任何同时实现这三个方法的类型都是 ReadWriteCloser。

# 实战:自己定义组合接口

type Logger interface {
    Info(msg string)
    Error(msg string)
}

type Closer interface {
    Close() error
}

// 组合:能打日志、能关
type LogCloser interface {
    Logger
    Closer
}

type FileLogger struct{ f *os.File }

func (l *FileLogger) Info(msg string)  { l.f.WriteString("INFO " + msg + "\n") }
func (l *FileLogger) Error(msg string) { l.f.WriteString("ERR  " + msg + "\n") }
func (l *FileLogger) Close() error     { return l.f.Close() }

func main() {
    var lc LogCloser = &FileLogger{f: os.Stdout}
    lc.Info("启动")
    lc.Error("出错")
    // lc.Close()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Go 标准库 io 包总共定义了 30+ 个接口,绝大多数都是 1-3 个方法的"原子接口",组合出 io.ReadWriter、io.ReadCloser、io.ReadWriteCloser 等"复合接口"。这就是 Rob Pike 那句"小接口、能组合"的工程体现。

# 接口嵌入的同名方法冲突(Go 1.14+ 才允许)

type A interface { M() }
type B interface { M() }
type C interface { A; B }   // Go 1.14 之前会报错重复,之后允许(视为同一个 M)
1
2
3

只要重叠的方法签名完全一致就允许。如果签名冲突(如返回类型不同),仍然编译错误。


# 10.8 经典接口:Stringer / error / io.Reader/Writer

Go 标准库里有几个"必须熟记"的接口。

# 10.8.1 fmt.Stringer

type Stringer interface {
    String() string
}
1
2
3

任何实现了 String() string 方法的类型,在 fmt.Println / fmt.Printf("%v", ...) 时会自动调用 String()。

type Money struct {
    Amount   int    // 分
    Currency string
}

func (m Money) String() string {
    return fmt.Sprintf("%s%.2f", m.Currency, float64(m.Amount)/100)
}

func main() {
    m := Money{Amount: 12345, Currency: "¥"}
    fmt.Println(m)             // ¥123.45 ← 自动调用了 String()
    fmt.Printf("%v\n", m)      // ¥123.45
    fmt.Printf("%+v\n", m)     // ¥123.45(仍走 String,不走默认结构体打印)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

给所有"对外展示用"的领域类型实现 Stringer,是 Go 工程化的好习惯。

# 10.8.2 error

type error interface {
    Error() string
}
1
2
3

只有一个方法。任何实现了 Error() string 的类型都是 error。

type DBError struct {
    Op  string
    Err error
}

func (e *DBError) Error() string {
    return fmt.Sprintf("db op %q: %v", e.Op, e.Err)
}

func query() error {
    return &DBError{Op: "SELECT", Err: errors.New("connection refused")}
}

func main() {
    if err := query(); err != nil {
        fmt.Println(err)  // db op "SELECT": connection refused
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

错误处理是入门卷下一章的重点(第 11 章),这里只展示接口的形态。

# 10.8.3 io.Reader / io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}
1
2
3
4
5
6
7

这两个接口统治了整个 Go IO 世界:

类型 是 Reader 是 Writer
os.File ✅ ✅
bytes.Buffer ✅ ✅
strings.Reader ✅ ❌
net.Conn ✅ ✅
gzip.Reader ✅ ❌
http.Response.Body ✅ ❌

只要一个东西"能往里读字节",它就是 Reader。所以 io.Copy(dst Writer, src Reader) 一个函数,就能干文件复制 / 网络转发 / 压缩 / 解压 / HTTP 转存 等几十种活——只要源和目标分别是 Reader/Writer。

// 一个例子:把 HTTP 响应体写到本地文件
resp, _ := http.Get("https://example.com")
defer resp.Body.Close()

f, _ := os.Create("out.html")
defer f.Close()

io.Copy(f, resp.Body)  // resp.Body 是 Reader,f 是 Writer,搞定
1
2
3
4
5
6
7
8

这就是接口设计的力量:一旦确定了"小接口",整个生态都为它服务。


# 10.9 多态范式:策略模式、模板方法、依赖倒置

接口在面向对象里最重要的工程价值是多态——同一段代码,根据传入对象的不同走不同分支。

# 10.9.1 策略模式

让算法可替换:

type Pricer interface {
    Price(amount float64) float64
}

type NormalPricer struct{}

func (NormalPricer) Price(a float64) float64 { return a }

type VIPPricer struct{ Discount float64 }

func (v VIPPricer) Price(a float64) float64 { return a * (1 - v.Discount) }

type EventPricer struct{ Off float64 }

func (e EventPricer) Price(a float64) float64 {
    if a > 100 {
        return a - e.Off
    }
    return a
}

func Checkout(p Pricer, amount float64) float64 {
    return p.Price(amount)
}

func main() {
    fmt.Println(Checkout(NormalPricer{}, 200))         // 200
    fmt.Println(Checkout(VIPPricer{Discount: 0.2}, 200)) // 160
    fmt.Println(Checkout(EventPricer{Off: 30}, 200))   // 170
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

加新策略只要新加一个实现 Pricer 的类型,不动 Checkout——开闭原则的天然落地。

# 10.9.2 模板方法(Go 没有继承,但有组合 + 接口)

type Step interface {
    DoStep() error
}

func Pipeline(steps []Step) error {
    for i, s := range steps {
        fmt.Printf("步骤 %d 执行\n", i+1)
        if err := s.DoStep(); err != nil {
            return fmt.Errorf("步骤 %d 失败: %w", i+1, err)
        }
    }
    return nil
}

type LoadStep struct{}
type ParseStep struct{}
type SaveStep struct{}

func (LoadStep) DoStep() error  { return nil }
func (ParseStep) DoStep() error { return nil }
func (SaveStep) DoStep() error  { return nil }

func main() {
    Pipeline([]Step{LoadStep{}, ParseStep{}, SaveStep{}})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 10.9.3 依赖倒置(DIP)

高层模块不依赖低层模块,两者都依赖抽象。

// ❌ 反例:高层服务直接依赖具体的 MySQL 实现
type UserService struct {
    db *MySQLDB  // 紧耦合,没法替换、没法测试
}

// ✅ 正例:高层服务依赖接口
type UserStore interface {
    Get(id int) (*User, error)
    Save(u *User) error
}

type UserService struct {
    store UserStore  // 任何实现 UserStore 的类型都行
}

// 测试时塞个内存实现
type memStore struct{ m map[int]*User }
func (s *memStore) Get(id int) (*User, error)  { return s.m[id], nil }
func (s *memStore) Save(u *User) error          { s.m[u.ID] = u; return nil }

// 生产环境用 MySQL 实现
type mysqlStore struct{ db *sql.DB }
func (s *mysqlStore) Get(id int) (*User, error) { /* ... */ }
func (s *mysqlStore) Save(u *User) error         { /* ... */ }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这是 Go 工程代码的灵魂:依赖接口而不是依赖结构体。理解了这一条,你写出的 Go 代码就立刻进入"工程级"。


# 10.10 综合示例:实现一个插件式日志框架

下面用接口实现一个能切换"控制台 / 文件 / JSON"输出的日志框架:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "os"
    "time"
)

// ===== 1. 定义核心接口 =====

type Level int

const (
    DEBUG Level = iota
    INFO
    WARN
    ERROR
)

func (l Level) String() string {
    return []string{"DEBUG", "INFO", "WARN", "ERROR"}[l]
}

// Sink 是日志输出端:能写一条日志就行
type Sink interface {
    Write(lvl Level, msg string, fields map[string]any) error
}

// Logger 是顶层接口
type Logger interface {
    Debug(msg string, fields ...Field)
    Info(msg string, fields ...Field)
    Warn(msg string, fields ...Field)
    Error(msg string, fields ...Field)
}

type Field struct {
    Key   string
    Value any
}

func F(k string, v any) Field { return Field{Key: k, Value: v} }

// ===== 2. 实现一个具体 Logger(依赖 Sink 接口) =====

type stdLogger struct {
    sink Sink
    lvl  Level
}

func New(sink Sink, lvl Level) Logger {
    return &stdLogger{sink: sink, lvl: lvl}
}

func (l *stdLogger) write(lvl Level, msg string, fields []Field) {
    if lvl < l.lvl {
        return
    }
    fmap := make(map[string]any, len(fields))
    for _, f := range fields {
        fmap[f.Key] = f.Value
    }
    _ = l.sink.Write(lvl, msg, fmap)
}

func (l *stdLogger) Debug(msg string, fs ...Field) { l.write(DEBUG, msg, fs) }
func (l *stdLogger) Info(msg string, fs ...Field)  { l.write(INFO, msg, fs) }
func (l *stdLogger) Warn(msg string, fs ...Field)  { l.write(WARN, msg, fs) }
func (l *stdLogger) Error(msg string, fs ...Field) { l.write(ERROR, msg, fs) }

// ===== 3. 提供多个 Sink 实现 =====

// 3.1 控制台输出
type ConsoleSink struct{}

func (ConsoleSink) Write(lvl Level, msg string, fields map[string]any) error {
    fmt.Printf("[%s] %s %s %v\n",
        time.Now().Format("15:04:05"), lvl, msg, fields)
    return nil
}

// 3.2 文件输出(依赖 io.Writer 接口)
type FileSink struct {
    w io.Writer
}

func (s *FileSink) Write(lvl Level, msg string, fields map[string]any) error {
    _, err := fmt.Fprintf(s.w, "[%s] %s %s %v\n",
        time.Now().Format(time.RFC3339), lvl, msg, fields)
    return err
}

// 3.3 JSON 输出
type JSONSink struct {
    w io.Writer
}

func (s *JSONSink) Write(lvl Level, msg string, fields map[string]any) error {
    record := map[string]any{
        "time":   time.Now().Format(time.RFC3339),
        "level":  lvl.String(),
        "msg":    msg,
        "fields": fields,
    }
    return json.NewEncoder(s.w).Encode(record)
}

// 3.4 多路 Sink(组合多个 Sink)
type MultiSink struct {
    sinks []Sink
}

func (m *MultiSink) Write(lvl Level, msg string, fields map[string]any) error {
    for _, s := range m.sinks {
        _ = s.Write(lvl, msg, fields)
    }
    return nil
}

// ===== 4. 使用 =====

func main() {
    // 同时输出到控制台和文件
    f, _ := os.OpenFile("app.log",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    defer f.Close()

    multi := &MultiSink{sinks: []Sink{
        ConsoleSink{},
        &JSONSink{w: f},
    }}

    log := New(multi, INFO)

    log.Debug("不会显示")  // < INFO 被过滤
    log.Info("用户登录", F("user_id", 42), F("ip", "1.2.3.4"))
    log.Error("数据库失联", F("err", "connection refused"))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

这个例子用到了本章 7 个核心知识点:

  1. 接口定义:Sink / Logger(§10.2)
  2. 小接口:Sink 只有一个方法(§10.2.2)
  3. 接口组合:MultiSink 持有多个 Sink(§10.7)
  4. 依赖接口:stdLogger 不知道具体 Sink(§10.9.3)
  5. io.Writer 接口复用:FileSink/JSONSink 直接用标准库接口(§10.8)
  6. Stringer 接口:Level.String() 让 fmt.Print 自动格式化(§10.8.1)
  7. 多态:换 sink 不改 logger,加 sink 不改任何已有代码(§10.9)

把这个例子吃透,你已经会写工程级 Go 代码了。


# 10.11 本章底层原理(简介)

# 10.11.1 接口调用比直接调用慢多少?

// benchmark
func BenchmarkDirect(b *testing.B) {
    d := Dog{}
    for i := 0; i < b.N; i++ {
        d.Sound()
    }
}

func BenchmarkIface(b *testing.B) {
    var a Animal = Dog{}
    for i := 0; i < b.N; i++ {
        a.Sound()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

经验数字(M1 Mac, Go 1.22):

BenchmarkDirect-8   1000000000   0.30 ns/op
BenchmarkIface-8     500000000   2.10 ns/op
1
2

接口调用大约比直接调用慢 5-10 倍——因为多了 itab 查找 + 间接跳转。但都是纳秒级,绝大多数业务代码完全无感。

只在以下场景该担心:

  • 内层热点循环每秒亿级调用
  • 数值计算密集型代码

否则放心用接口。

# 10.11.2 itab 缓存

每个 (接口类型, 具体类型) 对,runtime 第一次会算出 itab 并放进全局哈希表,之后所有相同对的接口装箱都复用这个 itab。所以"接口装箱"在重复路径上不分配内存。

但首次装箱(cold path)和装箱小值(如 int)可能分配内存。这也是为什么 Go 有 sync.Pool 等技术减少接口装箱。

# 10.11.3 编译期检查 vs 运行时检查

操作 检查时机
var a Animal = Dog{} 编译期——Dog 不满足就直接编译错误
a.(*Dog) 运行时——失败时 panic 或返回 ok=false
switch v := i.(type) 运行时

利用编译期检查的小技巧("接口实现自检"):

var _ Animal = (*Dog)(nil)  // 编译期断言:*Dog 必须满足 Animal
1

工程里非常常见。把它放在文件顶部,重构破坏了实现关系会立刻报错。


# 10.12 Go 新手陷阱 Top 5

# 陷阱 1:nil 接口 vs nil 指针 (最经典)

func find() error {
    var e *MyError = nil
    return e            // ⚠️ 接口非 nil
}

if err := find(); err != nil {  // 永远进
    log.Println(err)            // <nil>
}
1
2
3
4
5
6
7
8

✅ 修复:要 nil 就 return nil,绝不返回类型为 *MyError 的 nil。

# 陷阱 2:把 any 当 Java Object 滥用

// ❌ 反例
func Add(a, b any) any { ... }

// ✅ 正例:用泛型
func Add[T constraints.Number](a, b T) T { return a + b }
1
2
3
4
5

any 失去了类型安全和性能。Go 1.18+ 优先用泛型(卷一第 16 章)。

# 陷阱 3:在大接口上做 mock

// ❌ 上帝接口
type UserStore interface {
    Get(id int) (*User, error)
    Save(u *User) error
    Delete(id int) error
    List(filter Filter) ([]*User, error)
    Count() (int, error)
    Search(q string) ([]*User, error)
    // ... 还有 10+ 个
}
1
2
3
4
5
6
7
8
9
10

测试时只用到 Get,结果 mock 时要实现全部 15 个方法(即使返回 panic("not implemented"))。改一个方法签名,所有 mock 全要改。

✅ 修复:拆成小接口,测试时只需要哪个就传哪个。

type UserGetter interface { Get(id int) (*User, error) }
type UserSaver  interface { Save(u *User) error }
// ... 按需组合

func sendWelcome(g UserGetter, id int) {  // 只依赖最小子集
    u, _ := g.Get(id)
    // ...
}
1
2
3
4
5
6
7
8

# 陷阱 4:方法集不一致导致接口不实现

type Speaker interface { Speak() }

type Dog struct{}
func (d Dog) Speak()    {}  // 值接收者

type Cat struct{}
func (c *Cat) Speak()   {}  // 指针接收者

func main() {
    var s Speaker
    s = Dog{}    // ✅ Dog 满足
    s = &Dog{}   // ✅ *Dog 也满足
    s = Cat{}    // ❌ 编译错误:Cat 没 Speak(只有 *Cat 有)
    s = &Cat{}   // ✅ *Cat 满足
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

✅ 修复:要么把方法改值接收者,要么传 &Cat{}。回顾第 9 章 §9.4 方法集铁律。

# 陷阱 5:类型断言不带 ok,panic 没 recover

// ❌ 反例
func handle(i any) {
    s := i.(string)        // panic 服务挂掉
    fmt.Println(len(s))
}

// ✅ 正例
func handle(i any) {
    s, ok := i.(string)
    if !ok {
        return
    }
    fmt.Println(len(s))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

生产代码写 i.(T) 不带 ok 的,约等于在生产环境写 unsafe.Pointer:除非你拿命担保。


# 10.13 思考题

  1. 解释一段话:为什么 var a Animal = (*Dog)(nil); a == nil 是 false?画出双指针图。
  2. 代码题:给定一个 []any{1, "hi", 3.14, true, nil, []int{1,2}},写一个 dump 函数用 type switch 逐个打印类型 + 值。
  3. 设计题:用接口设计一个"折扣计算器",支持组合多个折扣(如"满 100 减 20" + "VIP 9 折")。
  4. 重构题:你看到一段代码 func Process(s *MySQLStore),怎么改成依赖接口?把改造前后对比写出来。
  5. 底层题:以下哪些操作是编译期检查、哪些是运行时检查?
    • var i Reader = &File{}
    • r := i.(*File)
    • _, ok := i.(io.Closer)
    • var _ Reader = (*File)(nil)
  6. 陷阱题:下面代码输出什么?为什么?
    func mayFail() error {
        var p *os.PathError
        return p
    }
    func main() {
        err := mayFail()
        fmt.Println(err == nil)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  7. 进阶题:io.Copy(dst, src) 内部会优先尝试 dst.(io.ReaderFrom) 或 src.(io.WriterTo)。请解释这种"快速路径"设计的好处,以及它如何兼顾性能与通用性。
  8. 设计哲学题:Rob Pike 说 "the bigger the interface...",结合本章实例,谈谈你对这句话的理解。

# 10.14 推荐阅读

  • 卷一第 16 章 标准库与泛型(接口的现代替代品)
  • 卷一第 11 章 错误处理(error 接口的深度用法)
  • 卷三第 5 章 接口与类型系统
  • 卷二第 3 章 职工管理系统(接口多态首用)
  • 卷二第 8 章 轮训管理系统(策略模式落地)
  • Effective Go - Interfaces (opens new window)
  • Russ Cox - Go Data Structures: Interfaces (opens new window)(双指针经典讲解)
  • Dave Cheney - Don't just check errors, handle them gracefully (opens new window)
  • Rob Pike - Go Proverbs (opens new window)("The bigger the interface...")

下一章预告:第 11 章《错误处理》——Go 没有 try/catch,但有 error 接口、panic/recover、errors.Is/As、错误包装链。我们会把"为什么 Go 把错误当值"这件事讲清楚。

上次更新: 2026/06/10, 11:13:41
结构体与方法
错误处理

← 结构体与方法 错误处理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式