1. 1.1 基本的 IO 接口
io 包为 I/O 原语提供了基本的接口 主要包装了 原语的已有实现
由于这些被接口包装的I/O原语是由不同的低级操作实现 因此 在另有声明之前不该假定 并行 执行是安全的
在 io 包中最重要的是两个接口 Reader 和 Writer 接口
本章所提到的各种 IO 包 都跟这两个接口有关 也就是说 只要满足这两个接口 就可以使用 IO 包的功能

1.1. Reader 接口 定义如下
type Reader interface {
   Read(p []byte) (n int, err error)
}
 
Read 将 len(p) 个字节读取到 p 中
返回读取的字节数 n(0 <= n <= len(p))

任何遇到的错误
即使 Read 返回的 n < len(p)  也会在调用过程中占用 len(p) 个字节作为暂存空间
若可读取的数据不到 len(p) 个字节 Read 会返回可用数据
而不是等待更多数据

当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF (end-of-file)  会返回读取的字节数
可能会同时在本次的调用中返回一个non-nil错误
或在下一次的调用中返回这个错误(且 n 为 0)
一般情况下, Reader会返回一个非0字节数n
若 n = len(p) 个字节从输入源的结尾处由 Read 返回
Read可能返回 err == EOF 或者 err == nil
并且之后的 Read() 都应该返回 (n:0, err:EOF)

调用者在考虑错误之前应当首先处理返回的数据
这样做可以正确地处理在读取一些字节后产生的 I/O 错误
同时允许EOF的出现

根据 Go 语言中关于接口和满足了接口的类型的定义(Interface_types)
知道 Reader 接口的方法集(Method_sets)只包含一个 Read 方法
因此 所有实现了 Read 方法的类型都满足 io.Reader 接口
也就是说 在所有需要 io.Reader 的地方 可以传递实现了 Read() 方法的类型的实例

例谈该接口的用法
func ReadFrom(reader io.Reader, num int) ([]byte, error) {
    p := make([]byte, num)
    n, err := reader.Read(p)
    if n > 0 {
        return p[:n], nil
    }
    return p, err
}
ReadFrom 函数将 io.Reader 作为参数
也就是说 ReadFrom 可以从任意的地方读取数据
只要来源实现了 io.Reader 接口
比如 可以从标准输入 文件 字符串等读取数据

示例
data, err = ReadFrom(os.Stdin, 11)// 从标准输入读取
data, err = ReadFrom(file, 9)// 从普通文件读取 其中 file 是 os.File 的实例
data, err = ReadFrom(strings.NewReader("from string"), 12)// 从字符串读取

完整的演示例子源码见 code/src/chapter01/io/reader.go

小贴士
io.EOF 变量的定义 var EOF = errors.New("EOF") 是 error 类型
根据 reader 接口的说明 在 n > 0 且数据被读完了的情况下
当次返回的 error 有可能是 EOF 也有可能是 nil

1.2. Writer 接口 定义如下
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
Write 将 len(p) 个字节从 p 中写入到基本数据流中
返回从 p 中被写入的字节数 n(0 <= n <= len(p))
任何遇到的引起写入提前停止的错误 若 Write 返回的 n < len(p)  就必须返回一个 非nil 的错误
所有实现了 Write 方法的类型都实现了 io.Writer 接口

fmt标准库 有一组函数 Fprint/Fprintf/Fprintln 接收一个 io.Wrtier 类型参数(第一个参数)
将数据格式化输出到 io.Writer 中那么 调用这组函数时 该如何传递这个参数呢?
以 fmt.Fprintln 为例 同时看一下 fmt.Println 函数的源码
func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}
很显然 fmt.Println会将内容输出到标准输出中

1.3. 实现了 io.Reader 接口或 io.Writer 接口的类型
标准库中有哪些类型实现了 io.Reader 或 io.Writer 接口?
os.File 同时实现了这两个接口  os.Stdin/Stdout 分别实现了 io.Reader/io.Writer 接口
实际上在 os 包中有这样的代码
var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
也就是说 Stdin/Stdout/Stderr 只是三个特殊的文件类型的标识(即都是 os.File 的实例) 自然也实现了 io.Reader 和 io.Writer

可以通过查看标准库文档 列出实现了 io.Reader 或 io.Writer 接口的类型(导出的类型)
注 godoc 命令支持额外参数 -analysis  能列出都有哪些类型实现了某个接口
相关参考
godoc -h 或 Static analysis features of godoc

