Go 文件读写操作 | 青训营笔记

117 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

Created: January 15, 2023 9:21 PM Tags: golang

文件读写操作

Go 和 C 一样提供了多种文件操作的方式,我们可以对这些方式做一个简单的梳理,以便于在不同场景下选择合适的文件操作方式。

文件的打开和关闭

首先和 C 语言一样,Go 语言中操作文件也是通过一个 FILE 结构体:

type file struct {
	pfd     poll.FD
	name    string
	dirinfo *dirInfo 
}
type File struct {
	*file // os specific
}

func Open

func Open(name string) (*File, error)

Open 函数用于打开一个指定的文件,如果读取成功将返回对应的文件对象,如果读取失败将返回 *PathError 错误类型。

func OpenFile

func OpenFile(name string, flag int, perm FileMode) (*File, error)

OpenFile 函数可以使用指定的选项与指定的模式来打开文件,同样如何文件打开成功,返回一个可用于 IO 的文件对象,否则返回一个 *PathError 类型的错误。

OpenFile 函数接受一个字符串类型的文件名参数,同时还接受一个 int 类型的文件打开方式参数和一个打开模式参数,打开模式使用的是 Linux 中的文件模式。

打开方式参数 flag:

打开方式说明
O_RDONLY只读方式打开
O_WRONLY只写方式打开
O_RDWR读写方式打开
O_APPEND追加方式打开
O_CREATE不存在,则创建
O_EXCL如果文件存在,且标定了O_CREATE的话,则产生一个错误
O_TRUNG如果文件存在,且它成功地被打开为只写或读写方式,将其长度裁剪唯一。(覆盖)
O_NOCTTY如果文件名代表一个终端设备,则不把该设备设为调用进程的控制设备
O_NONBLOCK如果文件名代表一个FIFO,或一个块设备,字符设备文件,则在以后的文件及I/O操作中置为非阻塞模式。
O_SYNC当进行一系列写操作时,每次都要等待上次的I/O操作完成再进行。

打开模式参数 perm:

Unix 使用 -rwxrwxrwx 这样的形式来表示文件权限,其中:

  • 第1位:文件属性,`` 表示是普通文件,d 表示是一个目录
  • 第2-4位:文件所有者的权限
  • 第5-7位:文件所属用户组的权限
  • 第8-10位:其他人的权限
打开模式文件权限说明
0777-rwxrwxrwx创建了一个普通文件,所有人拥有所有的读、写、执行权限
0666-rw-rw-rw-创建了一个普通文件,所有人拥有对该文件的读、写权限,但是都不可执行
0644-rw-r--r--创建了一个普通文件,文件所有者对该文件有读写权限,用户组和其他人只有读权限,没有执行权限

示例

package main
import (
	"fmt"
	"os"
)

