第 6 章 · 接口与组合
本章目标:理解 interface 隐式实现;使用 io.Reader / io.Writer 抽象 IO;用 Reporter 接口 解耦 toolkit-go 报表输出;掌握组合 embedding 复用行为;理解空接口 any 与类型断言;对照 java-dev ch06 接口与 python-dev Protocol/ABC。
学时建议:3~4 小时(含 2 小时跟练)
前置:完成 go-dev ch05;建议已学 java-dev ch06。
6.1 场景说明:可插拔报表
toolkit-go(~/learn-go/toolkit-go,模块 example.com/toolkit-go)ch15 毕业项目要求:读取 access 日志 → 统计慢请求 → 输出 CSV 日报。不同输出格式(CSV / JSON / 控制台)应可替换,而不改核心统计逻辑——这正是 interface 的用武之地。
internal/
├── report/
│ ├── reporter.go # Reporter 接口 + SlowRow
│ ├── csv_reporter.go
│ ├── json_reporter.go
│ └── console_reporter.go
├── logparser/
│ └── parser.go # 依赖 io.Reader
└── model/
└── product.go # 可实现 fmt.Stringer
| 需求 | 抽象 | 实现 |
|---|---|---|
| 报表格式可换 | Reporter | CSV / JSON / Console |
| 输入源可换 | io.Reader | 文件、字符串、HTTP body |
| 输出目标可换 | io.Writer | 文件、Buffer、Response |
6.2 接口定义与隐式实现
Go 接口是方法集合;类型无需显式 implements,只要方法集匹配即实现。
// internal/report/reporter.go
package report
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io"
)
type SlowRow struct {
Path string
DurationMs int
Status int
}
type Reporter interface {
Generate(rows []SlowRow) ([]byte, error)
}
type Storer interface {
Save(data []byte) error
}
// internal/report/csv_reporter.go
type CSVReporter struct {
Title string
}
func (c CSVReporter) Generate(rows []SlowRow) ([]byte, error) {
var buf bytes.Buffer
w := csv.NewWriter(&buf)
_ = w.Write([]string{"path", "duration_ms", "status"})
for _, r := range rows {
_ = w.Write([]string{
r.Path,
fmt.Sprintf("%d", r.DurationMs),
fmt.Sprintf("%d", r.Status),
})
}
w.Flush()
return buf.Bytes(), w.Error()
}
| Java | Go | Python |
|---|---|---|
implements Reporter | 隐式满足方法集 | class X(Protocol) |
| 接口 + 抽象类 | 小接口 + 组合 struct | ABC + mixin |
| 编译期强制实现类声明 | 编译期检查使用方 | duck typing |
6.3 面向接口编程:RunReport
// internal/report/run.go
package report
import "fmt"
func RunReport(r Reporter, rows []SlowRow, w io.Writer) error {
out, err := r.Generate(rows)
if err != nil {
return fmt.Errorf("generate: %w", err)
}
if len(out) == 0 {
return nil
}
if w == nil {
return fmt.Errorf("writer is nil")
}
_, err = w.Write(out)
if err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}
// 调用方
func Demo() {
rows := []SlowRow{{Path: "/api/products", DurationMs: 800, Status: 200}}
_ = RunReport(CSVReporter{Title: "daily"}, rows, os.Stdout)
}
依赖倒置:高层 RunReport 依赖 Reporter 抽象,而非具体 CSV 实现。
6.4 ConsoleReporter 与 JSONReporter
// internal/report/console_reporter.go
type ConsoleReporter struct{}
func (ConsoleReporter) Generate(rows []SlowRow) ([]byte, error) {
for _, r := range rows {
fmt.Printf("[SLOW] %s %dms status=%d\n", r.Path, r.DurationMs, r.Status)
}
return nil, nil // 已直接打印,无字节返回
}
// internal/report/json_reporter.go
type JSONReporter struct{}
func (JSONReporter) Generate(rows []SlowRow) ([]byte, error) {
return json.Marshal(rows)
}
| 实现 | 输出 | 适用 |
|---|---|---|
| CSVReporter | 表格字节 | 文件 / 邮件附件 |
| JSONReporter | JSON 数组 | API / 前端 |
| ConsoleReporter | stdout | 本地调试 |
6.5 标准库接口:io.Reader / io.Writer
// internal/logparser/parser.go
package logparser
import (
"bufio"
"io"
"strconv"
"strings"
)
type SlowEntry struct {
Path string
DurationMs int
Status int
}
// 简化格式:path|duration_ms|status
func ParseSlowRequests(r io.Reader, thresholdMs int) ([]SlowEntry, error) {
scanner := bufio.NewScanner(r)
var out []SlowEntry
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Split(line, "|")
if len(parts) != 3 {
continue
}
ms, _ := strconv.Atoi(parts[1])
status, _ := strconv.Atoi(parts[2])
if ms >= thresholdMs {
out = append(out, SlowEntry{
Path: parts[0], DurationMs: ms, Status: status,
})
}
}
return out, scanner.Err()
}
| 接口 | 方法 | 常见实现 |
|---|---|---|
io.Reader | Read([]byte) (int, error) | 文件、strings.Reader、HTTP Body |
io.Writer | Write([]byte) (int, error) | 文件、bytes.Buffer、HTTP Response |
io.Closer | Close() error | 文件、连接 |
fmt.Stringer | String() string | 日志打印 |
测试友好:strings.NewReader(sample) 无需真实文件即可测 parser。
6.6 空接口 any 与类型断言
Go 1.18+ 用 any 作为 interface{} 别名。
func Describe(v any) string {
switch x := v.(type) {
case string:
return "str:" + x
case int:
return fmt.Sprintf("int:%d", x)
case Product: // 若在同包引用 model.Product
return "product:" + x.Slug
default:
return fmt.Sprintf("unknown:%T", v)
}
}
// 类型断言
var i any = "hello"
s, ok := i.(string)
if ok {
_ = s
}
原则:业务代码优先小接口;any 仅用于 JSON 解码、泛型边界等。
6.7 组合 struct 代替继承
// internal/report/base.go
type BaseReporter struct {
Title string
}
type TitledCSVReporter struct {