Go 从 0 到精通 · 第 14 课:接口 interface 入门

学习定位:这是整套 Go 教程的第 14 课,也是阶段三(核心抽象阶段)的第一课。
前置要求:已经完成第 13 课,掌握了方法的定义、值接收者与指针接收者的区别、方法的本质。
本课目标:理解 Go 的抽象能力来自接口而非继承,掌握接口的定义与实现方式,理解"鸭子类型"思想,能写出简单的接口并理解其设计意义。


1. 本课你要解决的核心问题

到目前为止,你写的代码都是"具体类型"——StudentBankAccountCounter

但现实中经常有这种需求:

  • 我有一个函数,它需要"能打印自身信息的东西"——不管是 Student 还是 BankAccount,只要能打印就行
  • 我想写一个通用的排序逻辑,不管是按成绩排序还是按年龄排序,只要能比较就行

在 Java 或 C++ 里,你会用继承抽象类来解决。Go 没有继承——它用接口来表达抽象。

你需要搞明白以下问题:

  • 接口是什么
  • Go 的接口和其他语言有什么不同
  • 什么是"鸭子类型"
  • 怎么定义接口、怎么实现接口
  • 空接口 interface{} 有什么用
  • 接口在实际开发中的价值

学完这一课,你就能理解 Go 的抽象能力是怎么来的了。


2. 接口是什么

2.1 接口是一组方法的集合

接口定义的不是"数据长什么样",而是"能做什么操作":

1
2
3
type Speaker interface {
Speak() string
}

这行代码说:任何类型,只要有一个 Speak() string 方法,就满足 Speaker 接口。

2.2 一个简单的例子

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
package main

import "fmt"

// 定义接口
type Speaker interface {
Speak() string
}

// Dog 类型
type Dog struct {
Name string
}

func (d Dog) Speak() string {
return d.Name + " 说:汪汪汪"
}

// Cat 类型
type Cat struct {
Name string
}

func (c Cat) Speak() string {
return c.Name + " 说:喵喵喵"
}

// 函数接收接口
func letSpeak(s Speaker) {
fmt.Println(s.Speak())
}

func main() {
dog := Dog{Name: "旺财"}
cat := Cat{Name: "咪咪"}

letSpeak(dog) // 旺财 说:汪汪汪
letSpeak(cat) // 咪咪 说:喵喵喵
}

DogCat 都没有显式声明"我实现了 Speaker 接口"——它们只是恰好有一个 Speak() string 方法。Go 编译器自动判断:你有这个方法,那你就是这个接口。

这就是鸭子类型(duck typing)。


3. 鸭子类型

3.1 什么是鸭子类型

如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。

在 Go 中,一个类型是否满足某个接口,不取决于它声明了什么,而取决于它有没有接口要求的方法

1
2
3
4
5
6
7
type Speaker interface {
Speak() string
}

// Dog 有 Speak 方法 → Dog 满足 Speaker
// Cat 有 Speak 方法 → Cat 满足 Speaker
// int 没有 Speak 方法 → int 不满足 Speaker

3.2 和其他语言的对比

语言 实现接口的方式
Java class Dog implements Speaker(显式声明)
C++ class Dog : public Speaker(显式继承)
Go 只要有方法就行(隐式满足)

Go 的好处:

  • 接口和实现解耦——定义接口的人不需要知道谁会实现它
  • 减少了"为了实现接口而写的样板代码"
  • 可以给已有的类型(甚至其他包的类型)添加接口支持

4. 接口的定义与实现

4.1 定义接口

1
2
3
4
type 接口名 interface {
方法名(参数) 返回值
方法名(参数) 返回值
}

接口里只有方法签名,没有字段:

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

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

type Closer interface {
Close() error
}

4.2 实现接口

不需要 implements 关键字。只要类型的方法集包含了接口的所有方法,就自动满足:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type File struct {
name string
}

func (f *File) Write(p []byte) (n int, err error) {
fmt.Println("写入", len(p), "字节到", f.name)
return len(p), nil
}

func (f *File) Close() error {
fmt.Println("关闭", f.name)
return nil
}

// *File 实现了 Writer 和 Closer,但没有实现 Reader

4.3 一个类型可以实现多个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

type File struct {
name string
}

func (f *File) Write(p []byte) (n int, err error) {
return len(p), nil
}

func (f *File) Close() error {
return nil
}