os.File 同时实现了 io.Reader 和 io.Writer
strings.Reader 实现了 io.Reader
bufio.Reader/Writer 分别实现了 io.Reader 和 io.Writer
bytes.Buffer 同时实现了 io.Reader 和 io.Writer
bytes.Reader 实现了 io.Reader
compress/gzip.Reader/Writer 分别实现了 io.Reader 和 io.Writer
crypto/cipher.StreamReader/StreamWriter 分别实现了 io.Reader 和 io.Writer
crypto/tls.Conn 同时实现了 io.Reader 和 io.Writer
encoding/csv.Reader/Writer 分别实现了 io.Reader 和 io.Writer
mime/multipart.Part 实现了 io.Reader
net/conn 分别实现了 io.Reader 和 io.Writer(Conn接口定义了Read/Write)

除此之外 io 包本身也有这两个接口的实现类型如
实现了 Reader 的类型 LimitedReader PipeReader SectionReader
实现了 Writer 的类型 PipeWriter
常用的类型有 os.File strings.Reader bufio.Reader/Writer bytes.Buffer bytes.Reader

小贴士
Go 接口的命名约定 接口名以 er 结尾
非强行要求 可以不以 er 结尾 标准库中有 接口 不是以 er 结尾的

1.4. ReaderAt 和 WriterAt 接口
type ReaderAt interface {
    ReadAt(p []byte, off int64) (n int, err error)
}
 
ReadAt 从基本输入源的偏移量 off 处开始 将 len(p) 个字节读取到 p 中 返回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误
当 ReadAt 返回的 n < len(p) 时  就会返回一个 非nil 的错误来解释 为什么没有返回更多的字节在这一点上 ReadAt 比 Read 更严格
即使 ReadAt 返回的 n < len(p)  也会在调用过程中使用 p 的全部作为暂存空间 若可读取的数据不到 len(p) 字节 ReadAt 就会阻塞 直到所有数据都可用或一个错误发生 在这一点上 ReadAt 不同于 Read
若 n = len(p) 个字节从输入源的结尾处由 ReadAt 返回 Read可能返回 err == EOF 或者 err == nil
若 ReadAt 携带一个偏移量从输入源读取 ReadAt 应当既不影响偏移量也不被 影响
可对相同的输入源并行执行 ReadAt 调用
可见 ReaderAt 接口使得可以从指定偏移量处开始读取数据
reader := strings.NewReader("Go语言中文网")
p := make([]byte, 6)
n, err := reader.ReadAt(p, 2)
if err != nil {
    panic(err)
}
fmt.Printf("%s, %d\n", p, n)//语言, 6

WriterAt 接口的定义如下
type WriterAt interface {
    WriteAt(p []byte, off int64) (n int, err error)
}
 
WriteAt 从 p 中将 len(p) 个字节写入到偏移量 off 处的基本数据流中
返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误若 WriteAt 返回的 n < len(p)  就必须返回一个 非nil 的错误
若 WriteAt 携带一个偏移量写入到目标中 WriteAt 应当既不影响偏移量也不被影响
若被写区域没有重叠 可对相同的目标并行执行 WriteAt 调用
可以通过该接口将数据写入到数据流的特定偏移量之后

WriteAt 方法的使用 os.File 实现了 WriterAt 接口
file, err := os.Create("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()
file.WriteString("Golang中文社区")
n, err := file.WriteAt([]byte("Go语言中文网"), 24)
if err != nil {
    panic(err)
}
fmt.Println(n)
打开文件 WriteAt.txt 内容是 Golang中文社区 Go语言中文网

file.WriteString("Golang中文社区") 往文件中写入 Golang中文社区 之后 file.WriteAt([]byte("Go语言中文网"), 24) 在文件流的 offset=24 处写入 Go语言中文网 覆盖该位置的内容

1.5. ReaderFrom 和 WriterTo 接口
type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}
 
ReadFrom 从 r 中读取数据 直到 EOF 或发生错误
返回值 n 为读取的字节数 除io.EOF外 在读取过程中遇到的任何错误也将被返回
如果 ReaderFrom 可用 Copy 函数就会使用
注意 ReadFrom 方法不会返回 err == EOF

