go标准库之输入输出

Posted by     "Richie" on Friday, February 1, 2019

io - 基本的IO接口

io 包为I/O原语提供了基本的接口. 它主要包装了这些原语的已有实现. 由于这些被接口包装的I/O原语是由不同的低级操作实现, 因此, 在另有声明之前不该假定他们的并行执行是安全的.

在io包中最重要的是两个接口: Reader和Writer 接口. 本文所提到的各种IO包, 都跟这两个接口有关, 也就是说, 只要满足这两个接口, 它就可以使用IO包的功能.

Reader 接口

Reader 接口的定义如下:

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

官方文档中关于该接口方法的说明:

  1. Read 将len(p) 个字节读取到p中. 它返回读取的字节数n (0 <= n <= len(p)) 以及任何遇到的错误. 即使 Read 返回的 n > len(p), 它也会在调用过程中占用len(p) 个字节作为暂用空间. 若可读取的数据不到len(p)个字节, Read 会返回可用数据, 而不是等待更多数据.
  2. 当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).
  3. 调用者在考虑错误之前应当首先处理返回的数据. 这样做可以正确地处理在读取一些字节后产生的I/O 错误, 同时允许EOF的出现.

根据Go语言中关于接口和满足了接口的类型的定义(Interface_type), 我们知道Reader接口的方法集(Method_sets)只包含一个Reade方法, 因此, 所有实现了Read方法的类型都满足io.Reader接口, 也就是说, 在所有需要io.Reader的地方, 可以传递实现了Read()方法的类型的实例. 下面, 我们通过具体的例子来谈谈该接口的用法:

package main

import (
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	ReaderExample()
}
func ReaderExample() {
FOREND:
	for {
		readerMenu()
		var ch string
		fmt.Scanln(&ch)
		var (
			data []byte
			err error
		)

		switch strings.ToLower(ch) {
		case "1":
			fmt.Println("请输入不多于9个字符, 以回车结束:")
			data, err = ReadFrom(os.Stdin, 11)
		case "2":
			file, err :=os.Open("/Users/finup/GoglandProjects/src/demo/test.txt")
			if err != nil {
				fmt.Println("打开文件错误test.txt ", err)
				continue
			}
			data, err = ReadFrom(file, 9)
			file.Close()
		case "3":
			data, err = ReadFrom(strings.NewReader("from string"), 12)
		case "4":
			fmt.Println("暂未实现")
		case "b":
			fmt.Println("返回上级菜单")
			break FOREND
		case "q":
			fmt.Println("程序退出")
			os.Exit(0)
		default:
			fmt.Println("输入错误")
			continue
		}

		if err != nil {
			fmt.Println("数据读取失败, 可以试试从其他输入源读取!")
		} else {
			fmt.Println("读到的数据是:%s\n", data)
		}
	}
}

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
}

func readerMenu() {
	fmt.Println(" ")
	fmt.Println("*********从不同来源读取数据************")
	fmt.Println("*********选择数据源, 请输入:***********")
	fmt.Println("1 表示 标准输入")
	fmt.Println("2 表示 普通文件")
	fmt.Println("3 表示 从字符串")
	fmt.Println("4 表示 从网络")
	fmt.Println("q 退出")
	fmt.Println("************************************")
}

ReadFrom函数将io.Reader 作为参数, 也就是说, ReadFrom可以从任意的地方读取数据,只要来源实现了io.Reader接口.比如, 我们可以从标准输入, 文件, 字符串等读取数据, 示例代码如下:

// 从标准输入读取
data, err = ReadFrom(io.Stdin, 11)

// 从普通文件读取, 其中file 是os.File 的实例
data, err = ReadFrom(file, 9)

// 从字符串读取
data, err = ReadFrom(strings.NewReader("from string"), 12)

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

Writer 接口

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.Write接口.

在上个例子中, 我们是自己实现了一个函数接受一个io.Reader类型的参数. 这里, 我们通过标准库的例子来学习.

在fmt标准库中, 有一组函数: Fprint/Fprintf/Fprintln, 它们接受一个io.Writer类型参数(第一个参数), 也就是说它们将数据格式化输出到io.Writer中. 那么, 调用这组函数时, 该如何传递这个参数呢?

我们以fmt.Fprintln为例, 同时看一下fmt.Println函数的源码.

// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

很显然, fmt.Println会将内容输出到标准输出中. 后面我们将详细介绍fmt包.