// *File 同时满足 Writer 和 Closer
var w Writer = &File{name: "test.txt"}
var c Closer = &File{name: "test.txt"}

5. 接口作为函数参数

接口最大的价值是让函数接受"任何满足条件的类型":

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
package main

import "fmt"

type Stringer interface {
String() string
}

type Student struct {
Name string
Score int
}

func (s Student) String() string {
return fmt.Sprintf("%s (分数: %d)", s.Name, s.Score)
}

type Product struct {
Name string
Price float64
}

func (p Product) String() string {
return fmt.Sprintf("%s (价格: ¥%.2f)", p.Name, p.Price)
}

// 通用打印函数
func PrintInfo(s Stringer) {
fmt.Println(s.String())
}

func main() {
stu := Student{Name: "小明", Score: 95}
prod := Product{Name: "键盘", Price: 299.0}

PrintInfo(stu) // 小明 (分数: 95)
PrintInfo(prod) // 键盘 (价格: ¥299.00)
}

PrintInfo 不关心传入的具体类型,只关心"你有没有 String() string 方法"。


6. 空接口 interface{}

6.1 什么是空接口

空接口不包含任何方法:

1
2
type Any interface{}
// 或者直接写 interface{}

因为没有任何方法要求,所有类型都满足空接口

1
2
3
4
5
6
7
8
9
10
func printAnything(v interface{}) {
fmt.Println(v)
}

func main() {
printAnything(42) // int
printAnything("hello") // string
printAnything(3.14) // float64
printAnything([]int{1, 2}) // []int
}

6.2 空接口的用途

空接口常用于需要"接受任意类型"的场景:

  • fmt.Println 的参数就是 ...interface{}
  • JSON 解码时目标类型可能是 interface{}
  • 通用的数据容器

6.3 从空接口取回具体类型(类型断言)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
var v interface{} = "hello"

// 类型断言
s, ok := v.(string)
if ok {
fmt.Println("是字符串:", s) // 是字符串: hello
}

n, ok := v.(int)
if ok {
fmt.Println("是整数:", n)
} else {
fmt.Println("不是整数") // 不是整数
}
}

v.(Type) 就是类型断言——尝试把 interface{} 转成具体类型。用"逗号 ok"模式避免 panic。

6.4 用 switch 做类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func classify(v interface{}) {
switch val := v.(type) {
case int:
fmt.Println("整数:", val)
case string:
fmt.Println("字符串:", val)
case bool:
fmt.Println("布尔值:", val)
default:
fmt.Println("未知类型")
}
}

func main() {
classify(42)
classify("hello")
classify(true)
classify(3.14)
}

v.(type) 是专门用在 switch 中的类型判断语法。


7. 接口变量的底层

7.1 接口变量包含两个信息

一个接口变量内部存储了两样东西:

  1. 具体类型:实际赋给它的类型
  2. 具体值:实际的值
1
2
var s Speaker = Dog{Name: "旺财"}
// s 内部:类型 = Dog,值 = Dog{Name: "旺财"}

7.2 nil 接口变量

1
2
3
4
5
6
var s Speaker       // s 是 nil 接口(类型和值都是 nil)
fmt.Println(s == nil) // true

var d *Dog
s = d // s 不是 nil(类型是 *Dog,值是 nil)
fmt.Println(s == nil) // false

这是一个常见的坑:把 nil 的具体类型赋给接口后,接口变量不等于 nil


8. 一段综合示例

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
package main

import (
"fmt"
"math"
)

// 接口:形状
type Shape interface {
Area() float64
Perimeter() float64
}

// 圆形
type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}

func (c Circle) String() string {
return fmt.Sprintf("圆形(半径=%.1f)", c.Radius)
}

