Golang的i/o对象及操作

Posted 玩家_名狱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang的i/o对象及操作相关的知识,希望对你有一定的参考价值。

本文部分内容参考:http://c.biancheng.net/view/5569.html

无论是实现 web 应用程序,还是控制台输入输出,又或者是网络操作,都不可避免的会遇到 I/O 操作。
我们平常在代码中使用的基本类型,如string或者[]byte,这些变量的数据都是在用户态下面,如果要写到磁盘,或者写到网卡(网络通信),会触发write系统调用操作,然后系统将用户态下面的数据拷贝到内核态下面的,再由内核态的系统调用写入磁盘,但是从用户态到内核态的转变是要发生系统调用的,而系统调用会影响性能。因此我们希望有一个缓冲空间,每次的write操作都把数据写入到缓冲空间中,等缓冲空间满了,再将缓冲区中的数据拷贝到内核态下,这样就减少了系统调用的次数

内核态与用户态、系统调用与库函数、文件IO与标准IO、缓冲区等概念介绍:https://blog.csdn.net/windeal3203/article/details/79083453
一文搞懂用户缓冲区与内核缓冲区https://blog.csdn.net/Jiangtagong/article/details/108703123

Go语言标准库的 bufio 包中,实现了对数据 I/O 接口的缓冲功能。这些功能封装于接口 io.ReadWriter、io.Reader 和 io.Writer 中,并对应创建了 ReadWriter、Reader 或 Writer 对象,在提供缓冲的同时实现了一些文本基本 I/O 操作功能。

我们平常使用的字符串或者网络发来的数据并不是 ReadWriter、Reader 或 Writer 对象,而要使用到缓冲区,就要求我们要操作的数据必须得是 ReadWriter、Reader 或 Writer 对象,所以我们要对数据转换成 ReadWriter、Reader 或 Writer 对象。

字符串转换成 ReadWriter、Reader 或 Writer 对象

data := []byte("你吃了吗?我吃了")  // 字符串转为字节切片
rd := bytes.NewReader(data)  // 字节切片转为Reader对象
buffRd := bufio.NewReader(rd)  // 将Reader对象放入到缓冲区中

这里只以转换为Reader对象为例,其他的类似,bytes.NewReader方法虽然也能将字节切片转为Reader对象,但是bytes包下面没有提供许多操作文本的API功能,bufio包提供的API更丰富。

一、操作Reader对象

1.1、Read()方法

Read() 方法的功能是读取数据,并存放到字节切片 p 中。Read() 执行结束会返回已读取的字节数,因为最多只调用底层的 io.Reader 一次,所以返回的 n 小于或等于 len( p ),当字节流结束时,n 为 0,err 为 io. EOF。

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	data := []byte("你吃了吗?我吃了")
	rd := bytes.NewReader(data)
	buffRd := bufio.NewReader(rd)
	var buf1 [18]byte
	num1, err := buffRd.Read(buf1[:])       // 注意传递的是切片,而不是数组
	fmt.Println(string(buf1[:]), num1, err) // 缓冲区较小,最多只能读取缓冲区大的字节
	var buf2 [28]byte
	num2, err := buffRd.Read(buf2[:]) // 接着上一次读取的尾部开始
	fmt.Println(string(buf2[:]), num2, err)
}
//你吃了吗?我 18 <nil>
//吃了

1.2、ReadByte()方法

ReadByte() 方法的功能是读取并返回一个字节,如果没有字节可读,则返回错误信息。

data := []byte("h你吃了吗?我吃了")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
c1, err := buffRd.ReadByte()  // 读取英文
fmt.Println(string(c1), err)  // 输出英文正常
c2, err := buffRd.ReadByte()  // 读取中文
fmt.Println(string(c2), err)  // 输出中文正常
//h <nil>
//ä <nil>

如果文本全部是英文,可以使用该方法,如果是中英混合,可以使用下面的ReadRune()方法

1.3、ReadBytes()方法

ReadBytes() 方法的功能是读取数据直到遇到第一个分隔符,并返回读取的字节序列(包括分隔符)。如果 ReadBytes 在读到第一个分隔符之前出错,它返回已读取的数据和那个错误(通常是 io.EOF)。只有当返回的数据不以分隔符结尾时,返回的 err 才不为空值。

data := []byte("h你吃了吗,我吃了")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
var tr byte = ','
c1, err := buffRd.ReadBytes(tr)
fmt.Println(string(c1), err)
c2, err := buffRd.ReadBytes(tr)
fmt.Println(string(c2), err)

分隔符不能为中文,只能是英文。因为分隔符需要是byte类型,也就是一个8位的字节

1.4、ReadLine()方法

