GoClub Pro

Go-bufio

最近更新:2026-10-24
本页是手写/批注类笔记,直接按原始笔记截图呈现(转成文字会乱序);原始笔记文件见「Go语言相关笔记」页的原始链接。
onenote-go-bufio 原始笔记截图 1

附件文档全文(Go bufio )#

Go语言 bufio 包功能用法及核心概念详解

你想要详细了解Go语言中bufio包的功能、用法和核心概念,我会从基础到进阶,用通俗易懂的方式为你讲解这个高频使用的标准库包。

一、bufio包的核心定位

bufio(buffered I/O)是Go标准库中对io包的缓冲增强封装,它在底层io.Reader/io.Writer之上增加了缓冲区,避免频繁的系统调用,大幅提升I/O操作的性能(尤其是小数据量、高频次读写场景)。

简单类比:直接用io包读写就像每次接水都接一杯就倒,频繁往返;而bufio就像先接满一桶水,再慢慢倒,减少往返次数。

二、bufio包的核心组件

bufio的核心分为三大类组件,对应不同的I/O场景:

  1. Reader:带缓冲的读取器
  2. Writer:带缓冲的写入器
  3. Scanner:简化的按行/按分隔符读取器(基于Reader封装)

1. 核心组件1:bufio.Reader(缓冲读取)

1.1 基本创建

通过bufio.NewReader创建,需传入一个基础的io.Reader(如os.File、strings.Reader、net.Conn等):

Go
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
// 打开文件(基础io.Reader)
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()

// 创建缓冲读取器(默认缓冲区大小4096字节)
reader := bufio.NewReader(file)

// 也可以指定缓冲区大小:bufio.NewReaderSize(file, 8192)
}

1.2 核心方法(高频)

方法

功能

适用场景

ReadByte()

读取1个字节

逐字节读取

ReadRune()

读取1个Unicode字符(处理多字节字符)

读取中文等多字节文本

ReadString(delim byte)

读取到指定分隔符(包含分隔符)

按自定义分隔符读取(如按逗号、换行符)

ReadLine()

读取一行(不包含换行符,低级别方法)

底层实现,一般用ReadString('\n')替代

ReadBytes(delim byte)

读取到指定分隔符,返回[]byte

二进制数据按分隔符读取

Peek(n int)

预览前n个字节(不移动读取指针)

提前判断数据格式

Read(p []byte)

读取数据到切片(兼容io.Reader)

批量读取

1.3 实战示例:按行读取文件

Go
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()

reader := bufio.NewReader(file)

// 循环读取每一行,直到EOF(文件结束)
for {
// 读取到换行符为止(包含换行符)
line, err := reader.ReadString('\n')
if err != nil {
// io.EOF是正常结束,其他是错误
if err.Error() == "EOF" {
// 处理最后一行(可能没有换行符)
if len(line) > 0 {
fmt.Print(line)
}
break
}
fmt.Println("读取失败:", err)
return
}
// 输出读取的行
fmt.Print(line)
}
}

2. 核心组件2:bufio.Writer(缓冲写入)

2.1 基本创建

通过bufio.NewWriter创建,传入基础io.Writer:

Go
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
// 打开文件(基础io.Writer,追加写入)
file, err := os.OpenFile("output.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()

// 创建缓冲写入器(默认4096字节缓冲区)
writer := bufio.NewWriter(file)

// 也可以指定缓冲区大小:bufio.NewWriterSize(file, 8192)
}

2.2 核心方法(高频)

方法

功能

注意事项

Write(p []byte)

写入字节切片到缓冲区

不立即刷盘

WriteString(s string)

写入字符串到缓冲区

最常用,简化字符串写入

WriteByte(c byte)

写入单个字节

逐字节写入

WriteRune(r rune)

写入单个Unicode字符

处理多字节字符

Flush()

刷新缓冲区:将缓冲区数据写入底层Writer

必须调用!否则数据可能丢失

Available()

返回缓冲区剩余可用空间

判断是否需要手动Flush

Buffered()

返回缓冲区中未刷新的字节数

监控缓冲区状态

2.3 实战示例:缓冲写入文件

Go
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
file, err := os.OpenFile("output.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()

writer := bufio.NewWriter(file)

// 写入数据到缓冲区(不立即刷盘)
data := []string{
"Hello, bufio!\n",
"Go语言缓冲I/O示例\n",
"缓冲区大小默认4096字节\n",
}

for _, s := range data {
// 写入字符串到缓冲区
_, err := writer.WriteString(s)
if err != nil {
fmt.Println("写入缓冲区失败:", err)
return
}
fmt.Printf("已写入缓冲区: %q (缓冲区未刷新字节数: %d)\n", s, writer.Buffered())
}

// 关键:刷新缓冲区,将数据写入文件
err = writer.Flush()
if err != nil {
fmt.Println("刷新缓冲区失败:", err)
return
}
fmt.Println("所有数据已写入文件")
}

