一文带你完整了解Go语言IO基础库
全文9792字,预计阅读时间10分钟。
GEEK TALK01
IO库首先来看一下golang最基础的IO库,包名为"io", 它包括两大部分功能:第一部分定义了最基本的流操作接口,包括Writer, Reader, Seeker, Closer这几个以及相关的组合接口。分别表达写入,读取, 偏移读和关闭操作处理。全局类图以及关系如下,方便大家更直观的理解:
以下对接口进行了源码摘取并进行中文注释:
/*Reader 是包装基本 Read 方法的接口。Read 将最多 len(p) 个字节读取到 p 中。它返回读取的字节数 (0 <= n <= len(p)) 以及遇到的任何错误。即使 Read 返回 n < len(p),它也可能在调用期间使用所有 p 作为暂存空间。如果某些数据可用,但不是 len(p) 个字节,则 Read 通常会返回可用数据,而不是等待更多数据。当 Read 在成功读取 n > 0 字节后遇到错误或文件结束条件时,它返回读取的字节数。它可能会从同一调用返回(非零)错误,或从后续调用返回错误(且 n == 0)。这种一般情况的一个实例是,在输入流末尾返回非零字节数的 Reader 可能返回 err == EOF 或 err == nil。下一次读取应返回 0、EOF。在考虑错误 err 之前,调用者应始终处理返回的 n > 0 字节。这样做可以正确处理读取一些字节后发生的 I/O 错误以及允许的 EOF 行为。如果 len(p) == 0,Read 应始终返回 n == 0。如果已知某些错误条件(例如 EOF),则可能会返回非零错误。不鼓励 Read 的实现返回带有 nil 错误的零字节计数,除非 len(p) == 0 时。调用者应将返回 0 和 nil 视为表示没有发生任何事情;特别是它不指示 EOF。*/type Writer interface { Write(p []byte) (n int, err error)}/*Writer 是包装基本 Write 方法的接口。Write 将 p 中的 len(p) 个字节写入底层数据流。它返回从 p (0 <= n <= len(p)) 写入的字节数以及遇到的导致写入提前停止的任何错误。如果 Write 返回 n < len(p),则必须返回非零错误。写入不得修改切片数据,即使是暂时的。*/type Reader interface { Read(p []byte) (n int, err error)}/*Seeker 是包装基本 Seek 方法的接口。Seek 将下一次读取或写入的偏移量设置为 offset,根据从何处解释:SeekStart表示相对于文件开头, SeekCurrent表示相对于当前偏移量, SeekEnd表示相对于结尾(例如,offset = - 2 指定文件的倒数第二个字节)。Seek 返回相对于文件开头的新偏移量或错误(如果有)。寻找文件开始之前的偏移量是错误的。可以允许寻求任何正偏移量,但如果新偏移量超过底层对象的大小,则后续 I/O 操作的行为取决于实现。*/type Seeker interface { Seek(offset int64, whence int) (int64, error)}/*Closer 是包装基本 Close 方法的接口。第一次调用后 Close 的行为未定义。具体的实现可能会记录它们自己的行为。*/type Closer interface { Close() error}在基本的接口外, io库还提供了一些扩展的读写处理能力的接口定义,以提升更便捷的使用:读相关的定义包括:ReaderAt, RuneReader, RuneScanner, ByteReader, ByteScanner, ReaderFrom全局类图以及关系如下, 方便大家更直观的理解:
以下对接口进行了源码摘取并进行中文注释:
/*ReaderAt 是包装基本 ReadAt 方法的接口。ReadAt 从底层输入源中的偏移量 off 处开始将 len(p) 个字节读取到 p 中。它返回读取的字节数 (0 <= n <= len(p)) 以及遇到的任何错误。当 ReadAt 返回 n < len(p) 时,它返回一个非零错误,解释为什么没有返回更多字节。在这方面,ReadAt比Read更严格。即使 ReadAt 返回 n < len(p),它也可能在调用期间使用所有 p 作为暂存空间。如果某些数据可用但不是 len(p) 个字节,则 ReadAt 会阻塞,直到所有数据可用或发生错误。在这方面,ReadAt 与 Read 不同。如果 ReadAt 返回的 n = len(p) 字节位于输入源的末尾,则 ReadAt 可能返回 err == EOF 或 err == nil。如果 ReadAt 正在从具有寻道偏移的输入源读取,则 ReadAt 不应影响底层寻道偏移,也不会受其影响。ReadAt 的客户端可以在同一输入源上执行并行 ReadAt 调用。*/type ReaderAt interface { ReadAt(p []byte, off int64) (n int, err error)}/*RuneReader 是包装 ReadRune 方法的接口。ReadRune 读取单个编码的 Unicode 字符并返回符文及其大小(以字节为单位)。如果没有可用的字符,则会设置 err。*/type RuneReader interface { ReadRune() (r rune, size int, err error)}/*ByteReader 是包装 ReadByte 方法的接口。ReadByte 读取并返回输入中的下一个字节或遇到的任何错误。如果 ReadByte 返回错误,则表示没有消耗输入字节,并且返回的字节值未定义。ReadByte 为逐字节处理提供了高效的接口。未实现 ByteReader 的 Reader可以使用 bufio.NewReader 进行包装以添加此方法。*/type ByteReader interface { ReadByte() (byte, error)}/*ReaderFrom 是包装 ReadFrom 方法的接口。ReadFrom 从 r 读取数据,直到 EOF 或出现错误。返回值n是读取的字节数。读取期间遇到的除 EOF 之外的任何错误也会返回。Copy函数使用ReaderFrom (如果可用)。*/type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error)}写相关的定义包括:WriterAt, WriterTo, StringWriter等全局类图以及关系如下, 方便大家更直观的理解:
Copy:
func Copy(dst Writer, src Reader) (written int64, err error)△注:将副本从 src 复制到 dst,直到 src 达到 EOF 或发生错误。它返回复制的字节数以及复制时遇到的第一个错误(如果有)。
成功的 Copy 返回 err == nil,而不是 err == EOF。因为 Copy 被定义为从 src 读取直到 EOF,所以它不会将 Read 中的 EOF 视为要报告的错误。
如果 src 实现WriterTo,则通过调用 src.WriteTo(dst) 实现复制。否则,如果 dst 实现了ReaderFrom,则通过调用 dst.ReadFrom(src) 来实现复制。
CopyBuffer:func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)△注:CopyBuffer 与 Copy 相同,只是它分阶段遍历提供的缓冲区(如果需要)而不是分配临时缓冲区。如果 buf 为 nil,则分配 1;否则,如果它的长度为零,CopyBuffer 就会出现混乱。
如果 src 实现WriterTo或 dst 实现ReaderFrom,则 buf 将不会用于执行复制。
CopyN:func CopyN(dst Writer, src Reader, n int64) (written int64, err error)△注:CopyN 将 n 个字节(或直到出现错误)从 src 复制到 dst。它返回复制的字节数以及复制时遇到的最早错误。返回时,当且仅当 err == nil 时写为 == n。
如果 dst 实现ReaderFrom,则使用它来实现副本。Pipe:func Pipe() (*PipeReader, *PipeWriter)△注:Pipe 创建同步内存管道。它可用于将需要io.Reader的代码 与需要io.Writer 的代码连接起来。
管道上的读取和写入是一对一匹配的,除非需要多个读取来消耗单个写入。也就是说,对 PipeWriter 的每次写入都会阻塞,直到满足来自PipeReader的一次或多次读取(完全消耗写入数据)为止。数据直接从Write复制到对应的Read(或Reads);没有内部缓冲。
并行调用 Read 和 Write 或与 Close 一起调用是安全的。对 Read 的并行调用和对 Write 的并行调用也是安全的:各个调用将按顺序进行门控。
ReadAll:func ReadAll(r Reader) ([]byte, error)△注:ReadAll 从 r 读取直到出现错误或 EOF,然后返回读取的数据。成功地调用返回 err == nil,而不是 err == EOF。因为 ReadAll 被定义为从 src 读取直到 EOF,所以它不会将 Read 中的 EOF 视为要报告的错误。
ReadAtLeast:
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)△注:ReadAtLeast 从 r 读取到 buf 中,直到读取至少 min 个字节。它返回复制的字节数,如果读取的字节数较少,则返回错误。仅当未读取任何字节时,错误才为 EOF。如果读取少于 min 字节后发生 EOF,则 ReadAtLeast 返回ErrUnexpectedEOF。如果 min 大于 buf 的长度,则 ReadAtLeast 返回ErrShortBuffer。返回时,n >= min 当且仅当 err == nil 时。如果 r 在读取了至少 min 个字节后返回错误,则该错误将被丢弃。
ReadFull:func ReadFull(r Reader, buf []byte) (n int, err error)△注:ReadFull 将 r 中的 len(buf) 个字节准确读取到 buf 中。它返回复制的字节数,如果读取的字节数较少,则返回错误。仅当未读取任何字节时,错误才为 EOF。如果在读取部分字节但不是全部字节后发生 EOF,则 ReadFull 返回ErrUnexpectedEOF。返回时,n == len(buf) 当且仅当 err == nil 时。如果 r 在读取至少 len(buf) 个字节后返回错误,则该错误将被丢弃。
WriteString:func WriteString(w Writer , s string ) (n int , err error)△注:WriteString 将字符串 s 的内容写入 w,它接受字节切片。如果 w 实现StringWriter,则直接调用 [StringWriter.WriteString] 。否则,[Writer.Write] 只会被调用一次。
文件操作读写示例:// ReadFileExample 读取文件内容并输出func ReadFileExample() { // 打开文件,第一个参数是文件路径,第二个参数是文件打开模式 file, err := os.Open("example.txt") if err != nil { fmt.Println("Error:", err) return } defer file.Close() // 延迟关闭文件,确保文件在函数执行完毕后被关闭 // 读取文件内容 data := make([]byte, 100) // 读取数据的缓冲区 count, err := file.Read(data) if err != nil { fmt.Println("Error:", err) return } // 输出文件内容 fmt.Printf("Read %d bytes: %s\n", count, data[:count])}// WriteFileExample 函数演示如何写入数据到文件中func WriteFileExample() { // 创建文件,第一个参数是文件路径,如果文件已存在则会被截断清空 file, err := os.Create("example.txt") if err != nil { fmt.Println("Error:", err) return } defer file.Close() // 延迟关闭文件,确保文件在函数执行完毕后被关闭 // 写入数据到文件 data := []byte("Hello, world!\n") _, err = file.Write(data) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Data has been written to output.txt")}io库的第二部分,定义了一个子包"fs", 定义了文件操作相关的接口,包括 File, FS, DirEntry等。全局类图以及关系如下, 方便大家更直观的理解:
02
OS库至此io库的部分已经介绍结束,但应该有同学会问, 如何使用这些库,特别是文件操作?那就要是与os库联合使用了。 下面也针对os库进行了整理,并给出了相关的示例,方便大家掌握。全局类图以及关系如下,方便大家更直观的理解:
03
http包接下来再来整理一下 http包下的文件相关的定义:
04
embed包最后再介绍一下 embed包。Golang1.16 版本引入的embed标准库, 支持把外部资源文件或目录直接在编译阶段打进编译包中,实现了应用打包时只需一个可执行包的效果。embed支持把外部资源以 string, []byte或embed.FS方式使用。下面是几个使用示例://go:embed hello.txtvar s string//go:embed hello.txtvar b []byte//go:embed hello.txtvar f embed.FS这里可以看到 embed也定义了 FS对象,用于FileSystem的操作处理。
示例代码:从embed.FS读取文件目录,发布成http静态资源服务
package mainimport ( "embed" "log" "net/http")//go:embed static/*var staticFiles embed.FSfunc main() { // 创建文件服务器 fileServer := http.FileServer(http.FS(staticFiles)) // 设置路由 http.Handle("/static/", http.StripPrefix("/static/", fileServer)) // 启动HTTP服务器 log.Println("Server started on: http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil))}GEEK TALK05
总结Go语言的基础库里,针对文件操作这一块,各个包都有自己的File, FS的定义,这给很多刚开始学习的同学带来了不少困惑,个人也是觉得设计上是有改进的空间的。希望上述的整理内容,可以帮助到大家更好的理解Go语言IO库的使用。END
参考资料:
[1]. https://pkg.go.dev/io@go1.22.1 官方IO类库
[2]. https://pkg.go.dev/embed@go1.22.1 官方Embed类库
[3]. https://github.com/jhunters/goassist goassist 开源go扩展库,提供了非常多的工具方法封装
扫一扫,关注我们