// 矩形
type Rectangle struct {
Width, Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

func (r Rectangle) String() string {
return fmt.Sprintf("矩形(宽=%.1f, 高=%.1f)", r.Width, r.Height)
}

// 通用打印函数
func printShapeInfo(s Shape) {
fmt.Printf("%s\n", s)
fmt.Printf(" 面积: %.2f\n", s.Area())
fmt.Printf(" 周长: %.2f\n", s.Perimeter())
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Circle{Radius: 3},
}

fmt.Println("=== 形状信息 ===")
for _, s := range shapes {
printShapeInfo(s)
fmt.Println()
}

// 计算总面积
totalArea := 0.0
for _, s := range shapes {
totalArea += s.Area()
}
fmt.Printf("总面积: %.2f\n", totalArea)
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 形状信息 ===
圆形(半径=5.0)
面积: 78.54
周长: 31.42

矩形(宽=4.0, 高=6.0)
面积: 24.00
周长: 20.00

圆形(半径=3.0)
面积: 28.27
周长: 18.85

总面积: 130.81

这个示例展示了接口的核心价值:用统一的方式操作不同的具体类型


9. 常见坑总结

9.1 以为需要显式声明实现接口

1
2
3
// 不需要这样写:
type Dog struct{}
// implements Speaker ← Go 中没有这个

Go 中只要方法签名对得上,自动满足接口。

9.2 nil 接口和 nil 具体值

1
2
3
4
5
var d *Dog
var s Speaker = d

fmt.Println(d == nil) // true
fmt.Println(s == nil) // false(类型信息还在)

9.3 在接口中定义太多方法

1
2
3
4
5
6
7
8
// 不好的设计
type Animal interface {
Speak() string
Walk()
Eat(food string)
Sleep(hours int)
Age() int
}

接口越小越好。Go 标准库中最常用的接口通常只有 1~3 个方法:

  • io.Reader:1 个方法
  • io.Writer:1 个方法
  • fmt.Stringer:1 个方法

接口越大,抽象越弱。——Rob Pike

9.4 过早定义接口

不要在写代码第一天就定义一堆接口。先写具体类型,等到确实需要抽象时再提取接口。

Go 社区的名言:

Accept interfaces, return structs.(接收接口,返回结构体。)

9.5 接口变量比较

如果接口的底层类型可比较,接口可以用 == 比较:

1
2
3
var a Speaker = Dog{Name: "旺财"}
var b Speaker = Dog{Name: "旺财"}
fmt.Println(a == b) // true

但如果底层类型不可比较(如切片),会 panic。


10. 本课练习

练习 1:定义接口

要求:

  • 定义 Describer 接口,包含 Describe() string 方法
  • 创建 BookMovie 两个结构体,都实现 Describer
  • 写一个函数接收 Describer,打印描述信息

练习 2:空接口与类型断言

要求:

  • 写一个函数 describe(v interface{}),对 intstringfloat64bool 分别输出不同信息
  • switch v.(type) 实现

练习 3:鸭子类型验证

要求:

  • 定义 Singer 接口(Sing() string
  • 定义 Dancer 接口(Dance() string
  • 创建 Person 类型,同时实现两个接口
  • 验证同一个 Person 可以赋给 SingerDancer 两种接口变量

练习 4:接口切片

要求:

  • 定义 Shape 接口(Area() float64
  • 实现 CircleRectangleTriangle 三种形状
  • 创建 []Shape 切片,遍历计算总面积

练习 5:小接口设计

要求:

  • 设计一个 Validator 接口,只有一个 Validate() error 方法
  • EmailPhone 结构体实现该接口
  • 写一个通用函数,接收 Validator 并打印验证结果

11. 自测题

11.1 概念题

  1. 接口定义的是什么?
  2. Go 中一个类型怎么"实现"接口?
  3. 什么是鸭子类型?
  4. 空接口 interface{} 表示什么?
  5. 类型断言的语法是什么?怎么安全地做类型断言?
  6. 接口变量的 nil 和具体类型的 nil 有什么区别?
  7. Go 社区对接口设计有什么常见建议?
  8. 一个类型可以同时满足多个接口吗?
  9. 接口中能定义字段吗?
  10. switch v.(type) 语法是做什么的?

12. 本课总结

这一课你学到了 Go 的核心抽象能力——接口。

你现在应该已经理解:

  • 接口是一组方法的集合,定义"能做什么"
  • Go 用鸭子类型:有方法就满足接口,不需要显式声明
  • 接口让函数接受多种具体类型,实现通用逻辑
  • 空接口 interface{} 可以接收任何类型
  • 用类型断言 v.(Type) 从接口取回具体类型
  • 接口越小越好,不要过早定义接口
  • nil 具体值赋给接口后,接口不等于 nil

13. 下一课预告

下一课我们学习 Go 的核心设计哲学:组合优于继承

会重点讲:

  • 为什么 Go 没有继承
  • 结构体组合(嵌入)怎么替代继承
  • 接口 + 组合的配合使用
  • 和传统 OOP 继承的对比

学完下一课,你就能避免用旧语言的思维硬套 Go,真正理解 Go 的设计哲学。