⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Aug.5 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
目录
有特定需要的内容直接跳转到相关章节查看即可。
Section 1. 什么是数据流 (data stream):向读者同步了数据流的概念
Section 2. package io: 提供 I/O 原语的基本接口:罗列了 package io 中提供的接口、结构体和函数
Section 3. package io/ioutil: 提供方便的 I/O 操作函数集:罗列了 package io/ioutil 中的函数
Section 4. package fmt: 提供格式化的 I/O 函数:分析了 package fmt 所提供的格式化 I/O 的功能
4.1 占位符一览:列举出了格式化输出常用的占位符,方便查询;
4.2 Print 序列函数:阐述了 fmt 包中的 Print 序列函数 Fprint / Fprintf / Fprintln / Sprint / Sprintf / Sprintln / Print / Printf / Println;
4.3 Scan 序列函数阐述了 fmt 包中的 Scan 序列函数 Fscan / Fscanf / Fscanln / Sscan / Sscanf / Sscanln / Scan / Scanf / Scanln;
4.4 Stringer 接口:阐述了 package fmt 中的 Stringer 接口;
4.5 Formatter 接口:阐述了 package fmt 中的 Formatter 接口;
Section 5. package bufio: 提供了缓存 I/O 的能力:分析了 package bufio 所提供的带缓冲区的 I/O 的功能
5.1 bufio.Reader 类型和方法:;
5.1.1 bufio.Reader 实现原理;
5.1.2 bufio.Reader 实现的接口;
5.1.3 bufio.NewReader: 实例化 bufio.Reader;
5.1.4 bufio.Reader.ReadSlice;
5.1.5 bufio.Reader.ReadBytes;
5.1.6 bufio.Reader.ReadString;
5.1.7 bufio.Reader.Peek: 实例化 bufio.Reader;
5.1.8 bufio.Scanner 引言;
5.2 Writer 类型和方法;
5.2.1 bufio.Writer 的原理;
5.2.2 bufio.Writer 实现的接口;
5.2.3 bufio.Writer.Available;
5.2.4 bufio.Writer.Buffered;
5.2.5 bufio.Writer.Flush;
5.3 Scanner 类型和方法;
5.3.1 Scanner 原理;
5.3.2 Scanner 实例化;
5.3.3 Scanner 的方法;
1. 什么是数据流 (data stream)?
在开始讨论 I/O 接口之前,我们必须明确:Linux 系统下的数据流是什么?
我们给出定义:数据流是 Byte 的集合。我们回顾一句名言:"Linux 中一切皆文件"。Linus Torvalds 后来认为这句话应该是:"Linux 中一切皆数据流"。因此我理解,由于文件是一堆 Byte 的集合,因此存储在磁盘中不动的文件可以被理解为一种静态数据流,而比如说位于内存中的动态的键盘缓冲区就可以被理解为动态数据流。
像 Golang 一样的高级编程语言通常为我们封装好了用于 I/O 操作的底层库函数实现,而这些 I/O 库函数的操作对象就是数据流,可以是文件、可以是某个系统内存缓冲区(e.g. 标准输入缓冲区),也可以是程序自身在内存中的数据(e.g. 某个字符串),它们都是一堆 Byte 的集合。因此我们明确:我们本文所讨论的 Golang I/O 标准库,其操作的对象是 Byte 的集合,具体表现可以是文件、缓冲区、程序变量等。
2. package io: 提供 I/O 原语的基本接口
io 包为基本的 I/O 操作提供了基本的接口。再次明确基础知识:接口是对功能的约定。因此 io 包中提供的是对 I/O 操作功能的约定,即 I/O 接口。正如我们上面所说的,任何操作 Byte 集合的动作都可以视作 I/O 操作,标准库中其它很多涉及数据操作的包都实现了 package io 的这些 I/O 接口,只是它们的实现方式各有千秋。
下面我们整理出这些接口:
| 接口名称 | 定义 | 功能 | ||||
|---|---|---|---|---|---|---|
| Reader |
|
Read 从某个数据流将 len(p) 个字节读取到缓冲区 p 中,具体从哪个数据流取决于具体实现方式 | ||||
| Writer |
|
Write 将 len(p) 个字节从 p 中写入到某个数据流中,具体写入哪个数据流取决于具体实现方式 | ||||
| ReaderAt |
|
ReadAt 从某个数据流的偏移量 off 处开始,将 len(p) 个字节读取到 p 中。具体从哪个数据流取决于具体实现方式 | ||||
| WriterAt |
|
WriteAt 从 p 中将 len(p) 个字节写入到某个数据流的偏移量为 off 的地方,具体写入哪个数据流取决于具体实现方式 | ||||
| ReaderFrom |
|
ReadFrom 从 r 中读取数据,直到 EOF 或发生错误。其返回值 n 为读取的字节数。 | ||||
| WriterTo |
|
WriteTo 将数据写入 w 中,直到没有数据可写或发生错误。 | ||||
| Seeker |
|
Seek 设置下一次 Read 或 Write 的偏移量为 offset,它的解释取决于参数 whence:0 表示相对于文件的起始处,1 表示相对于当前的偏移,而 2 表示相对于其结尾处。Seek 返回新的偏移量和一个错误,如果有的话。 | ||||
| Closer |
|
该接口比较简单,只有一个 Close() 方法,用于关闭数据流。文件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要手动关闭的资源都实现了 Closer 接口。 | ||||
| ByteReader ByteWriter |
|
(从某个数据流/向某个数据流)读/写一个字节 | ||||
| ByteScanner |
|
比 ByteReader 接口多了个 UnreadByte 方法,其作用是将上一次 ReadByte 的字节还原,使得再次调用 ReadByte 时效果一样。类似的关系还有 RuneReader 和 RuneScanner,不再赘述 | ||||
| ReadCloser ReadSeeker ReadWriteCloser ReadWriteSeeker ReadWriter WriteCloser WriteSeeker |
|
复合接口,不再赘述 |
在 package io 中还提供了一些结构体类型,整理如下:
| 结构体名 | 定义 | 功能 | ||||
|---|---|---|---|---|---|---|
| SectionReader |
|
SectionReader 实现了 Read, Seek 和 ReadAt 的同时内嵌了 ReaderAt 接口。它可以从 r 中的偏移量 off 处读取 n 个字节后以 EOF 停止,它可以帮助我们方便重复操作某一段 (section) 数据流,或者同时需要 ReadAt 和 Seek 的功能。 | ||||
| LimitedReader |
|
LimitedReader 只实现了 Read 方法,它用于从 R 读取但最多只能返回 N 字节数据 | ||||
| PipeReader PipeWriter |
|
PipeReader(一个没有任何导出字段的 struct)是管道 (i.e. 操作系统管道) 的读取端。它实现了 io.Reader 和 io.Closer 接口。它从管道中读取数据。该方法会堵塞,直到管道写入端开始写入数据或写入端被关闭。
PipeWriter(一个没有任何导出字段的 struct)是管道 (i.e. 操作系统管道) 的写入端。它实现了 io.Writer 和 io.Closer 接口。它可以写数据到管道中。该方法会堵塞,直到管道读取端读完所有数据或读取端被关闭。 |
此外,package io 还提供了一些好用的函数:
| Copy CopyN |
|
Copy 将 src 复制到 dst,直到在 src 上到达 EOF 或发生错误。它返回复制的字节数,如果有错误的话,还会返回在复制时遇到的第一个错误。
CopyN 将 n 个字节(或到一个error)从 src 复制到 dst。 它返回复制的字节数以及在复制时遇到的最早的错误。 |
||||
| ReadAtLeast ReadFull |
|
ReadAtLeast 将 r 读取到 buf 中,直到读了最少 min 个字节为止。它返回复制的字节数,如果读取的字节较少,还会返回一个错误。
ReadFull 精确地从 r 中将 len(buf) 个字节读取到 buf 中。它返回复制的字节数,如果读取的字节较少,还会返回一个错误。 |
||||
| WriteString |
|
WriteString 将 s 的内容写入 w 中,当 w 实现了 WriteString 方法时,会直接调用该方法,否则执行 w.Write([]byte(s))。 | ||||
| MultiReader MultiWriter |
|
MultiReader 用于将多个 Reader 组合成为一个 Reader,然后在调用这个组合后的 Reader 的 Read 函数时,第一次调用获取的是第一个组合前 Reader 的内容,第二次调用获取的是第二个组合前 Reader 的内容,在所有的 Reader 内容都被读完后,Reader 会返回 EOF。
MultiWriter 用于将多个 Writer 组合成为一个 Writer,然后在调用这个组合后的 Writer 的 Write 函数时,会向所有的 Writer 写入同样的内容 |
3. package io/ioutil: 提供方便的 I/O 操作函数集
package io/ioutil 在上文阐述的 package io 基础上,提供了更多的常用、方便的IO操作函数。列举阐述如下:
| 函数名 | 函数签名 | 功能 | ||||
|---|---|---|---|---|---|---|
| NopCloser |
|
NopCloser 用于包装一个 io.Reader,返回一个 io.ReadCloser ,而相应的 Close 方法啥也不做,只是返回 nil。用在有时候我们需要传递一个 io.ReadCloser 的实例,而我们仅有 io.Reader 的实例的情况下 | ||||
| ReadAll |
|
从io.Reader 中一次读取所有数据 | ||||
| ReadDir |
|
用于读取目录并返回排好序的文件和子目录名([]os.FileInfo) | ||||
| ReadFile WriteFile |
|
ReadFile 从 filename 指定的文件中读取数据并返回文件的内容。成功的调用返回的 err 为 nil 而非 EOF。
WriteFile 将 data 写入 filename 文件中,当文件不存在时会根据 perm 指定的权限进行创建一个,文件存在时会先清空文件内容。 |
||||
| TempDir TempFile |
|
TempDir 用于创建临时目录。若第一个参数如果为空,表明在系统默认的临时目录( os.TempDir )中创建临时目录;第二个参数指定临时目录名的前缀,该函数返回临时目录的路径。
TempFile 用于创建临时文件。若第一个参数如果为空,表明在系统默认的临时目录( os.TempDir )中创建临时文件;第二个参数指定临时文件名的前缀,该函数返回临时文件的路径。 创建者创建的临时文件和临时目录要负责删除这些临时目录和文件。 |
4. package fmt: 提供格式化的 I/O 函数
package fmt 中提供了很多格式化的 I/O 函数。所谓 "格式化" 就是可以使用 "占位符 (e.g. %d, %f...)" 来实现格式化的字符/字节输入/输出。
4.1 占位符一览
本小节列举的例子中所使用的 类型/变量 定义如下:
1
2
3
4
5
6type Website struct {
Name string
}
// 定义结构体变量
var site = Website{Name:"studygolang"}
下面我们列举出常用的占位符,方便查询:
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %v | 相应值的默认格式,在打印结构体时,”加号” 标记(%+v)会添加字段名 | Printf(“%v”, site),Printf(“%+v”, site) | {studygolang},{Name:studygolang} |
| %#v | 相应值的Go语法表示 | Printf(“#v”, site) | main.Website{Name:”studygolang”} |
| %T | 相应值的类型的Go语法表示 | Printf(“%T”, site) | main.Website |
| %% | 字面上的百分号,并非值的占位符 | Printf(“%%”) | % |
| %t | 单词 true 或 false | Printf(“%t”, true) | true |
| %b | 二进制表示 | Printf(“%b”, 5) | 101 |
| %c | 相应 Unicode 码点所表示的字符 | Printf(“%c”, 0x4E2D) | 中 |
| %d | 十进制表示 | Printf(“%d”, 0x12) | 18 |
| %o | 八进制表示 | Printf(“%o”, 10) | 12 |
| %q | 单引号围绕的字符字面值,由 Go 语法安全地转义 | Printf(“%q”, 0x4E2D) | ‘中’ |
| %x | 十六进制表示,字母形式为小写 a-f | Printf(“%x”, 13) | d |
| %X | 十六进制表示,字母形式为大写 A-F | Printf(“%x”, 13) | D |
| %U | Unicode 格式:U+1234,等同于 “U+%04X” | Printf(“%U”, 0x4E2D) | U+4E2D |
| %b | 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 ‘b’ 转换格式一致。例如 -123456p-78 | ||
| %e | 科学计数法,例如 -1234.456e+78 | Printf(“%e”, 10.2) | 1.020000e+01 |
| %E | 科学计数法,例如 -1234.456E+78 | Printf(“%E”, 10.2) | 1.020000E+01 |
| %f | 有小数点而无指数,例如 123.456 | Printf(“%f”, 10.2) | 10.200000 |
| %g | 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 | Printf(“%g”, 10.20) | 10.2 |
| %G | 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 | Printf(“%G”, 10.20+2i) | (10.2+2i) |
| %s | 输出字符串表示(string类型或[]byte) | Printf(“%s”, []byte(“Go语言中文网”)) | Go语言中文网 |
| %q | 双引号围绕的字符串,由Go语法安全地转义 | Printf(“%q”, “Go语言中文网”) | “Go语言中文网” |
| %x | 十六进制,小写字母,每字节两个字符 | Printf(“%x”, “golang”) | 676f6c616e67 |
| %X | 十六进制,大写字母,每字节两个字符 | Printf(“%X”, “golang”) | 676F6C616E67 |
| %p | 十六进制表示,前缀 0x | Printf(“%p”, &site) | 0x4f57f0 |
| + | 总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符 | Printf(“%+q”, “中文”) | “\u4e2d\u6587” |
| - | 在右侧而非左侧填充空格(左对齐该区域) | ||
| # | 备用格式:为八进制添加前导 0(%#o),为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始(即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的 Unicode 编码形式(如字符 x 会被打印成 U+0078 ‘x’) | Printf(“%#U”, ‘中’) | U+4E2D ‘中’ |
| ‘ ‘ | (空格)为数值中省略的正负号留出空白(% d)以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开 | ||
| 0 | 填充前导的0而非空格;对于数字,这会将填充移到正负号之后 |
4.2 Print 序列函数
这里说的 Print 序列函数包括:Fprint/Fprintf/Fprintln/Sprint/Sprintf/Sprintln/Print/Printf/Println。之所以将放在一起介绍,是因为它们的使用方式类似、参数意思也类似。
1
2
3func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
Fprint/Fprintf/Fprintln 函数的第一个参数接收一个 io.Writer 类型,会将内容输出到 io.Writer 中去。
1
2
3func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)
Print/Printf/Println 函数是将内容输出到标准输出中,因此,直接调用 F类函数 做这件事,并将 os.Stdout 作为第一个参数传入。
1
2
3func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string
Sprint/Sprintf/Sprintln 是格式化内容为 string 类型,而并不输出到某处,需要格式化字符串并返回时,可以用这组函数。
在这三组函数中,分别有三种类型的后缀,它们的功能如下:
- S/F/Printf 函数通过指定的格式输出或格式化内容
- S/F/Print 函数只是使用默认的格式输出或格式化内容
- S/F/Println函数使用默认的格式输出或格式化内容,同时会在最后加上"换行符"
简单来说,要想使用占位符来实现格式化输出,则必须使用 S/F/Printf。
4.3 Scan 序列函数
该序列函数和 Print 序列函数相对应,包括:Fscan/Fscanf/Fscanln/Sscan/Sscanf/Sscanln/Scan/Scanf/Scanln。
1
2
3func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
Fscan/Fscanf/Fscanln 函数的第一个参数接收一个 io.Reader 类型,从其读取内容并赋值给相应的实参。
1
2
3func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)
Scan/Scanf/Scanln 正是从标准输入获取内容,因此,直接调用 F类函数 做这件事,并将 os.Stdin 作为第一个参数传入。
1
2
3func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
Sscan/Sscanf/Sscanln 则直接从字符串中获取内容。
为了更好地说明它们的区别,我们从另一个唯独来区分这九个函数:
(1)无后缀:Scan/FScan/Sscan
1 | var ( |
输出为:
1
2 polaris 28
不管"polaris 28"是用空格分隔还是"\n"分隔,输出一样。也就是说,Scan/FScan/Sscan 这组函数将连续由空格分隔的值存储为连续的实参(换行符也记为空格)。
(2)f 后缀:Scanf/FScanf/Sscanf
1 | var ( |
输出:
1
2 polaris 28
如果将"空格"分隔改为"\n"分隔,则输出为:1 polaris 0。可见,Scanf/FScanf/Sscanf 这组函数将连续由空格分隔的值存储为连续的实参, 其格式由 format 决定,换行符处停止扫描(Scan)。
(3)ln 后缀:Scanln/FScanln/Sscanln
1 | var ( |
输出如下:
1
2 polaris 28
Scanln/FScanln/Sscanln 表现和上一组一样,遇到 "\n" 停止(对于 Scanln,表示从标准输入获取内容,最后需要回车)。
4.4 Stringer 接口
1 | type Stringer interface { |
Stringer 类型包含的 String() 主要用于打印该类型的一些属性。一个类型只要有 String() string 方法,我们就说它实现了 Stringer 接口。而在上文我们已经说到,如果想用上面列举的格式化输出函数来格式化输出某种类型的值,只要该类型实现了 String() 方法,那么会调用 String() 方法进行处理。
4.5 Formatter 接口
1 | type Formatter interface { |
通过实现 Formatter 接口可以做到自定义输出格式(自定义占位符),例子如下所示:
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
33type Person struct {
Name string
Age int
Sex int
}
func (this *Person) String() string {
buffer := bytes.NewBufferString("This is ")
buffer.WriteString(this.Name + ", ")
if this.Sex == 0 {
buffer.WriteString("He ")
} else {
buffer.WriteString("She ")
}
buffer.WriteString("is ")
buffer.WriteString(strconv.Itoa(this.Age))
buffer.WriteString(" years old.")
return buffer.String()
}
func (this *Person) Format(f fmt.State, c rune) {
if c == 'L' {
f.Write([]byte(this.String()))
f.Write([]byte(" Person has three fields."))
} else {
// 没有此句,会导致 fmt.Printf("%s", p) 啥也不输出
f.Write([]byte(fmt.Sprintln(this.String())))
}
}
p := &Person{"polaris", 28, 0}
fmt.Printf("%L", p)
输出为:
1
This is polaris, He is 28 years old. Person has three fields.
5. package bufio: 提供了缓存 I/O 的能力
package bufio 包实现了缓存 I/O。当我们在讨论 package bufio 的优点的时候,我们的背景是放在文件这种数据流的背景下。与 package io 提供的接口对比,package io 的接口实现的是 "数据 -> 文件",而 package bufio 实现的是 "数据 -> 缓冲区 -> 文件"。缓冲区的设计是为了在内存中存储多次的写入,最后一口气把缓冲区内容写入文件,从而避免了多次触发文件 io 导致的性能问题。
package bufio 包中包含了 bufio.Reader 结构 和 bufio.Writer 结构,以及相关的函数来实现带有缓冲区的 I/O 操作,下面我们进行阐述。
5.1 bufio.Reader 类型和方法
(1) bufio.Reader 实现原理
1 | type Reader struct { |
bufio.Read(p []byte) 相当于读取大小len(p)的内容,思路如下:
- 当缓存区有内容的时,将缓存区内容全部填入 p 并清空缓存区
- 当缓存区没有内容的时候且 len(p) `>` len(buf) ,即要读取的内容比缓存区还要大,直接去文件读取即可
- 当缓存区没有内容的时候且 len(p) `<` len(buf),即要读取的内容比缓存区小,缓存区从文件读取内容充满缓存区,并将 p 填满 (此时缓存区有剩余内容)。以后再次读取时缓存区有内容,将缓存区内容全部填入 p 并清空缓存区(此时和情况 1 一样)
bufio.Read 源码如下:
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// Read reads data into p.
// It returns the number of bytes read into p.
// The bytes are taken from at most one Read on the underlying Reader,
// hence n may be less than len(p).
// To read exactly len(p) bytes, use io.ReadFull(b, p).
// At EOF, the count will be zero and err will be io.EOF.
func (b *Reader) Read(p []byte) (n int, err error) {
n = len(p)
if n == 0 {
return 0, b.readErr()
}
// b.r == b.w 代表缓冲区的数据都被读走了
// 应该触发新的 I/O 读取
if b.r == b.w {
if b.err != nil {
return 0, b.readErr()
}
// 企图读取的长度超过缓冲区大小,直接即进行 I/O 读取
if len(p) >= len(b.buf) {
// Large read, empty buffer.
// Read directly into p to avoid copy.
n, b.err = b.rd.Read(p)
if n < 0 {
panic(errNegativeRead)
}
if n > 0 {
b.lastByte = int(p[n-1])
b.lastRuneSize = -1
}
return n, b.readErr()
}
// One read.
// Do not use b.fill, which will loop.
b.r = 0
b.w = 0
n, b.err = b.rd.Read(b.buf)
if n < 0 {
panic(errNegativeRead)
}
if n == 0 {
return 0, b.readErr()
}
b.w += n
}
// 将 I/O 缓冲区中的内容拷贝到目标缓冲区中
// copy as much as we can
n = copy(p, b.buf[b.r:b.w])
b.r += n
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = -1
return n, nil
}
(2) bufio.Reader 实现的接口
bufio.Reader 实现了 io.Reader io.WriterTo io.ByteScanner io.RuneScanner 等我们上面讲过的接口。bufio.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
26
27
28
29
30
31
32
33
34// NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象,
// 缓存大小由 size 指定(如果小于 16 则会被设置为 16)。
// 如果 rd 的基类型就是有足够缓存的 bufio.Reader 类型,则直接将
// rd 转换为基类型返回。
func NewReaderSize(rd io.Reader, size int) *Reader
// NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader
// Peek 返回缓存的一个切片,该切片引用缓存中前 n 个字节的数据,
// 该操作不会将数据读出,只是引用,引用的数据在下一次读取操作之
// 前是有效的。如果切片长度小于 n,则返回一个错误信息说明原因。
// 如果 n 大于缓存的总大小,则返回 ErrBufferFull。
func (b *Reader) Peek(n int) ([]byte, error)
// Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误。
// 如果缓存不为空,则只能读出缓存中的数据,不会从底层 io.Reader
// 中提取数据,如果缓存为空,则:
// 1、len(p) >= 缓存大小,则跳过缓存,直接从底层 io.Reader 中读
// 出到 p 中。
// 2、len(p) < 缓存大小,则先将数据从底层 io.Reader 中读取到缓存
// 中,再从缓存读取到 p 中。
func (b *Reader) Read(p []byte) (n int, err error)
// Buffered 返回缓存中未读取的数据的长度。
func (b *Reader) Buffered() int
// ReadBytes 功能同 ReadSlice,只不过返回的是缓存的拷贝。
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
// ReadString 功能同 ReadBytes,只不过返回的是字符串。
func (b *Reader) ReadString(delim byte) (line string, err error)
...
下面我们分析一下这些函数的使用方法。
(3) bufio.NewReader: 实例化 bufio.Reader
我们可以通过 NewReader 函数来实例化一个 bufio.Reader 对象。
1
func NewReader(rd io.Reader) *Reader
(4) bufio.Reader.ReadSlice
在 package bufio 中,提供了 bufio.Reader 的一些方法:ReadSlice、ReadBytes、ReadString 等。我们先以 ReadSlice 为例:
1
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
ReadSlice 从输入中读取,直到遇到第一个界定符(delim)为止,返回一个指向缓存中字节的 slice,在下次调用读操作(read)时,这些字节会无效。请看下面的例子:
1
2
3
4
5
6
7reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))
输出结果如下:
1
2
3
4the line:http://studygolang.com. # Line 3 的输出
the line:It is the home of gophers # Line 6 的输出
It is the home of gophers # Line 7 的输出
可见,第一次 ReadSlice 的结果 line 在第二次 ReadSlice 被调用后发生了改变,这是因为 line 是一个指向 bufio.Reader 缓冲区的 slice,是一个引用类型,而不是缓冲区的一份 copy。bufio.Reader 缓冲区发生改变时,我们从 line 中获取的内容自然也就发生了改变。
(5) bufio.Reader.ReadBytes
再来看 ReadBytes
1
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
ReadBytes 和 ReadSlice 的区别在于 ReadBytes 返回的是一份缓冲区的 copy。再看下面的例子:
1
2
3
4
5
6
7reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))
输出为:
1
2
3
4
5the line:http://studygolang.com.
the line:http://studygolang.com.
It is the home of gophers
(6) bufio.Reader.ReadString
再来看 ReadString
1
2
3
4func (b *Reader) ReadString(delim byte) (line string, err error) {
bytes, err := b.ReadBytes(delim)
return string(bytes), err
}
它调用了 ReadBytes 方法,并将结果的 []byte 转为 string 类型。不再赘述。
(7) bufio.Reader.Peek: 实例化 bufio.Reader
从方法的名称可以猜到,该方法只是 "窥探" 一下 Reader 中没有读取的 n 个字节。好比栈数据结构中的取栈顶元素,但不出栈。
1
func (b *Reader) Peek(n int) ([]byte, error)
同上面介绍的 ReadSlice一样,返回的 []byte 只是 buffer 中的引用,在下次IO操作后会无效,可见该方法(以及ReadSlice这样的,返回buffer引用的方法)对多 goroutine 是不安全的,也就是在多并发环境下,不能依赖其结果。
(8) bufio.Scanner 引言
从上面的 ReadSlice, ReadBytes 和 ReadString 方法我们可以看到,bufio.Reader 结构体中所有读取数据的方法,都包含了 delim 分隔符。有时候我们只是想简单地读取一行,上面所说的这三个函数用起来很不方便,或者说它们的实现太复杂了。所以 Google 对此在 go1.1 版本中加入了 bufio.Scanner 结构体,用于更加方便地读取数据。我们将在 Scanner 类型和方法 中进行介绍。
5.2 Writer 类型和方法
(1) bufio.Writer 的原理
bufio.Writer 是bufio中对io.Writer 的封装
1
2
3
4
5
6
7
8
9
10
11
12// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
bufio.Write(p []byte) 的思路如下:
- 判断 buf 中可用容量是否可以放下 p
- 如果能放下,直接把 p 拼接到 buf 后面,即把内容放到缓冲区
- 如果缓冲区的可用容量不足以放下,且此时缓冲区是空的,直接把 p 写入文件即可
- 如果缓冲区的可用容量不足以放下,且此时缓冲区有内容,则用 p 把缓冲区填满,把缓冲区所有内容写入文件,并清空缓冲区
- 判断 p 的剩余内容大小能否放到缓冲区,如果能放下(此时和步骤 1 情况一样)则把内容放到缓冲区
- 如果 p 的剩余内容依旧大于缓冲区,(注意此时缓冲区是空的,情况和步骤 3 一样)则把 p 的剩余内容直接写入文件
源码如下:
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// Write writes the contents of p into the buffer.
// It returns the number of bytes written.
// If nn < len(p), it also returns an error explaining
// why the write is short.
func (b *Writer) Write(p []byte) (nn int, err error) {
// b.Available() 为buf可用容量,等于 len(buf) - n
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
// Large write, empty buffer.
// Write directly from p to avoid copy.
n, b.err = b.wr.Write(p)
} else {
n = copy(b.buf[b.n:], p)
b.n += n
b.Flush() // b.Flush() 会将缓存区内容写入文件
}
nn += n
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
(2) bufio.Writer 实现的接口
bufio.Writer 实现了接口: io.Writer, io.ReaderFrom, io.ByteWriter。bufio.Writer 实现的方法列举如下:
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// NewWriterSize 将 wr 封装成一个带缓存的 bufio.Writer 对象,
// 缓存大小由 size 指定(如果小于 4096 则会被设置为 4096)。
// 如果 wr 的基类型就是有足够缓存的 bufio.Writer 类型,则直接将
// wr 转换为基类型返回。
func NewWriterSize(wr io.Writer, size int) *Writer
// NewWriter 相当于 NewWriterSize(wr, 4096)
func NewWriter(wr io.Writer) *Writer
// WriteString 功能同 Write,只不过写入的是字符串
func (b *Writer) WriteString(s string) (int, error)
// WriteRune 向 b 写入 r 的 UTF-8 编码,返回 r 的编码长度。
func (b *Writer) WriteRune(r rune) (size int, err error)
// Flush 将缓存中的数据提交到底层的 io.Writer 中
func (b *Writer) Flush() error
// Available 返回缓存中未使用的空间的长度
func (b *Writer) Available() int
// Buffered 返回缓存中未提交的数据的长度
func (b *Writer) Buffered() int
// Reset 将 b 的底层 Writer 重新指定为 w,同时丢弃缓存中的所有数据,复位
// 所有标记和错误信息。相当于创建了一个新的 bufio.Writer。
func (b *Writer) Reset(w io.Writer)
...
(3) bufio.Writer.Available
Available 方法获取 I/O 缓冲区中还未使用的字节数(缓存大小 - 字段 n 的值)
(4) bufio.Writer.Buffered
Buffered 方法获取写入当前 I/O 缓冲区中的字节数(字段 n 的值)
(5) bufio.Writer.Flush
该方法将缓存中的所有数据写入底层的 io.Writer 对象中。使用 bufio.Writer 时,在所有的 Write 操作完成之后,应该调用 Flush 方法使得缓存都写入 io.Writer 对象中。
5.3 Scanner 类型和方法
bufio.Scanner 的主要作用是使用带缓存的 I/O 的办法,把数据流分割成一个个标记并除去它们之间的分隔符。相当于一个用于格式化的 bufio.Reader。
为了去除分隔符,Go 语言提供了四种 ScanWords 方法,ScanBytes(返回单个字节作为一个 token), ScanLines(返回一行文本), ScanRunes(返回单个 UTF-8 编码的 rune 作为一个 token)和ScanWords(返回通过"空格"分词的单词)。bufio.Scanner 的使用例子如下所示:
1
2
3
4
5
6
7
8
9
10
11func main(){
scanner:=bufio.NewScanner(
strings.NewReader("ABCDEFG\nHIJKELM"),
)
/* ScanWords 是四种方式之一,你也可以自定义, 实现 SplitFunc 方法*/
scanner.Split(ScanWords)
for scanner.Scan(){
fmt.Println(scanner.Text()) // scanner.Bytes()
}
}
(1) Scanner 原理
Scanner 定义如下:
1
2
3
4
5
6
7
8
9
10type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
}
这里 split、maxTokenSize 和 token 需要讲解一下。在讲解之前,需要先讲解 split 字段的类型 SplitFunc。
SplitFunc 类型定义如下:
1
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
SplitFunc 定义了 用于对输入进行分词的 split 函数的签名。参数 data 是还未处理的数据,atEOF 标识 Reader 是否还有更多数据(是否到了EOF)。返回值 advance 表示从输入中读取的字节数,token 表示下一个结果数据,err 则代表可能的错误。
比如,对于数据 "studygolang\tpolaris\tgolangchina",通过 "\t" 进行分词,那么会得到三个 token,它们的内容分别是:studygolang、polaris 和 golangchina。而 SplitFunc 的功能是:进行分词,并返回未处理的数据中第一个 token。对于这个数据,就是返回 studygolang。
如果 data 中没有一个完整的 token,例如,在扫描行(scanning lines)时没有换行符,SplitFunc 会返回(0,nil,nil)通知 Scanner 读取更多数据到 slice 中,然后在这个更大的 slice 中同样的读取点处,从输入中重试读取。如下面要讲解的 split 函数的源码中有这样的代码:
1
2// Request more data.
return 0, nil, nil
正如我们在最开始的例子展示的那样,在 bufio 包中预定义了一些 split 函数,也就是说,在 Scanner 结构中的 split 字段,可以通过这些预定义的 split 赋值,同时 Scanner 类型的 Split 方法也可以接收这些预定义函数作为参数。所以,我们可以说,这些预定义 split 函数都是 SplitFunc 类型的实例。这些函数包括:ScanBytes、ScanRunes、ScanWords 和 ScanLines。(由于都是 SplitFunc 的实例,自然这些函数的签名都和 SplitFunc 一样)这些函数如下所示:
- ScanBytes 返回单个字节作为一个 token
- ScanRunes 返回单个 UTF-8 编码的 rune 作为一个 token。返回的 rune 序列(token)和 range string类型 返回的序列是等价的,也就是说,对于无效的 UTF-8 编码会解释为 U+FFFD = "\xef\xbf\xbd"
- ScanWords 返回通过 "空格" 分词的单词。如:study golang,调用会返回 study。注意,这里的"空格" 是 unicode.IsSpace(),即包括:'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)
- ScanLines 返回一行文本,不包括行尾的换行符。这里的换行包括了Windows下的"\r\n"和Unix下的"\n"
一般地,我们不会单独使用这些函数,而是提供给 Scanner 实例使用。现在我们回到 Scanner 的 split、maxTokenSize 和 token 字段上来。
split 字段(SplitFunc 类型实例),很显然,代表了当前 Scanner 使用的分词策略,可以使用上面介绍的预定义 SplitFunc 实例赋值,也可以自定义 SplitFunc 实例。(当然,要给 split 字段赋值,必须调用 Scanner 的 Split 方法)
maxTokenSize 字段表示通过 split 分词后的一个 token 允许的最大长度。在该包中定义了一个常量 MaxScanTokenSize = 64 * 1024,这是允许的最大 token 长度(64k)
token 字段 上文已经解释了这个是什么意思。
(2) Scanner 实例化
bufio 提供了下面的函数用于 Scanner 实例化:
1
func NewScanner(r io.Reader) *Scanner
(3) Scanner 的方法
Split 方法 前面我们提到过可以通过 Split 方法为 Scanner 实例设置分词行为。由于 Scanner 实例的默认 split 总是 ScanLines,如果我们想要用其他的 split,可以通过 Split 方法做到。
1
scanner.Split(bufio.ScanWords)
Scan 方法该方法好比 iterator 中的 Next 方法,它用于将 Scanner 获取下一个 token,以便 Bytes 和 Text 方法可用。当扫描停止时,它返回 false,这时候,要么是到了输入的末尾要么是遇到了一个错误。注意,当 Scan 返回 false 时,通过 Err 方法可以获取第一个遇到的错误(但如果错误是 io.EOF,Err 方法会返回 nil)。
Bytes 和 Text 方法这两个方法的行为一致,都是返回最近的 token,无非 Bytes 返回的是 []byte,Text 返回的是 string。该方法应该在 Scan 调用后调用,而且,下次调用 Scan 会覆盖这次的 token。比如:
1
2
3
4
5scanner := bufio.NewScanner(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
if scanner.Scan() {
scanner.Scan()
fmt.Printf("%s", scanner.Text())
}
返回的是:It is the home of gophers 而不是 http://studygolang.com.
附录:参考源
- 知乎, GO 语言基础进阶教程:bufio 包
- 知乎, 深入理解 Go 标准库之 bufio.Scanner
- Go 语言中文网, 输入输出 (Input/Output)