将文件中的数据全部读取(显示在标准输出)
file, err := os.Open("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()
当然 可以通过 ioutil 包的 ReadFile 函数获取文件全部内容其实 跟踪一下 ioutil.ReadFile 的源码 会发现其实也是通过 ReadFrom 方法实现(用的是 bytes.Buffer  实现了 ReaderFrom 接口)
如果不通过 ReadFrom 接口来做这件事 而是使用 io.Reader 接口 有两种思路
先获取文件的大小(File 的 Stat 方法) 之后定义一个该大小的 []byte 通过 Read 一次性读取
定义一个小的 []byte 不断的调用 Read 方法直到遇到 EOF 将所有读取到的 []byte 连接到一起
查看 bufio.Writer 或 strings.Buffer 类型的 ReadFrom 方法实现 会发现 实现和上面说的第 2 种思路类似

WriterTo 的定义
type WriterTo interface {
    WriteTo(w Writer) (n int64, err error)
}
 
WriteTo 将数据写入 w 中 直到没有数据可写或发生错误 返回值 n 为写入的字节数 在写入过程中遇到的任何错误也将被返回
如果 WriterTo 可用 Copy 函数就会使用
ReaderFrom 和 WriterTo 接口的方法接收的参数是 io.Reader 和 io.Writer 类型

将一段文本输出到标准输出
reader := bytes.NewReader([]byte("Go语言中文网"))
reader.WriteTo(os.Stdout)
如果这样的需求 可以 使用这两个接口 “一次性从某个地方读或写到某个地方去”

1.6. Seeker 接口
type Seeker interface {
    Seek(offset int64, whence int) (ret int64, err error)
}
 
Seek 设置下一次 Read 或 Write 的偏移量为 offset 解释取决于 whence  0 表示相对于文件的起始处 1 表示相对于当前的偏移 而 2 表示相对于其结尾处 Seek 返回新的偏移量和一个错误 如果有的话
也就是说 Seek 方法是用于设置偏移量的 这样可以从某个特定位置开始操作数据流听起来和 ReaderAt/WriteAt 接口有些类似 不过 Seeker 接口更灵活 可以更好的控制读写数据流的位置

获取倒数第二个字符(需要考虑 UTF-8 编码)
reader := strings.NewReader("Go语言中文网")//一个UTF-8 编码中文 占3个字符
reader.Seek(-6, io.SEEK_END)
r, _, _ := reader.ReadRune()
fmt.Printf("%c\n", r)//文


whence 的值 在 io 包中定义了相应的常量 应该使用这些常量
const (
  SeekStart   = 0 // seek relative to the origin of the file
  SeekCurrent = 1 // seek relative to the current offset
  SeekEnd     = 2 // seek relative to the end
)
而原先 os 包中的常量已经被标注为 Deprecated
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
  SEEK_SET int = 0 // seek relative to the origin of the file
  SEEK_CUR int = 1 // seek relative to the current offset
  SEEK_END int = 2 // seek relative to the end
)

1.7. Closer接口
type Closer interface {
    Close() error
}
只有一个 Close() 方法 用于关闭数据流
文件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要手动关闭的资源都实现了 Closer 接口
编程中 常将 Close 方法的调用放在 defer 语句中 在 Close 之前校验错误是个好习惯!
func (f *File) Close() error {
    if f == nil {
        return ErrInvalid
    }
    return f.file.close()
}

1.8. 其他接口
1.8.1. ByteReader 和 ByteWriter 读或写一个字
type ByteReader interface {
    ReadByte() (c byte, err error)
}
type ByteWriter interface {
    WriteByte(c byte) error
}
在标准库中 有如下类型实现了 io.ByteReader 或 io.ByteWriter:
bufio.Reader/Writer 分别实现了io.ByteReader 和 io.ByteWriter
bytes.Buffer 同时实现了 io.ByteReader 和 io.ByteWriter
bytes.Reader 实现了 io.ByteReader
strings.Reader 实现了 io.ByteReader

通过 bytes.Buffer 来一次读取或写入一个字节
var ch byte
fmt.Scanf("%c\n", &ch)
buffer := new(bytes.Buffer)
err := buffer.WriteByte(ch)
if err == nil {
    fmt.Println("写入一个字节成功!准备读取该字节")
    newCh, _ := buffer.ReadByte()
    fmt.Printf("读取的字节 %c\n", newCh)
} else {
    fmt.Println("写入错误")
}
程序从标准输入接收一个字节(ASCII 字符) 调用 buffer 的 WriteByte 将该字节写入 buffer 中 之后通过 ReadByte 读取该字节
这两个接口有哪些用处呢?
在标准库 encoding/binary 中 实现Google-ProtoBuf中的 Varints 读取 ReadVarint 就需要一个 io.ByteReader 类型的参数 需要一个字节一个字节的读取
在标准库 image/jpeg 中 Encode 函数的内部实现使用了 ByteWriter 写入一个字节
通过在 Go 语言源码 src/pkg 中搜索 io.ByteReader 或 io.ByteWiter 获得哪些地方用到了这两个接口 发现 这两个接口在二进制数据或归档压缩时用的比较多