func main() {
	// 1.打开一个文件
	// 注意: 文件不存在不会创建, 会报错
	// 注意: 通过Open打开只能读取, 不能写入
	fp, err := os.Open("test.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.关闭一个文件
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()
}

文件读取

func Read(不带缓冲区去读)

func (f *File) Read(b []byte) (n int, err error)

Read 方法从 f 中读取最多 len(b) 字节数据并写入b

package main

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

func main() {
	// 1.打开一个文件
	// 注意: 文件不存在不会创建, 会报错
	// 注意: 通过Open打开只能读取, 不能写入
	fp, err := os.Open("test.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.关闭一个文件
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 3.读取指定指定字节个数据
	// 注意点: \n也会被读取进来
	//buf := make([]byte, 50)
	//count, err := fp.Read(buf)
	//if err != nil {
	//	fmt.Println(err)
	//}else{
	//	fmt.Println(count)
	//	fmt.Println(string(buf))
	//}

	// 4.读取文件中所有内容, 直到文件末尾为止
	buf := make([]byte, 10)
	for{
		count, err := fp.Read(buf)
		// 注意: 这行代码要放到判断EOF之前, 否则会出现少读一行情况
		fmt.Print(string(buf[:count]))
		if err == io.EOF {
			break
		}
	}
}

func ReadBytes 和 func ReadString(带缓冲区去读)

func (b *Reader) ReadBytes(delim byte) (line []byte, err error)

• ReadBytes 读取直到第一次遇到 delim 字节

func (b *Reader) ReadString(delim byte) (line string, err error)

• ReadString 读取直到第一次遇到 delim 字节

package main

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

func main() {
	// 1.打开一个文件
	// 注意: 文件不存在不会创建, 会报错
	// 注意: 通过Open打开只能读取, 不能写入
	fp, err := os.Open("test.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.关闭一个文件
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 3.读取一行数据
	// 创建读取缓冲区, 默认大小4096
	//r :=bufio.NewReader(fp)
	//buf, err := r.ReadBytes('\n')
	//buf, err := r.ReadString('\n')
	//if err != nil{
	//	fmt.Println(err)
	//}else{
	//	fmt.Println(string(buf))
	//}

	// 4.读取文件中所有内容, 直到文件末尾为止
	r :=bufio.NewReader(fp)
	for{
		//buf, err := r.ReadBytes('\n')
		buf, err := r.ReadString('\n')
		fmt.Print(string(buf))
		if err == io.EOF{
			break
		}
	}
}

bufio.NewReader() 函数返回的是一个 Reader 对象,Reader 对象可以对数据 I/O 接口 io.Reader 进行输入缓冲操作,Reader 结构定义如下:

type Reader struct {
    //contains filtered or unexported fields
)

默认情况下 Reader 对象没有定义初始值,输入缓冲区最小值为 16。当超出限制时,另创建一个二倍的存储空间。

func ReadFile

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

• 从 filename 指定的文件中读取数据并返回文件的所有内容。ioutil.ReadFile(filename string) 方法能够将文件所有字节读取出来,省去了使用字节缓存循环读取的过程。但要注意该方法不适合大文件读取。

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {

	filePath := "test.txt"
	buf, err := ioutil.ReadFile(filePath)
	if err !=nil {
		fmt.Println(err)
	}else{
		fmt.Println(string(buf))
	}
}

func Scanner

有时候为了便于分析处理,我们希望能够逐行读取文件内容,这个时候可以 Scanner 来完成:

// read-line-by-line.go

package main

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

func main() {

	fp, err := os.Open("test.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}
	
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

  // 接受io.Reader类型参数 返回一个bufio.Scanner实例
  scanner := bufio.NewScanner(fp)

  var count int

  for scanner.Scan() {
		count++
        
    // 读取当前行内容
    line := scanner.Text()

    fmt.Printf("%d %s\n", count, line)
  }
}

上面代码直接打印出了每一行的数据,如果大家想得到最终文件的内容,可以创建一个字符串切片,每次逐行扫描时,将当前行内容追加到切片中即可。

文件创建和写入

func Create

func Create(name string) (file *File, err error)

Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件存在会覆盖原有文件。

func Write

func (f *File) Write(b []byte) (n int, err error)

将指定字节数组写入到文件中

func WriteString

func (f *File) WriteString(s string) (ret int, err error)

将指定字符串写入的文件中

在打开文件之后,我们可以通过 Write() 和 WriteString() 方法写入数据,最后通过 Sync() 方法将数据持久化到磁盘:

package main

import (
	"fmt"
	"os"
)

func main() {

	// 注意点: 第三个参数在Windows没有效果
	// -rw-rw-rw- (666)   所有用户都有文件读、写权限。
	// -rwxrwxrwx (777)  所有用户都有读、写、执行权限。
	// fp, err := os.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0666)
	fp, err := os.OpenFile("test.txt", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println(err)
	}
	defer func() {
		err := fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()
	// 注意点:
	// 如果O_RDWR模式打开, 被打开文件已经有内容, 会从最前面开始覆盖
	// 如果O_APPEND模式打开, 被打开文件已经有内容, 会从在最后追加
	bytes := []byte{'h','e','l','l','o','\r','\n'}
	fp.Write(bytes)
	fp.WriteString("hello\r\n")
	file.Sync()
}

除了上述这种不带缓冲区的写入方式,还有利用 bufio 的 Writer 对象实现带缓冲区的写入方式:

package main

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

func main() {

	fp, err := os.OpenFile("test.txt", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println(err)
	}
	defer func() {
		err := fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 创建缓冲区
	w := bufio.NewWriter(fp)

	// 4.写入数据到缓冲区
	bytes := []byte{'h','e','l','l','o','\r','\n'}
	w.Write(bytes)
	w.WriteString("hello\r\n")

	// 将缓冲区中的数据刷新到文件
	w.Flush()
}