关键注意点:如果不调用Flush(),当缓冲区未满时,数据会一直留在内存中,程序退出后数据丢失!建议在defer中调用Flush():

Go
defer func() {
if err := writer.Flush(); err != nil {
fmt.Println("Flush失败:", err)
}
}()

3. 核心组件3:bufio.Scanner(简化读取)

Scanner是bufio中更易用的读取器,专门用于按“块”读取数据(默认按行),封装了bufio.Reader,适合快速实现按行/按自定义分隔符读取。

3.1 基本使用(按行读取)

Go
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()

// 创建Scanner
scanner := bufio.NewScanner(file)

// 循环扫描每一行(默认分割符是换行符)
lineNum := 1
for scanner.Scan() {
// 获取当前行内容
line := scanner.Text() // 返回string
// 也可以获取[]byte:scanner.Bytes()
fmt.Printf("第%d行: %s\n", lineNum, line)
lineNum++
}

// 检查扫描过程中是否出错
if err := scanner.Err(); err != nil {
fmt.Println("扫描失败:", err)
}
}

3.2 自定义分割符

Scanner支持通过Split方法自定义分割规则,内置的分割函数有:

  • bufio.ScanLines:按行分割(默认)
  • bufio.ScanWords:按单词分割(空格分隔)
  • bufio.ScanRunes:按Unicode字符分割
  • bufio.ScanBytes:按字节分割

示例:按单词读取

Go
package main

import (
"bufio"
"fmt"
"strings"
)

func main() {
// 从字符串创建Reader
data := "Hello Go bufio Scanner"
reader := strings.NewReader(data)
scanner := bufio.NewScanner(reader)

// 设置按单词分割
scanner.Split(bufio.ScanWords)

for scanner.Scan() {
fmt.Println("单词:", scanner.Text())
}

if err := scanner.Err(); err != nil {
fmt.Println("扫描失败:", err)
}
}

自定义分割函数示例(按逗号分割):

Go
package main

import (
"bufio"
"fmt"
"strings"
)

// 自定义分割函数:按逗号分割
func commaSplit(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 查找逗号的位置
for i := 0; i < len(data); i++ {
if data[i] == ',' {
// 返回逗号前的内容,跳过逗号
return i + 1, data[:i], nil
}
}
// 如果到了EOF且有剩余数据,返回剩余部分
if atEOF && len(data) > 0 {
return len(data), data, nil
}
// 没有找到分隔符,需要更多数据
return 0, nil, nil
}

func main() {
data := "apple,banana,orange,grape"
reader := strings.NewReader(data)
scanner := bufio.NewScanner(reader)

// 设置自定义分割函数
scanner.Split(commaSplit)

for scanner.Scan() {
fmt.Println("水果:", scanner.Text())
}

if err := scanner.Err(); err != nil {
fmt.Println("扫描失败:", err)
}
}

4. bufio的性能优势

为什么需要缓冲?举个简单对比:

  • 无缓冲:每次写1字节,需要调用1次系统调用(如write()),1000次写入就是1000次系统调用。
  • 有缓冲:先把1000字节写入内存缓冲区,满了后一次性调用1次系统调用刷盘,大幅减少系统调用次数。

测试结论:小数据量高频读写场景下,bufio比原生io快10-100倍。

总结

  1. bufio是Go的缓冲I/O库,核心作用是减少系统调用、提升I/O性能,基于io.Reader/io.Writer封装。
  2. 核心组件:Reader(缓冲读取)、Writer(缓冲写入,必须调用Flush()刷盘)、Scanner(简化按块读取,默认按行)。
  3. 高频用法:ReadString('\n')按行读取、WriteString()写入字符串、Scanner.Scan()循环读取。

掌握bufio是Go处理文件、网络I/O的基础,日常开发中绝大多数I/O场景都应该优先使用bufio而非原生io。

|(注:文档部分内容可能由 AI 生成)

附件原文:onenote-go-bufio-Go_bufio_.docx(点击下载)

附件文档全文(Go io Read Write )#

Go 语言 io 包中 Read 和 Write 方法的本质差异解析

你想弄清楚Go语言io包中核心的Read和Write方法的本质区别,我会从核心语义、参数/返回值、执行逻辑、使用场景 四个维度,把这两个方法的差异讲透,同时补充新手容易混淆的细节。

一、核心语义的本质区别

Read和Write是io包中对立且互补的两个方法,核心语义的差异是:

维度

Read(p []byte)

Write(p []byte)

核心目的

从「数据源」(如文件、网络、内存)读取数据到用户提供的缓冲区p 中

