lang

LangBook

12.文件io

是我们经常使用io处理数据的输入和输出,例如文件的读取和写入,网络的请求和响应,输出到命令行,从命令行读取输入等。

1. 经典的io接口

Go 中使用最多的接口是error, 其次就是io.Readerio.Writer

1.1 io.Reader的定义

type Reader interface {
    Read(p []byte) (n int, err error)
}

p是一个字节切片,将数据放到p中,返回读取的字节个数n和过程中发生的错误err

我们来实现一个io.Reader, 创建一个MyReader结构体:

type MyReader struct {
	i int    // 当前索引
	s string // 实际的字符串
}

创建一个New这个结构体的方法:

func NewMyReader(s string) *MyReader {
	return &MyReader{
		i: 0, // 刚开始索引为0
		s: s,
	}
}

实现Read方法:

func (r *MyReader) Read(p []byte) (n int, err error) {
	// r 中s的长度
	if r.i >= len(r.s) { // 当前索引大于等于r.s的长度,说明读完了
		return 0, io.EOF // io.EOF 是文件读完的标志, end of file
	}
	lenP := len(p)            // p切片的长度,表示能存入的byte个数
	lenR := len(r.s[r.i:])    // r.s中从当前索引开始还有多少长度的byte没读
	minLen := min(lenR, lenP) // 我们需要一个最小值,既不能超过lenR,也不能超过lenP
	for idx := range minLen {
		p[idx] = r.s[idx+r.i] // 将字节从r.s一个个复制到p
	}
	r.i += minLen // 将当前索引移动minLen步,已经读了minLen个字节了。
 
	return minLen, nil
}

你可以使用内置的copy函数把一个切片复制到另一个切片,返回复制的个数。既上述代码等价于:

func (r *MyReader) Read(p []byte) (n int, err error) {
	// r 中s的长度
	lenR := len(r.s)
	if r.i >= lenR { // 说明读完了
		return 0, io.EOF // io.EOF 是文件读完的标志, end of file
	}
	n = copy(p, r.s[r.i:]) // 从r.i开始复制到p切片
	r.i += n
 
	return n, nil
}

直接调用io.Copy它接收一个io.Writer和一个io.Reader, 把io.Reader的数据复制到io.Writer。来验证一下

func main() {
	mr := NewMyReader("hello world, my friend")
 
	// os.Stdout 实现了io.Writer接口,标准输出,即打印到命令行
	// mr 实现了io.Reader接口
	io.Copy(os.Stdout, mr)
}

运行

go run .

输出

hello world, my friend%   

完整代码:

package main
 
import "io"
 
type MyReader struct {
	i int    // 当前索引
	s string // 实际的字符串
}
 
func NewMyReader(s string) *MyReader {
	return &MyReader{
		i: 0, // 刚开始索引为0
		s: s,
	}
}
 
func (r *MyReader) Read(p []byte) (n int, err error) {
	// r 中s的长度
	lenR := len(r.s)
	if r.i >= lenR { // 说明读完了
		return 0, io.EOF // io.EOF 是文件读完的标志, end of file
	}
	n = copy(p, r.s[r.i:]) // 从r.i开始复制到p切片
	r.i += n
 
	return n, nil
}

上述的MyReader就是标准库strings.Reader的实现。

1.2 io.Writer的定义

type Writer interface {
    Write(p []byte) (n int, err error)
}

一个Write方法,它从p切片中拿出数据,返回拿出的byte的个数和过程中的错误。

我们知道os.Stdout 是标准输出,即打印到命令行,同时实现了io.Writer接口。我们来实现一个io.Writer, 将字母变大写再打印到命令行:

创建MyWriter结构体

type MyWriter struct {
	stdout io.Writer 
}

创建New方法:

func NewMyWriter(stdout io.Writer) *MyWriter {
	return &MyWriter{stdout: stdout}
}

实现Write方法,在这里将字母变大写。

func (w *MyWriter) Write(p []byte) (n int, err error) {
	p = bytes.ToUpper(p) // 标准库bytes把字母转换成大写
 
	n, err = w.stdout.Write(p) // 调用stdout打印到标准输出
	return n, err
}

我们把上一节代码中的os.Stdout替换成MyWriter试验一下:

func main() {
	mr := NewMyReader("hello world, my friend")
	mw := NewMyWriter(os.Stdout)
 
	// mw 实现了io.Writer接口
	// mr 实现了io.Reader接口
	io.Copy(mw, mr)
}

运行

go run .

输出

HELLO WORLD, MY FRIEND%

成功,完整代码:

package main
 
import "io"
 
type MyReader struct {
	i int    // 当前索引
	s string // 实际的字符串
}
 
func NewMyReader(s string) *MyReader {
	return &MyReader{
		i: 0, // 刚开始索引为0
		s: s,
	}
}
 