实现了io.Reader接口或io.Writer 接口的类型

很多人看到函数参数是一个接口类型, 很多时候有些束手无策, 不知道该怎么传递参数. 还有人问: 标准库中有哪些类型实现了io.Readerio.Writer 接口? 通过本节上面的例子, 我们可以知道, os.File同时实现了这两个接口. 我们还看到os.Stdin/Stdout这样的代码,它们似乎分别实现了io.Reader/io.Writer接口. 没错, 实际上在os包中有这样的代码:

// Stdin, Stdout, and Stderr are open Files pointing to the standard input,
// standard output, and standard error file descriptors.
//
// Note that the Go runtime writes to standard error for panics and crashes;
// closing Stderr may cause those messages to go elsewhere, perhaps
// to a file opened later.
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.

目前, Go文档中还没有直接列出实现了某个接口的所有类型. 不过, 我们可以通过查看标准库文档, 列出实现了io.Reader 或 io.Writer接口的类型(导出的类型): (注: godoc 命令支持额外参数-analysis,能列出都哪些类型实现了某个接口, 相关参考 godoc -hStatic 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
  • 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
  • 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结尾的.

ReaderAt 和 WriterAt 接口

ReaderAt 接口的定义如下:

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的错误来解释为什么没有返回更多的字节. 在这一点上ReadAtRead 更加严格. 即使ReadAt返回的n < len(p), 它也会在调用过程中使用p的全部作为暂存空间. 若可读取的数据不到len(p)字节, ReadAt就会阻塞, 直到所有数据都可用或者一个错误发生. 这一点上ReadAt不同于Readn = len(p) 个字节从输入源的结尾处由ReadAt 返回, Read可能返回err == EOF 或者 err == nilReadAt 携带一个偏移量从输入源读取, ReadAt应当既不会影响偏移量也不会被它所影响. 可对相同的输入源并行执行ReadAt调用.

可见, ReadAt接口使得可以从指定偏移量处开始读取数据.

简单示例代码如下:

package main

import (
	"fmt"
	"strings"
)

func main(){
	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)
}

官方文档中关于该接口方法的说明:

WriteAtp 中将len(p) 个字节写入到偏移量off处的基本数据流中.它返回从p中被写入的字节数n (0 <= n <= len(p)) 以及任何遇到的引起写入提前停止的错误.若WriteAt返回的n < len(p),它就必须返回一个非nil的错误. 若WriteAt 携带一个偏移量写入到目标中, WriteAt应当既不影响偏移量也不被它所影响. 若被写区域没有重叠, 可对相同的目标并行执行WriteAt调用.

我们可以通过该接口将数据写入到数据流的特定偏移量之后. 通过简单示例来演示WriteAt方法的使用(os.File实现了WriteAt接口):

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("./writeAt.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	file.WriteString("Golang测试wirteAt--这里是多余")
	n, err := file.WriteAt([]byte("Go语言开发"), 21)
	if err != nil {
		panic(err)
	}
	fmt.Println(n)
}

打开文件writeAt.txt, 内容是:Golang测试wirteAt--Go语言开发. 分析:

file.WriteString("Golang测试wirteAt--这里是多余")往文件中写入Golang测试wirteAt--这里是多余,之后
file.WriteAt([]byte("Go语言开发"), 21) 在文件流的offset=21处写入Go语言开发(会覆盖改位置的内容).

ReaderFrom 和 WriterTo 接口

ReaderFrom的定义如下:

type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}

官方文档中关于该接口方法的说明:

ReadFromr 中读取数据, 直到EOF或者发生错误. 其返回值n为读取的字节数.除io.EOF外, 在读取过程中遇到的任何错误也将被返回. 如果ReadFrom 可用, Copy 函数就会使用它.

注意: ReadFrom 方法不会返回 err == EOF. 下面的例子简单的实现将文件中的数据全部读取(显示在标准输出):

package main

import (
	"bufio"
	"os"
)