1.8.2. ByteScanner RuneReader 和 RuneScanner 将这三个接口放在一起 是考虑到与 ByteReader 相关或相应
type ByteScanner interface {
    ByteReader
    UnreadByte() error
}
内嵌了 ByteReader 接口(可以理解为继承了 ByteReader 接口) UnreadByte 方法的意思是 将上一次 ReadByte 的字节还原 使得再次调用 ReadByte 返回的结果和上一次调用相同 也就是说 UnreadByte 是重置上一次的 ReadByte注意 UnreadByte 调用之前必须调用了 ReadByte 且不能连续调用 UnreadByte即
buffer := bytes.NewBuffer([]byte{'a', 'b'})
err := buffer.UnreadByte()

buffer := bytes.NewBuffer([]byte{'a', 'b'})
buffer.ReadByte()
err := buffer.UnreadByte()
err = buffer.UnreadByte()
err 都 非nil 错误为 bytes.Buffer: UnreadByte: previous operation was not a read
RuneReader 接口和 ByteReader 类似 只是 ReadRune 方法读取单个 UTF-8 字符 返回其 rune 和该字符占用的字节数该接口在 regexp 包有用到
strings.Index("行业交流群", "交流") 返回的是单字节字符的位置 6 但是想要的是 unicode 字符的位置 2
这里借助utf8的RuneCountInString函数 // strings.Index 的 UTF-8 版本 即 Utf8Index("Go语言中文网", "中文") 返回 4 而不是 strings.Index 的 8
func Utf8Index(str, substr string) int {
    index := strings.Index(str, substr)
    if index < 0{
        return -1
    }
    return utf8.RuneCountInString(str[:index])
}
RuneScanner 接口和 ByteScanner 类似

1.8.3. ReadCloser ReadSeeker ReadWriteCloser ReadWriteSeeker ReadWriter WriteCloser 和 WriteSeeker 接口
这些接口是上面介绍的接口的两个或三个组合而成的新接口例如 ReadWriter 接口
type ReadWriter interface {
    Reader
    Writer
}
这是 Reader 接口和 Writer 接口的简单组合(内嵌) 有些时候同时需要某两个接口的所有功能 即必须同时实现了某两个接口的类型才能够被传入使用
可见 io 包中有大量的“小接口” 这样方便组合为“大接口”

1.9. SectionReader 类型
SectionReader 是一个 struct(没有任何导出的字段) 实现了 Read Seek 和 ReadAt 同时 内嵌了 ReaderAt 接口结构定义如下
type SectionReader struct {
    r     ReaderAt    // 该类型最终的 Read/ReadAt 最终都是通过 r 的 ReadAt 实现
    base  int64        // NewSectionReader 会将 base 设置为 off
    off   int64        // 从 r 中的 off 偏移处开始读取数据
    limit int64        // limit - off = SectionReader 流的长度
}

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader 文档说明
NewSectionReader 返回一个 SectionReader  从 r 中的偏移量 off 处读取 n 个字节后 以 EOF 停止
也就是说 SectionReader 只是内部(内嵌)ReaderAt 表示的数据流的一部分 从 off 开始后的 n 个字节
这个类型的作用是 方便重复操作某一段 (section) 数据流;或者同时需要 ReadAt 和 Seek 的功能

1.10. LimitedReader 类型
type LimitedReader struct {
    R Reader // underlying reader 最终的读取操作通过 R.Read 完成
    N int64  // max bytes remaining
}
从 R 读取但将返回的数据量限制为 N 字节 每次调用 Read 都将更新 N 来反应新的剩余数量 最多只能返回 N 字节数据
LimitedReader 只实现了 Read 方法(Reader 接口)
content := "This Is LimitReader Example"
reader := strings.NewReader(content)
limitReader := &io.LimitedReader{R: reader, N:7}
for limitReader.N > 0 {
    tmp := make([]byte, 2)
    limitReader.Read(tmp)
    fmt.Printf("[%s : %d]", tmp , limitReader.N)
}//[Th : 5][is : 3][ I : 1][s : 0] 可见 通过该类型 达到 只允许读取一定长度数据 的目的
在 io 包中 LimitReader 函数的实现 就是调用 LimitedReader
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