将「用户提供的缓冲区p中的数据」写入到数据目标(如文件、网络、内存)中

数据流向

数据源 → 内存缓冲区p(外部→内部)

内存缓冲区p → 数据目标(内部→外部)

操作对象

读取「数据源」的内容,p是“接收容器”

写入「数据目标」的内容,p是“待发数据”

核心角色

你是“消费者”,从数据源拿数据

你是“生产者”,向目标送数据

二、参数与返回值的细节区别

两者的方法签名看似相似,但每个参数/返回值的含义和使用逻辑完全不同:

Go
// Read:从数据源读数据到p
func (r Reader) Read(p []byte) (n int, err error)

// Write:把p中的数据写到目标
func (w Writer) Write(p []byte) (n int, err error)

1. 参数p []byte的区别

维度

Read的p

Write的p

作用

用于「接收」从数据源读取的数据

用于「提供」要写入到目标的数据

数据状态

调用前:p是空白/旧数据(会被覆盖);调用后:p[:n]是新读取的数据

调用前:p必须填充好要写入的数据;调用后:p内容不变(数据已被拷贝到目标)

容量要求

len(p)决定“最多能读取多少字节”(越大单次读取越多,但内存占用高)

len(p)决定“要写入的总字节数”(必须是已填充数据的长度)

2. 返回值n int的区别

n都是「实际处理的字节数」,但含义完全不同:

维度

Read的n

Write的n

含义

实际从数据源读取到p中的字节数(0 ≤ n ≤ len(p))

实际从p中写入到目标的字节数(0 ≤ n ≤ len(p))

常见场景

n < len(p) 是正常的(如数据源只剩5字节,但p长度是10)

n < len(p) 是异常的(表示写入失败,需返回非nil错误,如io.ErrShortWrite)

特殊值

n=0 通常伴随err=io.EOF(数据源读完)

n=0 通常伴随非nil错误(如写入权限不足)

3. 返回值err error的区别

维度

Read的err

Write的err

正常结束

err=io.EOF(数据源读取完毕)

err=nil(全部写入成功)

部分成功

允许n>0且err=io.EOF(最后一次读取了部分数据,然后到末尾)

不允许n>0且err=nil(若n<len(p),必须返回err)

常见错误

io.EOF、os.ErrNotExist(文件不存在)、网络断开

os.ErrPermission(权限不足)、磁盘满、网络断开

三、执行逻辑的核心区别

1. Read的执行逻辑

Read的核心是“尽可能读,但不保证读满”,步骤:

  1. 检查数据源是否有数据(如文件指针是否到末尾、网络是否有数据);
  2. 从数据源读取数据,填充到p中,填充多少算多少(最多len(p)字节);
  3. 返回实际填充的字节数n,以及对应的错误(如io.EOF表示读完);
  4. 即使只填充了部分数据(n < len(p)),只要没到末尾且无错误,err也为nil。

示例:数据源只剩3字节,p长度为10

Go
buf := make([]byte, 10)
n, err := reader.Read(buf)
// n=3(实际读了3字节),err=nil(还没到EOF)
// 下次调用Read:n=0,err=io.EOF

2. Write的执行逻辑

Write的核心是“尽可能写,写不满则报错”,步骤:

  1. 检查数据目标是否可写入(如文件是否可写、网络是否连通);
  2. 把p中的数据写入到目标,写入多少算多少;
  3. 如果全部写入(n=len(p)),返回n=len(p)、err=nil;
  4. 如果只写入部分(n < len(p)),必须返回n和非nil错误(如io.ErrShortWrite);
  5. 若完全写入失败(n=0),返回n=0和具体错误(如权限不足)。

示例:p长度为10,磁盘只剩5字节空间

Go
buf := []byte("0123456789")
n, err := writer.Write(buf)
// n=5(只写入了5字节),err=io.ErrShortWrite(写入不完整)

四、使用场景与代码示例的区别

1. Read的典型使用场景(循环读取)

因为Read不保证读满,必须循环读取直到io.EOF:

Go
// 读取文件全部内容(Read的典型用法)
func readFileContent(filePath string) ([]byte, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()

// 定义缓冲区(接收数据)
buf := make([]byte, 1024)
var result []byte

// 循环读取:直到EOF
for {
n, err := file.Read(buf) // 从文件(数据源)读数据到buf
if n > 0 {
// 把读取到的有效数据(buf[:n])追加到结果
result = append(result, buf[:n]...)
}
if err == io.EOF {
return result, nil // 读取完成
}
if err != nil {
return nil, err // 其他错误
}
}
}

2. Write的典型使用场景(循环写入)

因为Write可能写不满,必须循环写入直到全部数据写完:

Go
// 写入全部数据到文件(Write的典型用法)
func writeFileContent(filePath string, data []byte) (int, error) {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return 0, err
}
defer file.Close()

totalWritten := 0
// 循环写入:直到全部数据写入
for totalWritten < len(data) {
// 把data[totalWritten:]中的数据写入文件(目标)
n, err := file.Write(data[totalWritten:])
if err != nil {
return totalWritten, err // 写入失败,返回已写入的字节数
}
totalWritten += n
}
return totalWritten, nil // 全部写入成功
}

五、新手易混淆的关键细节

  1. “谁主动”的误区:
  • 不是你“主动读”,而是Read方法把数据源的数据“填到”你的缓冲区;
  • 不是你“主动写”,而是Write方法把你的缓冲区数据“拿走去”写入目标。
  1. 缓冲区所有权:
  • Read的p是你的缓冲区,方法会修改它的内容;
  • Write的p是你的数据,方法只会读取它的内容,不会修改。
  1. EOF的区别:
  • Read会返回io.EOF(正常结束标记);
  • Write永远不会返回io.EOF(写入目标没有“末尾”的概念)。

总结

  1. 核心流向:Read是「数据源→缓冲区」(拿数据),Write是「缓冲区→目标」(送数据);
  2. 参数角色:Read的p是“接收容器”,Write的p是“待发数据”;
  3. 返回值逻辑:Read允许n<len(p)且err=nil,Write不允许;Read有io.EOF,Write无;
  4. 使用方式:两者都需循环调用,Read循环到EOF,Write循环到全部数据写入。

记住这两个方法的核心差异,就能正确处理Go中所有I/O场景,避免出现“读取不完整”“写入丢失数据”等常见问题。

|(注:文档部分内容可能由 AI 生成)

附件原文:onenote-go-bufio-Go_io_Read_Write_.docx(点击下载)

附件文档全文( Bufio )#

深入剖析 Bufio 的底层实现原理

你想深入了解bufio包的底层实现原理,我会从缓冲区结构、核心方法执行流程、性能优化的本质这几个维度,拆解bufio的底层逻辑,让你明白它“缓冲”的核心机制。

一、bufio的核心底层结构

bufio的所有功能都基于内存缓冲区 + 对底层io.Reader/io.Writer的封装 实现,先看最核心的两个结构体(简化版):

1. bufio.Reader 底层结构

Go
type Reader struct {
// 底层的原始读取器(如os.File、net.Conn)
rd io.Reader
// 核心缓冲区:存储从底层Reader读取的字节数据
buf []byte
// r: 缓冲区中已读取的字节数(读指针,指向下一个可读取的位置)
// w: 缓冲区中已写入的字节数(写指针,指向下一个可写入的位置)
r, w int
// 错误状态(如EOF、读取错误)
err error
// 标记是否调用过ReadByte/UnreadByte等方法
lastByte int
lastRune int
}

  • 缓冲区buf是一个固定大小的字节切片(默认4096字节,可通过NewReaderSize指定)。
  • r和w是两个核心指针:
  • 初始时r=0, w=0,缓冲区为空;
  • 从底层读取数据时,数据被填充到buf[w:],然后w后移;
  • 从缓冲区读取数据时,从buf[r:]取数据,然后r后移;
  • 当r == w时,缓冲区为空,需要重新从底层读取数据填充。

2. bufio.Writer 底层结构

Go
type Writer struct {
// 底层的原始写入器
wr io.Writer
// 核心缓冲区:存储待写入到底层Writer的字节数据
buf []byte
// n: 缓冲区中已写入的字节数(写指针)
n int
// 错误状态
err error
}

  • 缓冲区buf同样是固定大小的字节切片;
  • n是写指针,指向缓冲区中下一个可写入的位置,初始为0;
  • 当n == len(buf)时,缓冲区满,触发自动刷盘(调用Flush())。

二、bufio.Reader 底层读取流程

bufio.Reader的核心逻辑是:优先从缓冲区读取数据,缓冲区空了再批量从底层Reader读取数据填充缓冲区,避免每次读取都调用底层IO。

以最常用的ReadString('\n')为例,拆解完整执行流程:

步骤1:初始状态

缓冲区为空(r=0, w=0),调用ReadString('\n')时,先检查缓冲区是否有数据。

步骤2:填充缓冲区

因为缓冲区为空,调用reader.fill()方法从底层rd(如文件)批量读取数据:

Go
// 简化版fill逻辑
func (b *Reader) fill() {
// 1. 如果缓冲区有未读数据,先压缩(把未读数据移到缓冲区开头)
if b.r > 0 {
copy(b.buf, b.buf[b.r:b.w])
b.w -= b.r
b.r = 0
}

// 2. 从底层Reader读取数据,填充到buf[b.w:]
n, err := b.rd.Read(b.buf[b.w:])
b.w += n
b.err = err
}

  • 一次读取最多填满整个缓冲区(如4096字节),而不是只读取1行/1字节;
  • 这是性能优化的核心:用一次底层IO调用,换取多次内存读取。

