文章目录
- 前言
- 简介
- bufio.Reader
- bufio.Writer
- bufio.Reader
- 构造
- ReadString
- ReadLine
- bufio.Writer
- 构造
- WriteString
- Flush
前言
在上一篇文章Golang 怎么高效处理ACM模式输入输出中提到了用bufio来高效处理输入输出,本文来分析bufio高效的原理
主要看bufio.Reader和bufio.Writer如何包装缓冲区和真正的reader,writer,实现高效IO
阅读的go版本:1.22.6
简介
为啥要用bufio?我认为有以下两个方面:
-
提高性能:通过开辟一块缓冲区,减少系统调用的次数
- bufio.Reader:先调一次真正的reader读取一批数据到缓冲区,接下来的read就直接从缓冲区读,无需再调真正的reader发起系统调用
- bufio.Writer:大部分写把数据写到缓冲区就可以返回了,等缓冲区写不下,或调Flush时才把数据往真正的writer写
-
提供易于使用的api:例如调ReadLine,reader会读取并返回一行完整的数据,免去自己手动处理换行符的操作
下面简单介绍下api怎么使用
bufio.Reader
假设要从标准输入读两行文本:
func main() {reader := bufio.NewReader(os.Stdin)datas := []string{}for i := 0; i < 2; i++{data, _, _ := reader.ReadLine()datas = append(datas, string(data))}
}
标准输入:
hello
world
输出结果如下:
[hello world]
bufio.Writer
假设要往控制台输出hello world:
func main() {w := bufio.NewWriter(os.Stdout)fmt.Fprint(w, "Hello, ")fmt.Fprint(w, "world!")w.Flush() // Don't forget to flush!
}
输出结果:
Hello, world!
本文接下来的部分是源码分析
bufio.Reader
type Reader struct {// 缓冲区buf []byte// 真正的readerrd io.Reader // 读,写位置r, w int err errorlastByte int lastRuneSize int
}
其中:
- r:下次从哪来开始读
- w:下次从真正的reader读取数据后,往缓冲区的哪个位置写
构造
构造reader:主要是分配一个缓冲区
func NewReader(rd io.Reader) *Reader {return NewReaderSize(rd, defaultBufSize)
}func NewReaderSize(rd io.Reader, size int) *Reader {// (可以不关心)如果已经是bufio.Reader了,b, ok := rd.(*Reader)if ok && len(b.buf) >= size {return b}r := new(Reader)r.reset(make([]byte, max(size, minReadBufferSize)), rd)return r
}
默认值defaultBufSize
为4096,也可以调bufio.NewReaderSize
自己指定缓冲区长度
reset:设置buf,reader字段
func (b *Reader) reset(buf []byte, r io.Reader) {*b = Reader{buf: buf,rd: r,lastByte: -1,lastRuneSize: -1,}
}
Buffered:返回缓冲区有多少数据可以读
func (b *Reader) Buffered() int { return b.w - b.r }
ReadString
该方法读直到delim为止的所有字符串
例如传delim = '\n’时,表示读一行数据
func (b *Reader) ReadString(delim byte) (string, error) {/**读数据,直到遇到delim字符为止full, frag都是读到的数据*/full, frag, n, err := b.collectFragments(delim)// 将读到的[]byte数据转换成字符串var buf strings.Builderbuf.Grow(n)for _, fb := range full {buf.Write(fb)}// 写最后一段buf.Write(frag)return buf.String(), err
}
内部调collectFragments
读数据,其两个返回值都是读到的数据:
- fullBuffers:没有delim的部分
- finalFragment:最后一段有delim的部分
为啥会有两部分?因为可能缓冲区满了都没遇到delim,于是会把已经读到的部分塞到fullBuffers中,然后清空buf,进行下一次读取。最后读到delim的部分放到finalFragment返回
func (b *Reader) collectFragments(delim byte) (fullBuffers [][]byte, finalFragment []byte, totalLen int, err error) {var frag []bytefor {var e error// 调ReadSlice读,一直遇到delim字符为止frag, e = b.ReadSlice(delim)if e == nil {break}// 遇到非ErrBufferFull的err,也就是非预期的err,break返回if e != ErrBufferFull { err = ebreak}// 到这说明buf都读满了,还没遇到delimbuf := bytes.Clone(frag)// 先把本次读到的暂存到fullBuffers,再继续读fullBuffers = append(fullBuffers, buf)totalLen += len(buf)}totalLen += len(frag)return fullBuffers, frag, totalLen, err
}
内部调ReadSlice方法:
- 如果从buf中能找到
delim
,就返回这一段 - 如果缓冲区buf满了也没读到
delim
,返回给上游特殊错误ErrBufferFull
,并把缓冲区中所有暂时读到的数据返回 - 缓冲区没满,也没读到
delim
,调fill方法从真正的reader中读数据,填充到缓冲区
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {s := 0 for {// 看已读到的数据中有没有特殊字符delimif i := bytes.IndexByte(b.buf[b.r+s:b.w], delim); i >= 0 {// 如果有,截取从b.r到delim的部分,返回i += sline = b.buf[b.r : b.r+i+1]b.r += i + 1break}// 读到err,返回if b.err != nil {line = b.buf[b.r:b.w]b.r = b.werr = b.readErr()break}// 缓冲区满了也没读到,先将缓冲区中所有数据返回// 并用ErrBufferFull告诉调用方缓冲区满了也没读到if b.Buffered() >= len(b.buf) {b.r = b.wline = b.buferr = ErrBufferFullbreak}// 没找到,且缓冲区没满的情况下,b.w - b.r就是这次扫描过的长度// 下次要从b.r+s位置开始扫描,因为已扫描过的不用再扫描了s = b.w - b.r// 调真正的reader读数据,并放到缓冲区中b.fill() }// 设置最后一个字节if i := len(line) - 1; i >= 0 {b.lastByte = int(line[i])b.lastRuneSize = -1}return
}
fill方法:调真正的reader读数据,并填充缓冲区中
- 将已有的数据移动buf头部,此时buf中的数据为:
[b.r : b.w]
,其中b.r
被置为0
- 调真正的reader读数据到
b.buf
中,从b.w
位置开始写
func (b *Reader) fill() {// 将已有的数据移动slice头部if b.r > 0 {copy(b.buf, b.buf[b.r:b.w])b.w -= b.rb.r = 0}if b.w >= len(b.buf) {panic("bufio: tried to fill full buffer")}for i := maxConsecutiveEmptyReads; i > 0; i-- {// 调真正的reader读数据,到b.buf中// 从b.w位置开始写n, err := b.rd.Read(b.buf[b.w:])if n < 0 {panic(errNegativeRead)}b.w += nif err != nil {b.err = errreturn}// 读到数据就返回if n > 0 {return}}b.err = io.ErrNoProgress
}
ReadLine
ReadLint方法的作用是:读一行完整的数据返回,也就是一直读,直到遇到换行符为止
流程为:
- 内部调
ReadSlice
- 如果缓冲区满了也没读到
换行符
,就先把读到的部分返回,并返回isPrefix=true表示该case - 否则读到了,去除末尾的换行符返回
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {line, err = b.ReadSlice('\n')// 如果缓冲区满了还没读到if err == ErrBufferFull {// 处理\r\n跨越缓冲块的边界情况if len(line) > 0 && line[len(line)-1] == '\r' {// 将\r放回去b.r--line = line[:len(line)-1]}return line, true, nil}if len(line) == 0 {if err != nil {line = nil}return}err = nil// 去除末尾的换行符if line[len(line)-1] == '\n' {drop := 1if len(line) > 1 && line[len(line)-2] == '\r' {drop = 2}line = line[:len(line)-drop]}return
}
bufio.Writer
构造
type Writer struct {err error// 输出缓冲区buf []byte// 缓冲区写到哪了n int// 被包装的writerwr io.Writer
}
buf是缓冲区,其中buf[0:n]表示装已经写了的数据,buf[n:] 表示还可以写的空间
Available:缓冲区是否还有剩余空间可写
如果n到最后了,说明没位置可写了
func (b *Writer) Available() int { return len(b.buf) - b.n }
WriteString
整体流程比较直白:
- 如果缓冲区够写,就把字符串s写到缓冲区,返回
- 否则剩下的缓冲区不够写完整的s,就先写一部分,把缓冲区填满,调Flush方法把缓冲区的全部数据写到真正的writer中
- 再把s剩下的部分写到缓冲区中
func (b *Writer) WriteString(s string) (int, error) {var sw io.StringWritertryStringWriter := truenn := 0// 剩余的缓冲区不够写s了for len(s) > b.Available() && b.err == nil {var n int// 先把s的一部分拷贝到buf中n = copy(b.buf[b.n:], s)b.n += n// 调真正的writer把buf的数据写出去b.Flush()nn += n// 扣减本次写过的部分,for循环下次写剩下的部分s = s[n:]}if b.err != nil {return nn, b.err}// 到这说明缓存空间还够写s,那就把s完整复制到buf中n := copy(b.buf[b.n:], s)b.n += nnn += nreturn nn, nil
}
Flush
将缓冲区中的所有数据调真正的writer写出去,然后清空缓冲区
func (b *Writer) Flush() error {if b.err != nil {return b.err}if b.n == 0 {return nil}// 调真正的writer,写buf中的所有数据n, err := b.wr.Write(b.buf[0:b.n])if n < b.n && err == nil {err = io.ErrShortWrite}if err != nil {// 返回err,但写成功了一部分,写成功的这部分从缓冲区移除if n > 0 && n < b.n {copy(b.buf[0:b.n-n], b.buf[n:b.n])}b.n -= nb.err = errreturn err}// 写完后清空bufb.n = 0return nil
}