1.11. PipeReader 和 PipeWriter 类型
PipeReader(一个没有任何导出字段的 struct)是管道的读取端 实现了 io.Reader 和 io.Closer
type PipeReader struct {
    p *pipe
}
PipeReader.Read 方法 从管道中读取数据 该方法会堵塞 直到管道写入端开始写入数据或写入端被关闭 如果写入端关闭时带有 error(即调用 CloseWithError 关闭) 该Read返回的 err 就是写入端传递的error;否则 err 为 EOF

PipeWriter(一个没有任何导出字段的 struct)是管道的写入端 实现了 io.Writer 和 io.Closer
type PipeWriter struct {
    p *pipe
}
PipeWriter.Write 方法 写数据到管道中该方法会堵塞 直到管道读取端读完所有数据或读取端被关闭 如果读取端关闭时带有 error(即调用 CloseWithError 关闭) 该Write返回的 err 就是读取端传递的error;否则 err 为 ErrClosedPipe
func main() {
    pipeReader, pipeWriter := io.Pipe()
    go PipeWrite(pipeWriter)
    go PipeRead(pipeReader)
    time.Sleep(30 * time.Second)
}
func PipeWrite(writer *io.PipeWriter){
    data := []byte("Go语言中文网")
    for i := 0; i < 3; i++{
        n, err := writer.Write(data)
        if err != nil{
            fmt.Println(err)
            return
        }
        fmt.Printf("写入字节 %d\n",n)
    }
    writer.CloseWithError(errors.New("写入段已关闭"))
}
func PipeRead(reader *io.PipeReader){
    buf := make([]byte, 128)
    for{
        fmt.Println("接口端开始阻塞5秒钟...")
        time.Sleep(5 * time.Second)
        fmt.Println("接收端开始接受")
        n, err := reader.Read(buf)
        if err != nil{
            fmt.Println(err)
            return
        }
        fmt.Printf("收到字节: %d\n buf内容: %s\n",n,buf)
    }
}
io.Pipe() 用于创建一个同步的内存管道 (synchronous in-memory pipe) 函数签名
func Pipe() (*PipeReader, *PipeWriter)
将 io.Reader 连接到 io.Writer 一端的读取匹配 另一端的写入 直接在这两端之间复制数据; 没有内部缓存 对于并行调用 Read 和 Write 以及 函数或 Close 来说都是安全的一旦等待的 I/O 结束 Close 就会完成并行调用 Read 或并行调用 Write 也同样安全 同种类的调用将按顺序进行控制
正因为是同步的 因此不能在一个 goroutine 中进行读和写
另外 对于管道的 close 方法(非 CloseWithError 时) err 会被置为 EOF

1.12. Copy 和 CopyN 函数
func Copy(dst Writer, src Reader) (written int64, err error)
 
Copy 将 src 复制到 dst 直到在 src 上到达 EOF 或发生错误 返回复制的字节数 如果有错误的话 还会返回在复制时遇到的第一个错误
成功的 Copy 返回 err == nil 而非 err == EOF 由于 Copy 被定义为从 src 读取直到 EOF 为止 因此 不会将来自 Read 的 EOF 当做错误来报告
若 dst 实现了 ReaderFrom 接口 其复制操作可通过调用 dst.ReadFrom(src) 实现 此外 若 src 实现了 WriterTo 接口 其复制操作可通过调用 src.WriteTo(dst) 实现
 
io.Copy(os.Stdout, strings.NewReader("Go语言中文网"))
直接将内容输出(写入 Stdout 中) 甚至可以这么做
package main
import (
    "fmt"
    "io"
    "os"
)
func main() {
    io.Copy(os.Stdout, os.Stdin)
    fmt.Println("Got EOF -- bye")
}
执行 echo "Hello, World" | go run main.go

CopyN 函数
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
CopyN 将 n 个字节(或到一个error)从 src 复制到 dst  返回复制的字节数 以及在复制时遇到的最早的错误 当且仅当err == nil时 written == n
若 dst 实现了 ReaderFrom 接口 复制操作也就会使用 来实现
io.CopyN(os.Stdout, strings.NewReader("Go语言中文网"), 8)//Go语言