ReadLine是一个低级的读行原语。 大多数调用者应该使用ReadBytes(’\\n’)或ReadString(’\\n’)来代替或使用Scanner。

ReadLine尝试返回一行,不包括行结束字节。 如果该行对缓冲区来说太长,则设置isPrefix(英文是是否为前缀的意思)为true并返回该行的开头。 这一行的其余部分将从以后的调用中返回。 返回行的最后一个片段时,isPrefix将为false。 返回的缓冲区只在下一次调用ReadLine之前有效。 ReadLine要么返回一个非nil行,要么返回一个错误,永远不会两者都返回。

ReadLine返回的文本不包括行结束符("\\r\\n"或"\\n")。 如果输入结束时没有最后一行结束,则没有给出任何指示或错误。 在ReadLine之后调用UnreadByte将始终未读取读到的最后一个字节(可能是属于行结束符的字符),即使该字节不属于ReadLine返回的行。

data := []byte("h你吃了吗?\\r\\n我吃了\\n哈哈")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
line, prefix, err := buffRd.ReadLine()
fmt.Println(string(line), prefix, err)

line2, prefix2, err := buffRd.ReadLine()
fmt.Println(string(line2), prefix2, err)
//h你吃了吗? false <nil>
//我吃了 false <nil>

分隔符可以是/r/n,也可以是/n,或者两者可同时使用

1.5、ReadRune()方法

ReadRune读取一个UTF-8编码的Unicode字符,并返回符文及其字节大小。 如果编码的符文无效,它将只读取一个字节并消耗这一个字节,同时返回unicode.ReplacementChar (U+FFFD),大小为1。

data := []byte("h你吃了吗?\\r\\n我吃了\\n哈哈")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
char, size, err := buffRd.ReadRune()
fmt.Println(string(char), size, err)

char2, size2, err := buffRd.ReadRune()
fmt.Println(string(char2), size2, err)
//h 1 <nil>
//你 3 <nil>

1.6、ReadString()方法

ReadString读取到输入中分隔符第一次出现为止,返回一个包含分隔符的数据的字符串。 如果ReadString在找到分隔符之前遇到错误,则返回在错误之前读取的数据和错误本身(通常是io.EOF)。 当且仅当返回的数据不以分隔符结尾时,ReadString返回err != nil。

data := []byte("h你吃了吗,我吃了")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
var tr byte = ','
str, err := buffRd.ReadString(tr)
fmt.Println(string(str), err)
str2, err := buffRd.ReadString(tr)
fmt.Println(string(str2), err)
//h你吃了吗, <nil>
//我吃了 EOF

1.7、Buffered()方法

Buffered() 方法的功能是返回可从缓冲区读出数据的字节数

data := []byte("h你吃了吗,我吃了")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
fmt.Println(buffRd.Buffered())  // 操作前计数
var buf [12]byte
n, err := buffRd.Read(buf[:])
fmt.Println(string(buf[:n]), n, err)
fmt.Println(buffRd.Buffered())  // 计数时计数
var buf2 [12]byte
n2, err := buffRd.Read(buf2[:])
fmt.Println(string(buf2[:n]), n2, err)
fmt.Println(buffRd.Buffered())  // 计数后计数
//0
//h你吃了� 12 <nil>
//11
//�,我吃了

从结果可以看出几个问题:

  • bufio.NewReader(rd)并没有真正的将数据写入到缓冲区,直到使用类似 buffRd.Read()等方法操作时,数据才会写入到该缓冲区对象中
  • 操作的字符是以字节为单位时,中文出现乱码,但是出现问题的就只有这么一个字符,其他字符不受影响

1.8、Peek()方法

Peek返回下一个n个字节,而没有推进读取器,即这些被读取的数据不会从缓冲区中清除。 这些字节在下一个read调用时停止有效。 如果Peek返回的字节小于n,它还会返回一个错误说明原因。 如果n大于缓冲区大小,则错误为ErrBufferFull。

调用Peek会阻止UnreadByte或UnreadRune调用成功,直到下一次读取操作。

data := []byte("h你吃了吗,我吃了")
rd := bytes.NewReader(data)
buffRd := bufio.NewReader(rd)
da, err := buffRd.Peek(4)  // 读取4个字节
fmt.Println(string(da), err)
da2, err := buffRd.Peek(10)  // 读取10个字节
fmt.Println(string(da2), err)
da3, err := buffRd.Peek(17)  // 读取17个字节
fmt.Println(string(da3), err)
//h你 <nil>
//h你吃了 <nil>
//h你吃了吗,我 <nil>

二、操作Writer对象

操作 Writer 对象的方法共有 7 个,分别是 Available()、Buffered()、Flush()、Write()、WriteByte()、WriteRune() 和 WriteString() 方法