func main() {

	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接口, 我们有两种思路:

  1. 先获取文件的大小(FileStat方法),之后定义一个该大小的[]byte,通过Read一次性读取
  2. 定义一个小的[]byte,不断的调用Read方法直到遇到EOF, 将所有读取到的[]byte连接到一起

这里不给出实现代码了,有兴趣的可以实现下.

提示

通过查看bufio.Writer 或者strings.Buffer类型的ReadFrom 方法实现, 会发现, 其实它们的实现和上面说的第二种思路类似.

WriterTo的定义如下:

type WriterTo interface {
    WriteTo(w Writer) (n int64, err error)
}

官方文档中关于该接口方法的说明:

WriteTo将数据写入w 中, 直到没有数据可写或者发生错误.其返回值n为写入的字节数.在写入过程中遇到的任何错误也将返回. 如果WriterTo可用, Copy函数就会使用它. 读者是否发现, 其实ReaderFromWriterTo接口的方法接受的参数是io.Readerio.Writer类型.根据io.Readerio.Writer接口的讲解, 对该接口的使用应该可以很好的掌握.

这里只提供简单的示例代码: 将一段文本输出到标准输出

package main

import (
	"bytes"
	"os"
)


func main(){

	reader := bytes.NewReader([]byte("Go语言中文网"))
	reader.WriteTo(os.Stdout)
}

io.ReaderFromio.WriterTo的学习, 我们知道, 如果这样的需求, 可以考虑使用这两个接口:“一次性从某个地方读或者写到某个地方去.”

Seeker 接口

接口定义如下:

type Seeker interface {
    Seek(offset int64, whence int) (ret int64, err error)
}

官方文档中关于该接口方法的说明:

Seek 设置下一次ReadWrite的偏移量为offset, 它的解释取决于whence: 0表示相对于文件的起始处, 1表示相对于当前的偏移,而2表示相对于其结尾处.Seek返回新的偏移量和一个错误,如果有的话.

也就是说, Seek方法是用于设置偏移量的, 这样可以从某个特定位置开始操作数据流.听起来和Reader/WriterAt接口有些类似,不过Seek接口更加灵活, 可以更好的控制读写数据流的位置.

简单的示例代码: 获取倒数第二个字符(需要考虑UTF-8编码:utf-8编码一个汉字3个字节, 这里的代码只是一个示例):

package main

import (
	"fmt"
	"strings"
	"io"
)

func main() {
	reader := strings.NewReader("Go语言标准库学习")
	reader.Seek(-6, io.SeekEnd)
	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
)

Closer 接口

接口定义如下:

type Closer interface {
    Close() error
}

该接口比较简单, 只有一个Close()方法, 用于关闭数据流. 文件(os.File), 归档(压缩包), 数据库连接, Socket等需要手动关闭的资源都实现了Closer接口. 实际编程中, 经常讲Close方法的调用放在defer语句中. 提示 初学者容易写出这样的代码:

file, err := os.Open("test.txt")
defer file.Close()
if err != nil {
    ...
}

当文件test.txt不存在或者找不到时, file.Close()panic, 因为filenil.因此应该将defer file.Close()放在错误检查之后.
经过issue40提醒,查看源码:

func (f *File) Close() error{
    if f == nil {
        return ErrInvalid
    }
    return f.file.close()
}

可见并不会panic, 但在Close 之前校验错误是个好习惯!

其他接口

ByteReader 和 ByteWriter

通过名称大概也能猜出这组接口的用途: 读或者写一个字节. 接口定义如下:

type ByteReader interface {
    ReadByte() (c byte, err error)
}

type ByteWriter interface {
    WriterByte(c byte) error
}

在标准库中, 有如下类型实现了io.ByteReadre或io.ByteWriter:

  • bufio.Reader/Writer 分别实现了io.ByteReaderio.ByteWriter
  • bytes.Buffer同时实现了io.ByteReaderio.ByteWriter
  • bytes.Reader实现了io.ByteReader
  • strings.Reader实现了io.ByteReader

接下来的示例中, 我们通过bytes.Buffer来一次读取或者写入一个字节(主要代码):

package main

import (
	"fmt"
	"bytes"
)

func main() {
	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字符), 调用bufferWriteByte将该字节写入buffer中,之后通过ReadByte读取该字节.

一般地, 我们不会使用bytes.Buffer来一次读取或者写入一个字节. 那么, 这两个接口有哪些用处呢?

在标准库encoding/binary中, 实现Google-ProtoBuf中的Varints 读取, ReadVarint就需要一个io.ByteReader类型的参数, 也就是说, 它需要一个字节一个字节的读取.关于 encoding/binary 包在后面会详细介绍
在标准库image/jpeg中, Encode函数的内部实现使用了ByteWritre写入一个字节

小贴士 可以通过在Go语言源码src/pkg中搜索io.ByteReaderio.ByteWriter,获得哪些地方用到了这两个接口. 你会发现, 这两个接口在二进制数据或归档压缩用的比较多.

ByteScanner, RuneReader 和 RuneScanner

将这三个接口放在一起, 是考虑到与ByteReader相关或相应. ByteScanner 接口的定义如下:

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包有用到.

RuneScanner 接口和ByteScanner类似,就不赘述了.

ReadCloser, ReadSeeker, ReadWriteCloser, ReadWriteSeeker, ReadWriter, WriteCloser和WriteSeeker

这些接口是上面介绍的接口的两个或三个组合而成的新接口. 例如ReadWriter接口`:

type ReadWriter interface {
    Reader
    Writer
}

这是Reader 接口和Writer接口的简单组合(内嵌) 这些接口的作用是: 有些时候同时需要某两个接口的所有功能, 即必须同时实现了某两个接口的类型才能够被传入使用. 可见,io包中有大量的"小接口", 这样方便组合为"大接口".

SectionReader 类型

SectionReader是一个struct(没有任何导出的字段), 实现了Read, SeekReadAt, 同时内嵌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)数据流;或者同时需要ReadAtSeek的功能.

由于该类型所支持的操作, 前面都有介绍. 关于该类型在标准库中的使用, 我们在archive/zip - zip 归档访问会讲到.

LimitedReader 类型

LimitedReader 结构定义如下:

type LimitedReader struct {
    R Reader  // underlying reader, 最终的读取操作通过 R.Read 完成
    N int64   // max bytes remaining
}

文档说明如下:

R读取但将返回的数据量限制为N个字节. 每调用一次Read都讲更新N来反应新的剩余数量. 也就是说, 最多只能返回N字节数据.

LimitedReader 只实现Read方法(Reader接口)

使用示例如下:

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	content := "This is LimitReader Example"
	reader := strings.NewReader(content)
	limtReader := &io.LimitedReader{R: reader, N: 8}
	for limtReader.N > 0 {
		tmp := make([]byte, 2)
		limtReader.Read(tmp)
		fmt.Printf("%s", tmp)
	}
}

输出:

This is 

可见, 通过该类型可以达到只允许读取一定长度数据 的目的. 在io包中,LimitReader 函数的实现其实就是调用LimitedReader:

func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

PipeReader 和 PipeWriter 类型

PipeReader (一个没有任何导出字段的struct)是管道的读取端. 它实现了io.Readerio.Closer 接口. 结构定义如下:

type PipeReader struct {
    p *pipe
}

关于PipeReader.Read方法的说明: 从管道中读取数据. 该方法会阻塞, 直到管道写入端开始写入数据或写入端被关闭. 如果写入端关闭时带有error(即调用CloseWithError关闭), 该Read 返回的err 就是写入端传递的error; 否则errEOF.

PipeWriter(一个没有任何导出字段的struct) 是管道的吸入端. 它实现了io.Writerio.Closer 接口. 结构定义如下:

type PipeWriter struct {
    p *pipe
} 

关于PipeWriter.Write方法的说明: 写数据到管道中. 该方法会阻塞, 直到读取端读完所有数据或者读取端关闭. 如果读取端关闭带有error(即调用CloseWriteError关闭), 该Write返回的err 就是读取端传递的error;否则errErrClosePipe. 使用示例如下:

package main

import (
	"errors"
	"fmt"
	"io"
	"time"
)

func main() {
	pipeReader, pipeWriter := io.Pipe()
	go PipWrite(pipeWriter)
	go PipeRead(pipeReader)
	time.Sleep(30 * time.Second)
}

func PipWrite(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.一端的读取匹配另一端的写入, 直接在这两端之间复制数据;它没有内部缓存.它对于并行调用ReadWrite以及其他的函数或Close来说都是安全的.一旦等待的I/O结束,Close就会完成. 并行调用Read或并行调用Write也同样安全:同种类的调用将按顺序进行控制.

证因为是同步的, 因此不能在一个goroutine中进行读和写.

另外, 对于管道的Close方法(非CloseWithError时),err会被置为EOF.

Copy 和 CopyN 函数

Copy 函数 的签名:

func Copy(dst Writer, src Reader) (written int64, err error)

函数文档:

Copysrc复制到dst, 知道在src上到达EOF或发生错误. 它将返回复制的字节数, 如果有错误的话, 它还会返回在复制时遇到的第一个错误.
成功的Copy返回err == nil, 而非err == EOF.由于Copy被定义为从src读取直到EOF为止,因此它不会将来自ReadEOF当做错误来报告.
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("Go EOF -- bye")

}

执行: echo "Hello, World" | go run zrq.go **CopyN**函数的签名: func CopyN(dst Writer, src Reader, n int64) (written int64, err error) 函数文档:

CopyNn个字节(或到一个error)从src复制到dst. 它返回复制的字节数以及在复制时遇到的最早的错误.当且仅当err == nil时, written == n.

dst实现了ReaderFrom接口, 复制操作也就会使用它来实现.

代码:

package main

import (
	"io"
	"os"
	"strings"
)

func main() {
	io.CopyN(os.Stdout, strings.NewReader("G语言标准库学习"), 8)

}

会输出:

Go语言

ReadAtLeast 和 ReadFull 函数

ReadAtLeast 函数的签名: func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)

函数文档:

ReadAtLeastr读取到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)

函数文档:

ReadFull精确地从r中将len(buf)个字节读取到buf中. 它返回复制的字节数, 如果读取的字节较少., 还会返回一个错误. 若没有读取到字节, 错误就只有EOF. 如果一个EOF发生在读取了一些单不是所有的字节后, ReadFull就会返回ErrUnexpectedEOF.对于返回值, 当且仅当err == nil, 才有n === len(buf).

注意该函数和ReadAtLeast的区别: ReadFullbuf读满;而ReadAtLeast是最少读取min个字节.

WriteString 函数

这是为了方便写入string类型提供的函数, 函数签名: func WriteString(w Writer, s string) (n int, err error)

函数文档;

WriteStrings的内容写入w中, 当w实现了WriteString方法时, 会直接调用该方法, 否则执行w.Write([]bytes(s))

MultiReader 和 MultiWriter 函数

这两个函数的定义分别是:

func MultiReader(readers ...Reader) Reader
func MultiWriter(writers ...Writer) Writer

它们接收多个Reader或者Writer, 返回一个Reader或者Writer. 我们可以猜想到这两个函数就是操作做个ReaderWriter就像操作一个.

事实上, 在io包中定义了两个非导出类型: multiReadermultiWriter, 它们分别实现了io.Readerio.Writer接口. 类型定义为:

type multiReader struct {
    readers []Reader
}

type multiWriter struct {
    writers []Writer
}

对于这两种类型对应的实现方法(ReadWrite方法) 的使用,我们通过例子来演示. MultiReader的使用:

package main

import (
	"bytes"
	"fmt"
	"io"
	"strings"
)

func main() {
	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 readerfrom bytes buffer

代码中首先构造了一个io.Readerslice, 由strings.Readerbytes.Buffer两个实例组成, 然后通过MultiReader得到新的Reader, 循环读取新Reader中的内容. 从输出结果可以看到, 第一次调用ReaderReader方法获取到的是slice中第一个元素的内容… 也就是说, MultiReader只是逻辑上将多个Reader组合起来, 并不能通过调用一次Reader方法获取所有Reader内容. 在所有的eader内容都被读完后,Reader会返回EOF.

MultiWriter的使用:

package main

import (
	"io"
	"os"
)

func main() {
	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语言标准库学习"))
}

这段程序执行后在生成tmp.txt文件, 同时在文件和屏幕中都输出:Go语言标准库学习. 这和Unix中的tee命令相似.

TeeReader 函数

函数签名如下:

func TeeReader(r Reader, w Writer) Reader

TeeReader返回一个Reader, 它将从r中读到的数据写入w中.所有经由它处理的从r的读取都匹配于对应的对w的写入. 它没有内部缓存, 即写入必须在读取完成前完成. 任何写入时遇到的错误都将作为读取错误返回.

也就是说, 我们通过Reader读取内容后, 会自动写入到Writer中去. 例子代码如下:

package main

import (
	"io"
	"os"
	"strings"
)

func main() {
	reader := io.TeeReader(strings.NewReader("Go语言标准库学习"), os.Stdout)
	reader.Read(make([]byte, 20))
}

输出结果:

Go语言标准库学习

这种功能的实现其实很简单,无非是在Read完之后执行Write. 至此, io所有接口, 类型和函数都讲解完了.

「真诚赞赏,手留余香」

Richie Time

真诚赞赏,手留余香

使用微信扫描二维码完成支付