func (r *MyReader) Read(p []byte) (n int, err error) {
	// r 中s的长度
	lenR := len(r.s)
	if r.i >= lenR { // 说明读完了
		return 0, io.EOF // io.EOF 是文件读完的标志, end of file
	}
	n = copy(p, r.s[r.i:]) // 从r.i开始复制到p切片
	r.i += n
 
	return n, nil
}

1.3 实现io.Copy

既然都实现到这了,就再实现一个简单io.Copy吧。它将数据从io.Reader复制到io.Writer。(完整的实现还得看标准库)

copy.go
package main
 
import "io"
 
func MyCopy(dst io.Writer, src io.Reader) (writen int, err error) {
	var totalBytes int        // 总共写入了多少字节
	buf := make([]byte, 1024) // 每次读取 1KB
 
	for {
		// 从 src 中读取数据
		n, readErr := src.Read(buf)
		if n > 0 { // n 是读取到的字节数
			// 将读取的数据写入到 dst
			written, writeErr := dst.Write(buf[:n])
			if writeErr != nil { // 写入时出错,返回
				return totalBytes, writeErr
			}
			totalBytes += written // 将写入的字节数加到总字节数
		}
 
		// 如果读取遇到 EOF,表示结束
		if readErr == io.EOF {
			break // 中断for循环
		}
 
		// 如果读取出错,返回错误
		if readErr != nil {
			return totalBytes, readErr
		}
	}
 
	return totalBytes, nil
}

io.Copy替换成MyCopy

main.go
func main() {
	mr := NewMyReader("hello world, my friend")
	mw := NewMyWriter(os.Stdout)
 
	// mw 实现了io.Writer接口
	// mr 实现了io.Reader接口
	MyCopy(mw, mr)
}

运行

go run .

输出

HELLO WORLD, MY FRIEND%  

成功,

完整代码

package main
 
import "io"
 
type MyReader struct {
	i int    // 当前索引
	s string // 实际的字符串
}
 
func NewMyReader(s string) *MyReader {
	return &MyReader{
		i: 0, // 刚开始索引为0
		s: s,
	}
}
 
func (r *MyReader) Read(p []byte) (n int, err error) {
	// r 中s的长度
	lenR := len(r.s)
	if r.i >= lenR { // 说明读完了
		return 0, io.EOF // io.EOF 是文件读完的标志, end of file
	}
	n = copy(p, r.s[r.i:]) // 从r.i开始复制到p切片
	r.i += n
 
	return n, nil
}

2. 文件操作

2.1 创建和写入文件

main.go
package main
 
import (
	"fmt"
	"os"
)
 
func main() {
	file, err := os.Create("hello.md") // 在当前目录创建`hello.md`的文件
	if err != nil { // 创建发生错误直接退出
		fmt.Println("错误", err)
		return
	}
 
	defer file.Close() // 一定记得defer 关闭文件,释放资源
 
	// file 实现了io.Writer, 有write方法, 直接写入
	file.Write([]byte("hello world"))
}

运行,可以看到生成了hello.md文件,内容为

hello world

注意事项,file不是线程安全的,并发访问时需要加锁控制,例如:

func WriteToFile(file io.Writer, data []byte) (n int, err error) {
	mu.Lock() // 加锁保护
	n, err = file.Write(data)
	mu.Unlock() // 解锁
	return n, err
}

2.2 读取文件

我们来读取刚刚创建的hello.md文件

package main
 
import (
	"io"
	"os"
)
 
func main() {
	// 打开该文件
	file, err := os.Open("hello.md")
	if err != nil { // 打开时发生错误
		panic(err) // 不想处理了,直接panic终止程序,生产环境勿用
	}
 
	defer file.Close() // 一定记得defer 关闭文件,释放资源
 
	io.Copy(os.Stdout, file) // 将文件打印到命令行 stdout
}

输出

hello world%

2.3 读取大文件

如果一个文件很大,一次性读取可能会把内存挤爆,我们需要分快读取:

package main
 
import (
	"io"
	"os"
)
 
func main() {
	// 打开该文件, 假设这个文件很大
	file, err := os.Open("hello.md")
	if err != nil { // 打开时发生错误
		panic(err) // 不想处理了,直接panic终止程序,生产环境勿用
	}
 
	defer file.Close() // 一定记得defer 关闭文件,释放资源
 
	buf := make([]byte, 4096) // 每次最多读取4k个byte,缓冲
 
	for {
		n, err := file.Read(buf) // file实现了io.Reader, 有Read方法,读取数据到buf切片中
		if err == io.EOF {       // 读完了,没什么可读的了
			break // 退出for循环
		}
 
		if err != nil { // 如果err还不为nil, 说明读取过程中发生错误
			panic(err) // 直接panic,终止程序,生成环境勿用,返回错误即可
		}
 
		os.Stdout.Write(buf[:n]) // 将读到的数据打印到命令行。
	}
}

输出

hello world% 

3. 总结

  1. 介绍了两个经典的接口io.Readerio.Writer
  2. 如何创建文件,读取文件,写入文件。

On this page