Go 标准库学习——io相关

一般的,计算机程序是:输入 (Input) 经过算法处理产生输出 (Output)。各种语言一般都会提供IO库供开发者使用。Go语言也不例外。

Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中:

  • io 为 IO 原语(I/O primitives)提供基本的接口
  • io/ioutil 封装一些实用的 I/O 函数
  • fmt 实现格式化 I/O,类似 C 语言中的 printf 和 scanf
  • bufio 实现带缓冲I/O

介绍这些 IO 包提供的函数、类型和方法,同时通过实例讲解这些包的使用方法。

io — 基本的io接口

Reader接口

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

官方文档中关于该接口方法的说明:

Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p)) 以及任何遇到的错误。即使 Read 返回的 n < len(p),它也会在调用过程中占用 len(p) 个字节作为暂存空间。若可读取的数据不到 len(p) 个字节,Read 会返回可用数据,而不是等待更多数据。

当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF (end-of-file),它会返回读取的字节数。它可能会同时在本次的调用中返回一个non-nil错误,或在下一次的调用中返回这个错误(且 n 为 0)。 一般情况下, Reader会返回一个非0字节数n, 若 n = len(p) 个字节从输入源的结尾处由 Read 返回,Read可能返回 err == EOF 或者 err == nil。并且之后的 Read() 都应该返回 (n:0, err:EOF)。

调用者在考虑错误之前应当首先处理返回的数据。这样做可以正确地处理在读取一些字节后产生的 I/O 错误,同时允许EOF的出现。

该接口下的Read函数需要传入一个字节数组,读取完成之后就会返回读取的字节数和nil,该接口函数返回的err不是nil的时候可能是EOF。

io.EOF 变量的定义:var EOF = errors.New("EOF"),是 error 类型。根据 reader 接口的说明,在 n > 0 且数据被读完了的情况下,返回的 error 有可能是 EOF 也有可能是 nil。

根据 Go 语言中关于接口和满足了接口的类型的定义(Interface_types),我们知道 Reader 接口的方法集(Method_sets)只包含一个 Read 方法,因此,所有实现了 Read 方法的类型都满足 io.Reader 接口,也就是说,在所有需要 io.Reader 的地方,可以传递实现了 Read() 方法的类型的实例。
下面,我们通过具体例子来谈谈该接口的用法。

1
2
3
4
5
6
7
8
func ReadFrom(reader io.Reader, num int) ([]byte, error) {
p := make([]byte, num)
n, err := reader.Read(p)
if n > 0 {
return p[:n], nil
}
return p, err
}

ReadFrom 函数将 io.Reader 作为参数,也就是说,ReadFrom 可以从任意的地方读取数据,只要来源实现了 io.Reader 接口。比如,我们可以从标准输入、文件、字符串等读取数据,示例代码如下:

1
2
3
4
5
6
7
8
// 从标准输入读取
data, err = ReadFrom(os.Stdin, 11)

// 从普通文件读取,其中 file 是 os.File 的实例
data, err = ReadFrom(file, 9)

// 从字符串读取
data, err = ReadFrom(strings.NewReader("from string"), 12)

Writer接口

Writer 接口的定义如下:

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

官方文档中关于该接口方法的说明:

Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的 n < len(p),它就必须返回一个 非nil 的错误。

同样的,所有实现了Write方法的类型都实现了 io.Writer 接口。

ReaderAt接口

ReaderAt 接口的定义如下:

1
2
3
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}

ReadAt 从基本输入源的偏移量 off 处开始,将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误。

当 ReadAt 返回的 n < len(p) 时,它就会返回一个 非nil 的错误来解释 为什么没有返回更多的字节。在这一点上,ReadAt 比 Read 更严格。

即使 ReadAt 返回的 n < len(p),它也会在调用过程中使用 p 的全部作为暂存空间。若可读取的数据不到 len(p) 字节,ReadAt 就会阻塞,直到所有数据都可用或一个错误发生。 在这一点上 ReadAt 不同于 Read。

若 n = len(p) 个字节从输入源的结尾处由 ReadAt 返回,Read可能返回 err == EOF 或者 err == nil

若 ReadAt 携带一个偏移量从输入源读取,ReadAt 应当既不影响偏移量也不被它所影响。

可对相同的输入源并行执行 ReadAt 调用。

ReaderAt 接口使得可以从指定偏移量处开始读取数据。简单示例代码如下:

1
2
3
4
5
6
7
8
9
10
reader := strings.NewReader("Go标准库学习")
p := make([]byte, 6)
n, err := reader.ReadAt(p, 2)
if err != nil {
panic(err)
}
fmt.Printf("%s, %d\n", p, n)

// 输出
标准, 6

以上实例化了6个字节大小的byte数组,调用函数ReadAt函数,传入数组以及偏移量2,又因为Un
icode中一个汉字字符占3个字节,所以就读取2个汉字,即标准;

ReaderFrom接口

ReaderFrom 的定义如下:

1
2
3
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}

ReadFrom 从 r 中读取数据,直到 EOF 或发生错误。其返回值 n 为读取的字节数。除 io.EOF 之外,在读取过程中遇到的任何错误也将被返回。

下面的例子简单的实现将文件中的数据全部读取(显示在标准输出):