步骤3:从缓冲区查找分隔符

填充缓冲区后,在buf[r:w]范围内查找换行符\n:

  • 如果找到:截取buf[r:找到的位置+1]作为结果,然后r移动到“找到的位置+1”;
  • 如果没找到:
  1. 先把当前缓冲区所有数据返回(作为临时结果);
  2. 再次调用fill()填充缓冲区;
  3. 重复查找,直到找到换行符或遇到EOF。

步骤4:后续读取复用缓冲区

下次调用ReadString('\n')时,优先从缓冲区buf[r:w]读取,直到r == w(缓冲区空),才会再次调用fill()。

关键优化点:Peek方法的底层逻辑

Peek(n int)是“预览”数据而不移动读指针,底层只是简单返回buf[r:r+n](不修改r),完全是内存操作,无任何IO开销。

三、bufio.Writer 底层写入流程

bufio.Writer的核心逻辑是:优先写入到内存缓冲区,缓冲区满/手动Flush时,批量写入到底层Writer。

以WriteString(s string)为例,拆解流程:

步骤1:检查缓冲区剩余空间

调用WriteString时,先计算缓冲区剩余空间:free = len(buf) - n(n是当前已写入的字节数)。

步骤2:写入数据到缓冲区

  • 如果字符串长度 ≤ free:直接把字符串拷贝到buf[n:n+len(s)],然后n += len(s);
  • 如果字符串长度 > free:
  1. 先调用Flush()把当前缓冲区数据写入底层Writer;
  2. 如果字符串长度 > 缓冲区总大小(如写入10KB字符串,缓冲区4KB):直接把字符串写入底层Writer(避免多次拷贝);
  3. 否则:把字符串拷贝到空的缓冲区。

步骤3:刷盘(Flush)的底层逻辑

Flush()是Writer的核心方法,底层实现:

Go
// 简化版Flush逻辑
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
// 缓冲区有数据才写入
if b.n == 0 {
return nil
}
// 批量写入到底层Writer
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
// 写入失败,保留未写入的部分
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
// 清空缓冲区
b.n = 0
return nil
}

  • Flush()的本质是:把缓冲区中buf[0:n]的所有数据,一次性写入底层wr;
  • 未调用Flush()时,数据只存在于内存缓冲区,程序退出会丢失(这是新手最容易踩的坑)。

自动刷盘的场景

除了手动调用Flush(),Writer在以下场景会自动刷盘:

  1. 缓冲区满(n == len(buf));
  2. 调用Write/WriteString时,数据长度超过缓冲区剩余空间;
  3. Writer被关闭时(部分场景,如os.File的Close会触发Flush,但不依赖这个)。

四、bufio.Scanner 底层原理

Scanner是对bufio.Reader的封装,核心增加了“分割逻辑”,底层结构简化:

Go
type Scanner struct {
// 底层的缓冲读取器
r *Reader
// 分割函数(如按行、按单词)
split SplitFunc
// 缓存当前分割出的token(如一行数据)
buf []byte
// 错误状态
err error
}

Scanner.Scan()的底层流程:

  1. 调用r.Read从缓冲区读取数据到buf;
  2. 调用split分割函数(如ScanLines)处理buf,分割出一个token(如一行);
  3. 把分割后的token缓存,通过Text()/Bytes()返回;
  4. 剩余未分割的数据保留在buf中,下次Scan()继续处理。

五、bufio性能优化的本质

为什么bufio比原生io快?核心是减少系统调用次数:

  • 原生io:每次Read/Write都会触发一次系统调用(如Linux的read()/write()),系统调用需要从用户态切换到内核态,开销大;
  • bufio:
  1. 读取时:一次系统调用读取大量数据到内存缓冲区,后续读取都是内存操作;
  2. 写入时:多次内存写入缓冲区,一次系统调用刷盘到底层存储。

数据对比:

  • 原生io写1000次1字节数据:触发1000次系统调用;
  • bufio写1000次1字节数据:触发1次系统调用(缓冲区满后刷盘);
  • 系统调用的开销是内存操作的1000+倍,这是bufio性能优势的根本原因。

六、缓冲区大小的底层考量

bufio默认缓冲区大小是4096字节(4KB),这不是随便定的:

  1. 匹配操作系统的页大小(Linux/Windows默认页大小都是4KB),减少内存分页开销;
  2. 过小(如128字节):缓冲区频繁填满,系统调用次数增加;
  3. 过大(如64KB):内存占用增加,且单次系统调用读取的数据过多,可能导致内存浪费;
  4. 自定义场景:
  • 小文件/高频小数据:用默认4KB;
  • 大文件/批量数据:可设置8KB/16KB(不建议超过64KB)。