2.1、Available()方法

返回缓冲区中未使用的字节数

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
	wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
	fmt.Println("写入前缓冲区未使用的字节数:", wo.Available())
	var data = []byte("hello")
	wo.Write(data)
	fmt.Println("写入后缓冲区未使用的字节数:", wo.Available())
}
//写入前缓冲区未使用的字节数: 4096
//写入后缓冲区未使用的字节数: 4091

2.2、Buffered()方法

返回已写入当前缓冲区中的字节数

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
	wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
	fmt.Println("写入前缓冲区使用的字节数:", wo.Buffered())
	var data = []byte("hello")
	wo.Write(data)
	fmt.Println("写入后缓冲区使用的字节数:", wo.Buffered())
}
//写入前缓冲区使用的字节数: 0
//写入后缓冲区使用的字节数: 5

2.3、Flush()方法

把缓冲区中的数据写入底层的 io.Writer,并返回错误信息。如果成功写入,error 返回 nil,否则 error 返回错误原因。

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
	wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
	fmt.Println("缓冲区是空白的:", string(buf.Bytes()))
	var data = []byte("hello")
	wo.Write(data)
	fmt.Println("Write操作缓冲区后:", string(buf.Bytes()))
	wo.Flush()
	fmt.Println("Write加Flush操作缓冲区后:", string(buf.Bytes()))
}
//缓冲区是空白的: 
//Write操作缓冲区后: 
//Write加Flush操作缓冲区后: hello

结果表明:Write操作并没有将数据写入到缓冲区,而是Write对象指向了数据,等使用Flush操作后才将指向的数据写入缓冲区。但是但数据超过缓冲区的最大值时,就会自动触发Flush操作,并且扩容缓冲区

超过4096个字符后

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
	wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
	fmt.Println("缓冲区是空白的:", string(buf.Bytes()))
	var str string = `giao
	此处省略4096个字符
	`
	var data = []byte(str)
	wo.Write(data)
	fmt.Println("Write操作缓冲区后:", string(buf.Bytes()))
	// wo.Flush()
	// fmt.Println("Write加Flush操作缓冲区后:", string(buf.Bytes()))
}
//缓冲区是空白的: 
//Write操作缓冲区后: giao
//……

2.4、WriteRune()方法

WriteRune() 方法的功能是以 UTF-8 编码写入一个 Unicode 字符,返回写入的字节数和错误信息。

buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
var str rune = '你'
size, err := wo.WriteRune(str)
wo.Flush()
fmt.Println(string(buf.Bytes()), size, err)
//你 3 <nil>

2.5、WriteString()方法

WriteString() 方法的功能是写入一个字符串,并返回写入的字节数和错误信息。如果返回的字节数小于 len(s),则同时返回一个错误说明原因。

buf := bytes.NewBuffer(nil) // 创建一个空白的缓冲区
wo := bufio.NewWriter(buf)  // 创建Writer对象,该对象使用创建的空白缓冲区作为写入区域
var str = "你好啊~~~"
size, err := wo.WriteString(str)
wo.Flush()
fmt.Println(string(buf.Bytes()), size, err)

三、操作ReadWriter对象

ReadWriter对象其实只是整合了Reader对象和Writer对象,使得ReadWriter对象同时具备Reader对象和Writer对象的所有功能

package main

import (
	"bufio"
	"bytes"
	"fmt"
)

func main() {
	readBuf := bytes.NewBuffer(nil)       // 创建一个空白缓冲区给Reader对象操作
	readObj := bufio.NewReader(readBuf)   // 创建一个Reader对象
	writeBuf := bytes.NewBuffer(nil)      // 创建一个空白缓冲区给Writer对象操作
	writeObj := bufio.NewWriter(writeBuf) // 创建一个Writer对象
	// 创建ReadWriter对象,该对象同时具备Reader对象和Writer对象的所有方法
	readWriteObj := bufio.NewReadWriter(readObj, writeObj)
	// 使用ReadWriter对象的写方法
	readWriteObj.Write([]byte("写对象"))
	readWriteObj.Flush()
	fmt.Println(string(writeBuf.Bytes()))
	// 使用ReadWriter对象的读方法
	readBuf.Write([]byte("读对象"))
	var readData [11]byte
	readObj.Read(readData[:])
	fmt.Println(string(readData[:]))
}
//写对象
//读对象

以上是关于Golang的i/o对象及操作的主要内容,如果未能解决你的问题,请参考以下文章

golang rpc介绍

Java输入及输出处理(I/O流)

在golang磁盘写入性能

golang goroutine例子[golang并发代码片段]

《文件与I/O流》第4节:对象序列化

Java I/O 操作及优化建议