第19章 接口类型
第19章 接口类型
接口(Interface),Go语言的"魔法契约"!如果说类型是"房产证",那接口就是"职业资格证"——你不需要关心这个人姓甚名谁,只需要知道他能做什么。
19.1 接口的定义
19.1.1 接口声明语法
接口类型的声明形式和结构体类似,只是把字段换成方法签名。接口里写的是“方法长什么样”,不关心“谁来实现、怎么实现”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import "fmt"
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
func main() {
var w Writer
fmt.Printf("Writer接口初始值: %v\n", w) // <nil>
var r Reader
fmt.Printf("Reader接口初始值: %v, 动态类型: %T\n", r, r) // Reader接口初始值: <nil>, 动态类型: <nil>
}
|
19.1.2 接口命名惯例
| 惯例 | 示例 | 含义 |
|---|
-er结尾 | Reader, Writer | 表示"能做什么" |
19.2 空接口
19.2.1 interface{} 是什么
interface{} 是没有任何方法的接口。因为任何类型都至少满足零个方法,所以它可以是任意类型的容器——代价是取出来时必须做类型断言或反射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import "fmt"
func main() {
var i interface{}
i = 42
fmt.Printf("存整数: %d, 类型: %T\n", i, i) // 存整数: 42, 类型: int
i = "hello world"
fmt.Printf("存字符串: %s, 类型: %T\n", i, i) // 存字符串: hello world, 类型: string
i = []int{1, 2, 3}
fmt.Printf("存切片: %v, 类型: %T\n", i, i) // 存切片: [1 2 3], 类型: []int
i = map[string]int{"Java": 98}
fmt.Printf("存Map: %v, 类型: %T\n", i, i) // 存Map: map[Java:98], 类型: map[string]int
}
|
19.2.2 any 是 interface{} 的别名
从 Go 1.18 起,any 就是 interface{} 的别名(type any = interface{},注意有个等号)。两者完全等价,混用也不会报错——新代码建议统一写 any:
1
2
3
4
5
6
7
8
| package main
import "fmt"
func main() {
var data any = []int{1, 2, 3}
fmt.Printf("data: %v, 类型: %T\n", data, data) // data: [1 2 3], 类型: []int
}
|
19.2.3 泛型容器
用 any 做容器字段,可以装任何类型,但每次取用都要断言。Go 1.18 之后更好的选择是泛型——类型安全、没有装箱开销:
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
| package main
import "fmt"
type Container struct {
value any
}
func NewContainer(v any) *Container {
return &Container{value: v}
}
func (c *Container) Get() any {
return c.value
}
func main() {
containers := []*Container{
NewContainer(100),
NewContainer("Hello!"),
NewContainer([]int{1, 2, 3}),
}
for i, c := range containers {
fmt.Printf("容器%d: 值=%v, 类型=%T\n", i, c.Get(), c.Get())
}
// 容器0: 值=100, 类型=int
// 容器1: 值=Hello!, 类型=string
// 容器2: 值=[1 2 3], 类型=[]int
}
|
19.3 接口组合
19.3.1 接口嵌入
接口可以嵌入另一个接口,把对方的方法并入自己的方法集。嵌入 N 个接口,就要求实现方同时具备 N 个接口的全部方法:
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
| package main
import "fmt"
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 {
ReadWriter
Closer
}
type File struct {
name string
}
func (f *File) Read(p []byte) (n int, err error) {
fmt.Printf("[File] 读取文件 %s\n", f.name) // [File] 读取文件 data.txt
return len(p), nil
}
func (f *File) Write(p []byte) (n int, err error) {
fmt.Printf("[File] 写入文件 %s: %s\n", f.name, string(p)) // [File] 写入文件 data.txt: Hello
return len(p), nil
}
func (f *File) Close() error {
fmt.Printf("[File] 关闭文件 %s\n", f.name) // [File] 关闭文件 data.txt
return nil
}
func main() {
var rw ReadWriter = &File{name: "data.txt"}
rw.Read(make([]byte, 10))
rw.Write([]byte("Hello"))
fmt.Println()
var rwc ReadWriteCloser = &File{name: "log.txt"}
rwc.Read(make([]byte, 10))
rwc.Write([]byte("Log entry"))
rwc.Close()
}
|
19.3.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
| package main
import "fmt"
type A interface {
MethodA()
}
type B interface {
MethodB()
}
type C interface {
A
B
}
type MyStruct struct{}
func (s *MyStruct) MethodA() {
fmt.Println("MethodA 被调用") // MethodA 被调用
}
func (s *MyStruct) MethodB() {
fmt.Println("MethodB 被调用") // MethodB 被调用
}
func main() {
var c C = &MyStruct{}
c.MethodA()
c.MethodB()
}
|
19.4 接口设计原则
19.4.1 接口隔离原则
接口应该按调用方的需要来切分,而不是照着实现方的全部能力来定义。把大接口拆小,实现方要满足的方法更少,测试里也更容易造假实现:
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
| package main
import "fmt"
type Engine interface {
Start()
Stop()
}
type SpeedController interface {
Accelerate()
Brake()
}
type Car struct {
speed int
}
func (c *Car) Start() {
fmt.Println("发动机启动") // 发动机启动
}
func (c *Car) Stop() {
fmt.Println("发动机停止") // 发动机停止
}
func (c *Car) Accelerate() {
c.speed += 10
fmt.Printf("加速到 %d km/h\n", c.speed) // 加速到 10 km/h
}
func (c *Car) Brake() {
c.speed -= 10
fmt.Printf("减速到 %d km/h\n", c.speed) // 减速到 0 km/h
}
func main() {
car := &Car{}
var engine Engine = car
var speedCtrl SpeedController = car
engine.Start()
speedCtrl.Accelerate()
speedCtrl.Brake()
engine.Stop()
}
|
19.4.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
40
41
42
| package main
import (
"fmt"
"io"
)
type MemoryBuffer struct {
data []byte
off int // 当前读到的位置,io.Reader 必须自己记住进度
}
// Read 实现 io.Reader 约定:最多把 len(p) 个字节写进 p,
// 把实际读到的字节数作为 n 返回;读到结尾时返回 0, io.EOF。
func (b *MemoryBuffer) Read(p []byte) (n int, err error) {
if b.off >= len(b.data) {
return 0, io.EOF
}
n = copy(p, b.data[b.off:])
b.off += n
return n, nil
}
func (b *MemoryBuffer) Write(p []byte) (n int, err error) {
b.data = append(b.data, p...)
return len(p), nil
}
func main() {
buffer := &MemoryBuffer{data: []byte("Hello, ")}
var rw io.ReadWriter = buffer
rw.Write([]byte("World!"))
// io.ReadAll 会一直读到 Read 返回 io.EOF 为止
out, err := io.ReadAll(rw)
fmt.Printf("读取内容: %s (err=%v)\n", out, err) // 读取内容: Hello, World! (err=<nil>)
// 再读一次已经到末尾,会立刻拿到 io.EOF
n, err := rw.Read(make([]byte, 8))
fmt.Printf("末尾再读: n=%d, err=%v\n", n, err) // 末尾再读: n=0, err=EOF
}
|
19.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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| package main
import "fmt"
type Persistence interface {
Save(data []byte) error
Load() ([]byte, error)
}
type Service struct {
storage Persistence
}
func NewService(p Persistence) *Service {
return &Service{storage: p}
}
func (s *Service) ProcessAndSave(data []byte) error {
processed := append(data, []byte("_processed")...)
return s.storage.Save(processed)
}
type FileStorage struct {
filename string
}
func (f *FileStorage) Save(data []byte) error {
fmt.Printf("[FileStorage] 保存到文件 %s: %s\n", f.filename, string(data)) // [FileStorage] 保存到文件 data.bin: 原始数据_processed
return nil
}
func (f *FileStorage) Load() ([]byte, error) {
fmt.Printf("[FileStorage] 从文件 %s 加载\n", f.filename) // [FileStorage] 从文件 data.bin 加载
return []byte("loaded data"), nil
}
func main() {
storage := &FileStorage{filename: "data.bin"}
service := NewService(storage)
err := service.ProcessAndSave([]byte("原始数据"))
if err != nil {
fmt.Println("错误:", err)
}
}
|
19.5 标准库常用接口
19.5.1 io 包接口
io.Reader
io.Reader 是 Go 里最经典的接口:一次读一些字节,读到末尾返回 io.EOF。它是流式读取一切数据的统一入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package main
import (
"bytes"
"fmt"
"io"
)
func main() {
data := "Hello, Go I/O!"
buffer := bytes.NewBufferString(data)
var reader io.Reader = buffer
p := make([]byte, 1024)
n, err := reader.Read(p)
fmt.Printf("读取了 %d 字节: %q\n", n, string(p[:n])) // 读取了 16 字节: "Hello, Go I/O!"
n, err = reader.Read(p)
fmt.Printf("再次读取: n=%d, err=%v\n", n, err) // 再次读取: n=0, err=<nil>
}
|
io.Writer
io.Writer 与 io.Reader 对称:把一段字节写出去。n 是实际写入的字节数,小于 len(p) 时一定要当成错误处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package main
import (
"bytes"
"fmt"
"io"
)
func main() {
buffer := new(bytes.Buffer)
var writer io.Writer = buffer
n, err := writer.Write([]byte("Hello, "))
fmt.Printf("写入 %d 字节, err: %v\n", n, err) // 写入 6 字节, err: <nil>
n, err = writer.Write([]byte("World!"))
fmt.Printf("写入 %d 字节, err: %v\n", n, err) // 写入 6 字节, err: <nil>
fmt.Printf("缓冲区内容: %s\n", buffer.String()) // 缓冲区内容: Hello, World!
}
|
io.Closer
io.Closer 只有一个方法。它常和 Reader/Writer 组合使用,用于释放文件句柄、网络连接这类系统资源:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import (
"bytes"
"fmt"
"io"
)
func main() {
// bytes.Buffer 没有 Close 方法,所以它并不满足 io.Closer!
buffer := new(bytes.Buffer)
// var closer io.Closer = buffer // ❌ 编译错误:*bytes.Buffer does not implement io.Closer (missing method Close)
// 标准库提供了 io.NopCloser:把任何 io.Reader 包一层,Close 时什么都不做
var closer io.Closer = io.NopCloser(buffer)
err := closer.Close()
fmt.Printf("Close err: %v\n", err) // Close err: <nil>
}
|
io.Seeker
io.Seeker 表示“可以随机跳转定位”。普通的 socket、管道都不支持它,只有文件、bytes.Reader、strings.Reader 这类才算:
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
| package main
import (
"bytes"
"fmt"
"io"
)
func main() {
data := []byte("0123456789")
reader := bytes.NewReader(data) // bytes.Reader 实现了 io.Seeker
var seeker io.Seeker = reader
// 注意:bytes.Buffer 并没有实现 io.Seeker,因为它不支持随机定位
p := make([]byte, 5)
n, _ := reader.Read(p)
fmt.Printf("读取: %q (n=%d), 剩余: %d 字节\n", string(p), n, reader.Len()) // 读取: "01234" (n=5), 剩余: 5 字节
offset, err := seeker.Seek(-2, io.SeekCurrent)
fmt.Printf("Seek -2: 位置=%d, err=%v\n", offset, err) // Seek -2: 位置=3, err=<nil>
n, _ = reader.Read(p[:3])
fmt.Printf("再读取: %q (n=%d)\n", string(p[:3]), n) // 再读取: "345" (n=3)
}
|
19.5.2 fmt 包接口
fmt.Stringer
实现了 String() string 的类型,在 fmt 打印时会被自动调用。这是给类型“定制默认输出格式”的标准做法:
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
| package main
import "fmt"
type Color string
const (
Red Color = "红色"
Green Color = "绿色"
Blue Color = "蓝色"
)
func (c Color) String() string {
return string(c)
}
func main() {
colors := []Color{Red, Green, Blue}
fmt.Println("=== fmt.Stringer 演示 ===")
for _, c := range colors {
fmt.Printf("颜色: %s\n", c)
}
fmt.Printf("\n%v 格式: %v\n", Red, Red) // 红色
fmt.Printf("%s 格式: %s\n", Red, Red) // 红色
}
|
fmt.GoStringer
GoString() string 是 %#v 格式调用的方法,用来输出像 Go 源码一样的表示形式,方便调试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| package main
import "fmt"
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("Point(%d,%d)", p.X, p.Y)
}
func (p Point) GoString() string {
return fmt.Sprintf("main.Point{X:%d, Y:%d}", p.X, p.Y)
}
func main() {
p := Point{X: 10, Y: 20}
fmt.Printf("%v 格式: %v\n", p, p) // Point(10,20)
fmt.Printf("%#v 格式: %#v\n", p, p) // main.Point{X:10, Y:20}
}
|
19.5.3 sort 包接口
sort 包用 sort.Interface 抽象“可排序”这件事,只要实现三个方法,任何容器都能被排序。Go 1.21 之后更推荐用泛型的 slices.SortFunc,不再需要定义类型:
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
| package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
type PersonSlice []Person
func (p PersonSlice) Len() int { return len(p) }
func (p PersonSlice) Less(i, j int) bool { return p[i].Age < p[j].Age }
func (p PersonSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func main() {
people := PersonSlice{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
fmt.Println("=== 排序前(按年龄)===")
for _, p := range people {
fmt.Printf(" %s: %d岁\n", p.Name, p.Age)
}
sort.Sort(people)
fmt.Println("\n=== 排序后 ===")
for _, p := range people {
fmt.Printf(" %s: %d岁\n", p.Name, p.Age)
}
// === 排序前(按年龄)===
// Alice: 30岁
// Bob: 25岁
// Charlie: 35岁
//
// === 排序后 ===
// Bob: 25岁
// Alice: 30岁
// Charlie: 35岁
}
|
19.6 接口实现机制
19.6.1 接口的内部结构
接口值在运行时由类型信息和数据指针两部分组成。这一节用 reflect 把这两部分“看”出来:
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
| package main
import (
"fmt"
"reflect"
)
type Writer interface {
Write(p []byte) (n int, err error)
}
type File struct {
name string
}
func (f *File) Write(p []byte) (n int, err error) {
return len(p), nil
}
func main() {
var w Writer = &File{name: "test.txt"}
fmt.Printf("接口类型: %T\n", w) // 接口类型: *main.File
fmt.Printf("接口值: %v\n", w) // 接口值: &{test.txt}
t := reflect.TypeOf(w)
if t != nil {
fmt.Printf("动态类型: %s\n", t.Name()) // 动态类型: File
fmt.Printf("动态类型种类: %v\n", t.Kind()) // 动态类型种类: ptr
}
}
|
19.6.2 接口缓存
fmt 之类的库会缓存类型信息,避免每次调用都重新查表。这一节用一个小实验看看缓存的收益:
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
| package main
import (
"fmt"
"io"
"time"
)
type Counter struct {
count int
}
func (c *Counter) Write(p []byte) (n int, err error) {
c.count++
return len(p), nil
}
func main() {
var w io.Writer = &Counter{}
start := time.Now()
for i := 0; i < 1_000_000; i++ {
w.Write([]byte("test"))
}
elapsed := time.Since(start)
fmt.Printf("接口调用100万次耗时: %v\n", elapsed) // 接口调用100万次耗时: ~200ms
fmt.Printf("计数器值: %d\n", w.(*Counter).count) // 计数器值: 1000000
}
|
19.7 空接口详解
19.7.1 反射与空接口
反射是“在运行时查看和操作类型信息”的能力,它的入口正是空接口。代价是慢、容易 panic、编译期检查不到:
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
| package main
import (
"fmt"
"reflect"
)
func inspectInterface(i interface{}) {
val := reflect.ValueOf(i)
typ := reflect.TypeOf(i)
fmt.Printf("类型: %v\n", typ) // 类型: int / string / main.Person
fmt.Printf("种类(Kind): %v\n", val.Kind())
}
type Person struct {
Name string
Age int
}
func main() {
fmt.Println("=== 整数 ===")
inspectInterface(42)
fmt.Println("\n=== 字符串 ===")
inspectInterface("hello")
fmt.Println("\n=== 结构体 ===")
person := Person{Name: "小明", Age: 18}
inspectInterface(person)
}
|
19.7.2 any 类型演进
从 interface{} 到 any,再到泛型,Go 处理“任意类型”的手段在十年里演进了三次。这一节对比它们的取舍:
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
| package main
import "fmt"
type Stack[T any] struct {
data []T
}
func (s *Stack[T]) Push(v T) {
s.data = append(s.data, v)
}
func (s *Stack[T]) Pop() T {
if len(s.data) == 0 {
var zero T
return zero
}
v := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return v
}
func main() {
fmt.Println("=== 泛型Stack演示 ===")
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Printf("弹出: %d\n", intStack.Pop()) // 弹出: 3
fmt.Printf("弹出: %d\n", intStack.Pop()) // 弹出: 2
fmt.Printf("弹出: %d\n", intStack.Pop()) // 弹出: 1
}
|
本章小结
本章我们学习了Go接口:
接口基础:
空接口(any):
- 任何类型都满足空接口
- Go 1.18+推荐使用any
接口组合:
设计原则:
标准库重要接口:
io.Reader/Writer/Closerfmt.Stringersort.Interface