总结

  1. bufio的核心是内存缓冲区 + 读写指针:Reader通过r/w指针管理缓冲区读取,Writer通过n指针管理缓冲区写入;
  2. 性能优化本质:用“一次底层IO + 多次内存操作”替代“多次底层IO”,减少用户态↔内核态的切换开销;
  3. 核心方法逻辑:
  • Reader:缓冲区空→fill()填充→内存读取;
  • Writer:内存写入→缓冲区满/Flush→批量刷盘;
  • Scanner:基于Reader封装,增加分割逻辑简化读取。

理解这些底层原理后,你就能精准把控bufio的使用场景(如何时自定义缓冲区大小、何时手动Flush),避免踩坑(如Writer忘记Flush导致数据丢失)。

|(注:文档部分内容可能由 AI 生成)

附件原文:onenote-go-bufio-_Bufio_.docx(点击下载)

附件文档全文(io )#

io包详解

你想要详细了解Go语言标准库中io包的核心内容、设计思想和底层用法,我会从io包的定位、核心接口、常用函数、底层设计逻辑等维度,把这个Go I/O编程的“基础骨架”讲清楚,帮你理解io包是如何抽象和统一所有I/O操作的。

一、io包的核心定位

io包是Go语言所有I/O操作的基础抽象层,它不直接实现具体的I/O(如文件、网络、内存),而是定义了一套通用的、标准化的I/O接口(如Reader、Writer),以及基于这些接口的通用工具函数。

简单来说:

  • io包的核心是接口定义 + 通用I/O工具;
  • 所有具体的I/O实现(如os.File、net.Conn、bytes.Buffer)都遵循io包的接口规范;
  • bufio、ioutil(Go 1.16后并入io/os)等包都是基于io接口的上层封装。

二、io包的核心接口(最基础、最重要)

io包的灵魂是4个核心接口,几乎所有I/O操作都围绕它们展开,先看最核心的两个:

1. io.Reader:读取数据的通用接口

Go
// Reader 定义了从数据源读取字节的基本方法
type Reader interface {
// Read 将数据读取到字节切片 p 中,返回两个值:
// n: 实际读取的字节数(0 ≤ n ≤ len(p))
// err: 错误信息(如EOF表示读取到末尾,nil表示成功)
Read(p []byte) (n int, err error)
}

核心规则:

  • Read方法会尽可能读取len(p)字节的数据,但不保证一定读满(这是新手最容易误解的点);
  • 当读取到数据源末尾时,返回n=0和err=io.EOF(如果最后一次读取了部分数据,会先返回n>0和err=nil,下次调用才返回0, io.EOF);
  • 即使返回了n>0,也可能同时返回err(如读取到末尾时,可能读了部分数据,同时返回io.EOF)。

简单实现示例(从字符串读取):

Go
package main

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

func main() {
// strings.Reader 是io.Reader的一个实现(从字符串读取)
reader := strings.NewReader("Hello, io package!")

// 定义缓冲区,每次读取8字节
buf := make([]byte, 8)

for {
n, err := reader.Read(buf)
fmt.Printf("读取到 %d 字节: %s\n", n, buf[:n])
if err != nil {
if err == io.EOF {
fmt.Println("读取到末尾")
break
}
fmt.Println("读取错误:", err)
return
}
}
}

输出结果:

Plain Text
读取到 8 字节: Hello, i
读取到 7 字节: o package!
读取到 0 字节:
读取到末尾

2. io.Writer:写入数据的通用接口

Go
// Writer 定义了向数据目标写入字节的基本方法
type Writer interface {
// Write 将字节切片 p 中的数据写入到目标,返回两个值:
// n: 实际写入的字节数(0 ≤ n ≤ len(p))
// err: 错误信息(nil表示成功)
Write(p []byte) (n int, err error)
}

核心规则:

  • Write方法会尽可能写入len(p)字节的数据,若写入失败,返回n<len(p)和对应的错误;
  • 如果n < len(p)且err == nil,这是不符合规范的(应返回io.ErrShortWrite错误);
  • 写入成功时,n == len(p)且err == nil。

简单实现示例(写入到标准输出):

Go
package main

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

func main() {
// os.Stdout 是io.Writer的实现(标准输出)
writer := os.Stdout

data := []byte("Hello, io.Writer!\n")
n, err := writer.Write(data)
if err != nil {
fmt.Println("写入错误:", err)
return
}
fmt.Printf("成功写入 %d 字节\n", n)
}

输出结果:

Plain Text
Hello, io.Writer!
成功写入 16 字节

3. 扩展接口:io.ReaderFrom / io.WriterTo

针对“从一个Reader读取并写入到另一个Writer”的高频场景,io包定义了更高效的扩展接口:

Go
// ReaderFrom 从r读取数据,直到EOF或出错
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}

// WriterTo 向w写入数据,直到EOF或出错
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}

优势:相比手动循环Read+Write,这些接口的实现通常做了性能优化(如批量读写),比如os.File的ReadFrom会直接调用系统级的sendfile,避免用户态和内核态的拷贝。

4. 其他常用扩展接口

接口

核心方法

用途

io.ReaderAt

ReadAt(p []byte, off int64)

从指定偏移量读取数据(随机读取)

io.WriterAt

WriteAt(p []byte, off int64)

向指定偏移量写入数据(随机写入)

io.Seeker

Seek(offset int64, whence int)

调整读写指针位置(如文件的Seek)

io.Closer

Close() error

关闭I/O资源(如文件、网络连接)

组合接口:io包还定义了常用的组合接口,比如:

Go
// ReadWriter = Reader + Writer
type ReadWriter interface {
Reader
Writer
}

// ReadCloser = Reader + Closer
type ReadCloser interface {
Reader
Closer
}

三、io包的常用工具函数

io包提供了大量基于核心接口的通用工具函数,避免重复造轮子,高频使用的有:

1. io.Copy:数据拷贝(最常用)

Go
// Copy 将数据从src(Reader)拷贝到dst(Writer),返回拷贝的字节数和错误
func Copy(dst Writer, src Reader) (written int64, err error)

核心优势:

  • 自动处理缓冲区,无需手动循环Read/Write;
  • 会利用ReaderFrom/WriterTo接口优化性能(如果实现了的话)。

示例:拷贝文件内容

Go
package main

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

func main() {
// 打开源文件
srcFile, err := os.Open("source.txt")
if err != nil {
fmt.Println("打开源文件失败:", err)
return
}
defer srcFile.Close()

// 创建目标文件
dstFile, err := os.Create("dest.txt")
if err != nil {
fmt.Println("创建目标文件失败:", err)
return
}
defer dstFile.Close()

// 拷贝数据
written, err := io.Copy(dstFile, srcFile)
if err != nil {
fmt.Println("拷贝失败:", err)
return
}
fmt.Printf("成功拷贝 %d 字节\n", written)
}

2. io.CopyN:拷贝指定字节数

Go
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)

示例:只拷贝前100字节

Go
written, err := io.CopyN(dstFile, srcFile, 100)

3. io.ReadAll:读取全部数据到字节切片

Go
// ReadAll 读取src中的所有数据,返回字节切片和错误(Go 1.16+)
func ReadAll(r Reader) ([]byte, error)

示例:读取文件全部内容

Go
data, err := io.ReadAll(srcFile)
if err != nil {
// 处理错误
}
fmt.Println("文件内容:", string(data))

4. io.WriteString:写入字符串(简化Writer操作)

Go
// WriteString 向w写入字符串s,避免手动转换为[]byte
func WriteString(w Writer, s string) (n int, err error)

示例:

Go
io.WriteString(os.Stdout, "Hello, WriteString!\n")

5. io.Pipe:创建管道(内存中连接Reader和Writer)

Go
// Pipe 创建一个同步的管道,PipeWriter写入的数据会被PipeReader读取
func Pipe() (*PipeReader, *PipeWriter)

示例:

Go
package main

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

func main() {
r, w := io.Pipe()

// 协程1:写入数据
go func() {
defer w.Close()
io.WriteString(w, "Hello from pipe!\n")
time.Sleep(100 * time.Millisecond)
io.WriteString(w, "Second line\n")
}()

// 协程2:读取数据
data, err := io.ReadAll(r)
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println("读取到的数据:\n", string(data))
}

四、io包的底层设计思想

1. 面向接口编程(核心)

io包的设计是Go“面向接口编程”的典范:

  • 不关心数据源/目标是什么(文件、网络、内存、字符串),只关心是否实现了Reader/Writer接口;
  • 只要实现了接口,就能无缝使用io包的所有工具函数(如Copy、ReadAll);
  • 比如:strings.Reader(字符串)、bytes.Buffer(内存)、os.File(文件)、net.Conn(网络连接)都实现了Reader/Writer,可以互相替换使用。

2. 最小接口原则

io包的核心接口只有一个方法(如Read/Write),遵循“最小接口原则”:

  • 接口越小,越容易实现;
  • 组合接口(如ReadWriter)只是多个基础接口的组合,不增加新方法;
  • 比如:你只需要实现Read方法,就能让自定义类型成为Reader,无需实现其他方法。

