io.readcloser 是 go 标准库中组合 io.reader 和 io.closer 的接口,体现接口嵌入而非装饰模式;真正的装饰模式由用户实现——通过封装 io.readcloser 类型并选择性重写方法(如带计数、超时、解压等),在不修改原值前提下增强行为,且必须透传 close 以避免资源泄漏。

什么是 io.ReadCloser,它和装饰模式有什么关系
io.ReadCloser 本身不是装饰器,而是一个接口:它组合了 io.Reader 和 io.Closer。真正实现“包装+增强行为”的,是你自己写的、返回 io.ReadCloser 的封装类型——这正是装饰模式的 Go 实现方式:不修改原值,而是用新类型包裹它,并选择性地重写方法。
常见场景包括:日志读取内容、统计字节数、自动解压、超时控制、限速、错误重试等。关键点在于:你必须显式调用被包装对象的 Read 和 Close 方法,否则底层资源不会释放或数据不会流动。
写一个基础装饰器:带计数的 ReadCloser
下面这个类型在每次 Read 后累加已读字节数,并透传 Close:
type CountingReadCloser struct {
io.ReadCloser
N int64
}
<p>func (c *CountingReadCloser) Read(p []byte) (n int, err error) {
n, err = c.ReadCloser.Read(p)
c.N += int64(n)
return
}
</p>
注意三点:
- 嵌入
io.ReadCloser是为了自动继承未重写的方法(比如如果原值有ReadByte,这里也能用) - 不能直接嵌入
io.Reader+io.Closer,否则类型断言io.ReadCloser会失败——Go 接口实现是隐式的,但嵌入必须精确匹配接口定义 - 构造函数必须接收原始
io.ReadCloser,例如:return &CountingReadCloser{rc, 0}
为什么 Close 必须透传,且不能延迟或忽略
很多 bug 出在忘记调用底层 Close,导致文件句柄泄漏、HTTP 连接不复用、gzip reader 未清理缓冲区等。典型错误:
- 在装饰器
Close中只做日志,不调c.ReadCloser.Close() - 用
defer在Read内部关掉底层(错:Read可能被多次调用) - 把
io.ReadCloser转成io.Reader后丢弃Close能力(例如传给只接受io.Reader的函数后忘了关)
正确做法始终是:
func (c *CountingReadCloser) Close() error {
return c.ReadCloser.Close() // 必须调用
}
多个装饰器叠加时的顺序和性能影响
Go 没有语法糖支持链式装饰,只能手动嵌套,比如:
rc := &CountingReadCloser{
ReadCloser: &TimeoutReadCloser{
ReadCloser: httpResp.Body,
Timeout: 5 * time.Second,
},
}
这时调用 rc.Read 会依次进入 CountingReadCloser.Read → TimeoutReadCloser.Read → httpResp.Body.Read。要注意:
- 每层额外函数调用有微小开销,高频小读取(如逐字节解析)下可观测
- 错误处理要明确归属:
TimeoutReadCloser应把超时错误包装为os.ErrDeadlineExceeded,而非透传底层原始错误 - 别在装饰器里做阻塞操作(如网络请求、锁竞争),否则会卡住整个读取流程
最易被忽略的是:所有装饰器都必须保证 Read 行为符合 io.Reader 合约——比如返回 n == 0 && err == nil 是合法的(表示无数据但没结束),但若某层误判为 EOF 就会提前终止流。