1
2
3
4
5
6
7
8
file, err := os.Open("writeAt.txt")
if err != nil {
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()

打开 writeAt.txt 文件,发挥 fileInfo 类型对象,该结构体已经实现了Reader和Writer接口;

bufio.NewWriter 函数需要传入一个实现了Writer接口的参数,这里传入了os包下的标准输出;该函数返回一个Writer指针;

该指针调用ReadFrom函数,传入file文件,表示从文件里面读取数据,调用Flush函数刷新页面。

当然,我们可以通过 ioutil 包的 ReadFile 函数获取文件全部内容。其实,跟踪一下 ioutil.ReadFile 的源码,会发现其实也是通过 ReadFrom 方法实现(用的是 bytes.Buffer,它实现了 ReaderFrom 接口)。

ioutils—方便的io操作函数

虽然 io 包提供了不少类型、方法和函数,但有时候使用起来不是那么方便。比如读取一个文件中的所有内容。为此,标准库中提供了一些常用、方便的IO操作函数。

ReadAll函数

很多时候,需要一次性读取 io.Reader 中的数据,考虑到读取所有数据的需求比较多,Go 提供了 ReadAll 这个函数,用来从io.Reader 中一次读取所有数据。

1
func ReadAll(r io.Reader) ([]byte, error)

阅读该函数的源码发现,它是通过 bytes.Buffer 中的 ReadFrom 来实现读取所有数据的。该函数成功调用后会返回 err == nil 而不是 err == EOF。(成功读取完毕应该为 err == io.EOF,这里返回 nil 由于该函数成功期望 err == io.EOF,符合无错误不处理的理念)

ReadDir函数

ioutil 中提供了一个方便的函数:ReadDir,它读取目录并返回排好序的文件和子目录名
下面是一个读取目录并根据层级返回文件信息,提供一个类似于 tree 命令的效果展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 func main() {
// 这里传0,是为了在遍历目录下的文件时添加制表符,这样可以一眼看出这个文件是属于哪一个目录的
listAll("/home/yunzhou/note/study_go",0)
}

func listAll(path string, curHier int){
fileInfos, err := ioutil.ReadDir(path)
if err != nil{
fmt.Println(err);
return
}

for _, info := range fileInfos{
if info.IsDir(){
for tmpHier := curHier; tmpHier > 0; tmpHier--{
fmt.Printf("|\t")
}
fmt.Println(info.Name(),"\\")
listAll(path + "/" + info.Name(),curHier + 1)
}else{
for tmpHier := curHier; tmpHier > 0; tmpHier--{
fmt.Printf("|\t")
}
fmt.Println(info.Name())
}
}
}

// 输出
.idea \
| .gitignore
| modules.xml
| study_go.iml
| workspace.xml
01_standardLibrary \
| inout \
| | io.go
go.mod
main.go

ReadFile 和 WriteFile 函数

ReadFile 读取整个文件的内容,在上一节我们自己实现了一个函数读取文件整个内容,由于这种需求很常见,因此 Go 提供了 ReadFile 函数,方便使用。ReadFile 的实现和ReadAll 类似,不过,ReadFile 会先判断文件的大小,给 bytes.Buffer 一个预定义容量,避免额外分配内存。

ReadFile 函数的签名如下:

1
func ReadFile(filename string) ([]byte, error)

ReadFile 从 filename 指定的文件中读取数据并返回文件的内容。成功的调用返回的err 为 nil 而非 EOF。因为本函数定义为读取整个文件,它不会将读取返回的 EOF 视为应报告的错误。(同 ReadAll )

WriteFile 函数的签名如下:

1
func WriteFile(filename string, data []byte, perm os.FileMode) error

WriteFile 将data写入filename文件中,当文件不存在时会根据perm指定的权限进行创建一个,文件存在时会先清空文件内容。对于 perm 参数,我们一般可以指定为:0666,具体含义 os 包中讲解。

fmt — 格式化

fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf. 格式“占位符”衍生自C,但比C更简单。

fmt 包的官方文档对 Printing 和 Scanning 有很详细的说明。这里就直接引用文档进行说明,同时附上额外的说明或例子,之后再介绍具体的函数使用。

fmt.Printf函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type user struct {
name string
}

func main() {
u := user{"tang"}
//Printf 格式化输出
fmt.Printf("% + v\n", u) //格式化输出结构
fmt.Printf("%#v\n", u) //输出值的 Go 语言表示方法
fmt.Printf("%T\n", u) //输出值的类型的 Go 语言表示
fmt.Printf("%t\n", true) //输出值的 true 或 false
fmt.Printf("%b\n", 1024) //二进制表示
fmt.Printf("%c\n", 11111111) //数值对应的 Unicode 编码字符
fmt.Printf("%d\n", 10) //十进制表示
fmt.Printf("%o\n", 8) //八进制表示
fmt.Printf("%q\n", 22) //转化为十六进制并附上单引号
fmt.Printf("%x\n", 1223) //十六进制表示,用a-f表示
fmt.Printf("%X\n", 1223) //十六进制表示,用A-F表示
fmt.Printf("%U\n", 1233) //Unicode表示
fmt.Printf("%b\n", 12.34) //无小数部分,两位指数的科学计数法6946802425218990p-49
fmt.Printf("%e\n", 12.345) //科学计数法,e表示
fmt.Printf("%E\n", 12.34455) //科学计数法,E表示
fmt.Printf("%f\n", 12.3456) //有小数部分,无指数部分
fmt.Printf("%g\n", 12.3456) //根据实际情况采用%e或%f输出
fmt.Printf("%G\n", 12.3456) //根据实际情况采用%E或%f输出
fmt.Printf("%s\n", "wqdew") //直接输出字符串或者[]byte
fmt.Printf("%q\n", "dedede") //双引号括起来的字符串
fmt.Printf("%x\n", "abczxc") //每个字节用两字节十六进制表示,a-f表示
fmt.Printf("%X\n", "asdzxc") //每个字节用两字节十六进制表示,A-F表示
fmt.Printf("%p\n", 0x123) //0x开头的十六进制数表示
}

Go 标准库学习——io相关
https://sunshinechen.cn/2022/02/21/Go 标准库学习:IO相关库/
作者
sunshine
发布于
2022年2月21日
许可协议