3. 无缓冲设计

io包的原生接口(如Read/Write)是无缓冲的:

  • 每次调用Read/Write都会直接触发底层I/O操作(如系统调用);
  • 这也是为什么bufio包需要存在:在io接口之上增加缓冲区,减少底层I/O调用次数。

五、常见误区与注意事项

1. 认为Read一定会读满缓冲区

Read方法的返回值n可能小于len(p),这是合法的(比如数据源只剩少量数据),正确的读取方式是循环读取:

Go
// 正确的读取逻辑:循环读取直到EOF
func readAllData(r io.Reader) ([]byte, error) {
var buf []byte
tmp := make([]byte, 1024)
for {
n, err := r.Read(tmp)
if n > 0 {
buf = append(buf, tmp[:n]...)
}
if err == io.EOF {
return buf, nil
}
if err != nil {
return nil, err
}
}
}

2. 忽略Write的返回值n

Write可能只写入部分数据(返回n < len(p)),必须检查返回值:

Go
// 正确的写入逻辑:循环写入直到全部写入
func writeAllData(w io.Writer, data []byte) (int, error) {
total := 0
for total < len(data) {
n, err := w.Write(data[total:])
if err != nil {
return total, err
}
total += n
}
return total, nil
}

3. 忘记关闭资源

实现了io.Closer接口的类型(如os.File、net.Conn),必须调用Close()释放资源,建议用defer:

Go
file, err := os.Open("test.txt")
if err != nil {
// 处理错误
}
defer file.Close() // 确保关闭

总结

  1. io包是Go I/O编程的基础,核心是定义了Reader/Writer等通用接口,以及基于这些接口的工具函数;
  2. 核心设计思想:面向接口编程、最小接口原则、无缓冲原生I/O;
  3. 高频工具函数:io.Copy(数据拷贝)、io.ReadAll(读取全部数据)、io.WriteString(写入字符串);
  4. 关键注意点:Read不一定读满缓冲区,Write可能只写入部分数据,必须循环处理;实现Closer的资源要记得关闭。

理解io包是掌握Go所有I/O操作的基础,bufio、net、os等包的I/O功能都是基于io接口实现的,掌握了io包的核心思想,就能举一反三理解其他I/O相关包。

|(注:文档部分内容可能由 AI 生成)

附件原文:onenote-go-bufio-io_.docx(点击下载)

转换文字版(供搜索 / 复制)

主要用于优化 I/O 操作,通过使用缓冲区在内存中临时存储数据,从而减少对文件或网络连接等底层数据流的频繁操作

file, err := os.Open("example.txt")

if err != nil {

log.Fatal(err)

}

defer file.Close()

 

reader := bufio.NewReader(file)

func NewReader(rd io.Reader) *Reader

func (b *Reader) Read(p []byte) (n int, err error)

func (b *Reader) ReadByte() (c byte, err error)。

func (b *Reader) ReadRune() (r rune, size int, err error)

读取一个utf-8编码的unicode码值,返回该码值、其编码长度和可能的错误。如果utf-8编码非法,读取位置只移动1字节,返回U+FFFD,返回值size为1而err为nil。如果没有可用的数据,会返回错误。

func NewWriter(w io.Writer) *Writer

---------------------------------------------

file, err := os.Create("output.txt")

if err != nil {

log.Fatal(err)

}

defer file.Close()

 

writer := bufio.NewWriter(file)

 

 

func (b *Writer) Write(p []byte) (nn int, err error)

func (b *Writer) WriteString(s string) (int, error)

func (b *Writer) WriteByte(c byte) error

func (b *Writer) WriteRune(r rune) (size int, err error)

func (b *Writer) Flush() error

func NewScanner(r io.Reader) *Scanner

获取当前位置的token(该token可以通过Bytes或Text方法获得),并让Scanner的扫描位置移动到下一个token。

func (s *Scanner) Scan() bool

func (s *Scanner) Bytes() []byte

返回最近一次Scan调用生成的token,会申请创建一个字符串保存token并返回该字符串

func (s *Scanner) Text() string

package main

 

import (

"bufio"

"fmt"

"os"

)

 

func main() {

file, err := os.Open("example.txt")

if err != nil {

fmt.Println("Error opening file:", err)

return

}

defer file.Close()

 

scanner := bufio.NewScanner(file)

for scanner.Scan() {

line := scanner.Text() // 读取一行数据

fmt.Println(line)

}

 

if err := scanner.Err(); err != nil {

fmt.Println("Error reading file:", err)

}

}

内容反馈

发现错误或想补充内容?

GoClub Pro 是静态知识库,评论功能不再加载第三方脚本。你可以在 GitHub 仓库提交 Issue,反馈会直接进入项目协作流程。

提交 GitHub 反馈