1.13. ReadAtLeast 和 ReadFull 函数
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
将 r 读取到 buf 中 直到读了最少 min 个字节为止 返回复制的字节数 如果读取的字节较少 还会返回一个错误 若没有读取到字节 错误就只是 EOF 如果一个 EOF 发生在读取了少于 min 个字节之后 ReadAtLeast 就会返回 ErrUnexpectedEOF 若 min 大于 buf 的长度 ReadAtLeast 就会返回 ErrShortBuffer对于返回值 当且仅当 err == nil 时 才有 n >= min
一般可能不太会用到这个函数使用时需要注意返回的 error 判断

ReadFull 函数的签名
func ReadFull(r Reader, buf []byte) (n int, err error)
精确地从 r 中将 len(buf) 个字节读取到 buf 中 返回复制的字节数 如果读取的字节较少 还会返回一个错误 若没有读取到字节 错误就只是 EOF 如果一个 EOF 发生在读取了一些 但不是所有的字节后 ReadFull 就会返回 ErrUnexpectedEOF 对于返回值 当且仅当 err == nil 时 才有 n == len(buf)
注意该函数和 ReadAtLeast 的区别 ReadFull 将 buf 读满;而 ReadAtLeast 是最少读取 min 个字节

1.14. WriteString 函数 为了方便写入 string 类型
func WriteString(w Writer, s string) (n int, err error)
将s的内容写入w中 当 w 实现了 WriteString 方法时 会直接调用该方法 否则执行 w.Write([]byte(s))

1.15. MultiReader 和 MultiWriter 函数
func MultiReader(readers ...Reader) Reader
func MultiWriter(writers ...Writer) Writer
接收多个 Reader 或 Writer 返回一个 Reader 或 Writer 这两个函数就是操作多个 Reader 或 Writer 就像操作一个
在 io 包中定义了两个非导出类型 mutilReader 和 multiWriter  分别实现了 io.Reader 和 io.Writer 接口类型定义为
type multiReader struct {
    readers []Reader
}
type multiWriter struct {
    writers []Writer
}
这两种类型对应的实现方法(Read 和 Write 方法)的使用
MultiReader 的使用
readers := []io.Reader{
    strings.NewReader("from strings reader "),
    bytes.NewBufferString("from bytes buffer"),
}
reader := io.MultiReader(readers...)
data := make([]byte, 0, 128)
buf := make([]byte, 10)
for n, err := reader.Read(buf); err != io.EOF ; n, err = reader.Read(buf){
    if err != nil{
        panic(err)
    }
    data = append(data,buf[:n]...)
}
fmt.Printf("%s\n", data)//from strings reader from bytes buffer
 首先构造了一个 io.Reader 的 slice 由 strings.Reader 和 bytes.Buffer 两个实例组成 然后通过 MultiReader 得到新的 Reader 循环读取新 Reader 中的内容
 从输出结果可以看到 第一次调用 Reader 的 Read 方法 获取到的是 slice 中第一个元素的内容 也就是说 MultiReader 只是逻辑上将多个 Reader 组合起来 并不能通过调用一次 Read 方法获取所有 Reader 的内容 在所有的 Reader 内容都被读完后 Reader 会返回 EOF
 
 
MultiWriter 的使用 // 生成 tmp.txt 文件 同时在文件和屏幕中都输出 Go语言中文网这 和 Unix 中的 tee 命令类似
file, err := os.Create("tmp.txt")
if err != nil {
    panic(err)
}
defer file.Close()
writers := []io.Writer{
    file,
    os.Stdout,
}
writer := io.MultiWriter(writers...)
writer.Write([]byte("Go语言中文网"))
 
Go 实现 Unix 中 tee 命令的功能很简单
MultiWriter 的 Write 方法是如何实现的

1.16. TeeReader 函数
func TeeReader(r Reader, w Writer) Reader
TeeReader 返回一个 Reader  将从 r 中读到的数据写入 w 中 所有经由 处理的 从 r 的读取 都匹配于对应的对 w 的写入
没有内部缓存 即写入必须在读取完成前完成 任何在写入时遇到的错误都将作为读取错误返回
也就是说 通过 Reader 读取内容后 会自动写入到 Writer 中去
reader := io.TeeReader(strings.NewReader("Go语言中文网"), os.Stdout)
reader.Read(make([]byte, 20))//Go语言中文网
这种功能的实现其实挺简单 无非是在 Read 完后执行 Write
至此 io 所有接口 类型和函数都讲解完成