Golang基础笔记十四之文件操作

本文首发于公众号:Hunter后端

原文链接:Golang基础笔记十四之文件操作

这一篇笔记介绍 Golang 里文件的相关操作,主要用的库是 io

以下是本篇笔记目录:

  1. 文件读取
  2. 文件写入
  3. 文件追加
  4. os.OpenFile()
  5. 文件属性

1、文件读取

1. 文件的打开与关闭

1) os.Open()

打开一个文件可以使用 os.Open() 函数,其代码示例如下:

go 复制代码
filePath := "a.txt"
file, err := os.Open(filePath)

我们可以通过判断 err 是否等于 nil 来决定是否可以接着读取文件,假设这里的 filePath 不存在,那么 err 则的信息则会是:

go 复制代码
open file fail, err: open a.txt: The system cannot find the file specified.

file 的具体读取操作在后面再介绍。

2) file.Close()

在打开文件后,我们可以使用 defer file.Close() 操作来确保文件最后会被正常关闭。

2. 文件内容的读取

在文件打开以后,介绍几种对文件进行读取的方式,以下是打开文件的代码:

go 复制代码
filePath := "a.txt"
file, err := os.Open(filePath)
if err != nil {
    fmt.Println("open file fail, err:", err)
    return
}
defer file.Close()

假设 a.txt 文件内容为:

go 复制代码
第一行
second_line
end of line
1) 一次性读取

如果目标文件不大,希望一次性读取文件内容的话,可以使用 io.ReadAll() 函数:

go 复制代码
data, err := io.ReadAll(file)
if err != nil {
    fmt.Println("read file error: ", err)
    return
}
fmt.Println("read file data: ", string(data))
return

返回的结果是一个 []byte 类型,可以使用 string() 将其转换为字符串。

2) 分块读取

如果文件过大,我们可以分块读取,每次读取指定字节数的数据,下面提供一个示例,用于分批次读取文件内容,直到读完整个文件:

go 复制代码
data := make([]byte, 6)
for {
    count, err := file.Read(data)
    if err == io.EOF {
        fmt.Println("end of file, exit")
        break
    }
    if err != nil {
        fmt.Println("Error: ", err)
        break
    }
    if count > 0 {
        fmt.Println("read count: ", count, ", data: ", string(data[:count]))
    }
}

这里我们定义了一个长度为 6 的 byte 数组,然后在 for 循环里一直使用 file.Read() 读取,每次都往 data 中填充数据,直到读取到文件末尾,或者读取出现 error。

在这里需要注意,Go 里对读取文件到末尾的信息包装成了一个 error,我们需要进行判断下。

file.Seek()

在读取文件内容的时候,我们还可以指定指针读取的位置,比如重置读取的指针到开头,我们可以如下操作:

go 复制代码
file.Seek(0, io.SeekStart)

file.Seek() 函数接收两个参数,一个是偏移量,一个是起始位置,上面这行代码的含义就是从文件开头的偏移量为 0 的位置开始读取文件内容。

如果我们想从文件开头往后三个字节长度的地方开始读取,可以如下操作:

go 复制代码
file.Seek(3, io.SeekStart)

而指定读取有三个参数:

go 复制代码
const (
    SeekStart   = 0 // seek relative to the origin of the file
    SeekCurrent = 1 // seek relative to the current offset
    SeekEnd     = 2 // seek relative to the end
)

分别表示文件开头,指针当前位置和文件末尾。

当然,file.Seek() 的第一个参数也可以是负数,比如我们想读取文件最后六个字节的内容,可以如下操作:

go 复制代码
file.Seek(-6, io.SeekEnd)
3) 按行读取

我们可以使用 bufio.NewScanner() 函数来按行读取文件内容:

go 复制代码
file.Seek(0, io.SeekStart)
scanner := bufio.NewScanner(file)

for scanner.Scan() {
    fmt.Println(scanner.Bytes(), scanner.Text())
}

这里,我们使用了两个内容,一个是 .Bytes(),一个是 .Text(),分别打印的内容是该行的字节数组和字符串数据。

2、文件写入

1. 文件的打开与关闭

文件写入的操作中,打开与关闭一个文件的操作如下:

go 复制代码
filePath := "a.txt"
file, err := os.Create(filePath)
if err != nil {
    fmt.Println("create file error: ", err)
    return
}

defer file.Close()

使用到的函数是 os.Create(),在这里,如果目标文件 filePath 不存在则会自动创建一个文件,如果存在,则会清空原来的数据,重新写入。

2. 文件内容的写入

文件打开以后,下面介绍几种写入内容的方式

1) file.Write()

可以直接使用 file 以字节数组的形式往文件写入数据:

go 复制代码
    n, err := file.Write([]byte("first line\n"))
    if err != nil {
        fmt.Println("write error: ", err)
        return
    }
    fmt.Printf("write %d bytes", n)

这里需要注意,如果要换行需要在末尾手动加上 \n 字符。

2) io.WriteString()

我们也可以使用 io.WriteString() 函数往文件里写入数据:

go 复制代码
    n, err := io.WriteString(file, "first line\n")
    if err != nil {
        fmt.Println("write error: ", err)
        return
    }
    fmt.Printf("write %d ", n)
3) bufio.NewWriter()

我们还可以使用 bufio.NewWriter() 函数写入,这种操作是以缓冲的形式写入,操作示例如下:

go 复制代码
    writer := bufio.NewWriter(file)
    n, err := writer.WriteString("first line\n")
    if err != nil {
        fmt.Println("write error: ", err)
        return
    }
    writer.Flush()
    fmt.Printf("write %d bytes\n", n)

这种操作需要在最后使用 writer.Flush() 函数将数据从缓冲区写入文件。

3、文件追加

1. 文件的打开与关闭

如果要对文件内容进行追加,我们可以使用 os.OpenFile() 函数,以下是一个使用示例:

go 复制代码
filePath := "a.txt"

file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
    fmt.Println("append file error: ", err)
    return
}
defer file.Close()

在这里,os.OpenFile() 函数接受三个参数,第一个是文件地址,第二个是标志位,第三个是文件的权限。

对于标志位,这里的 os.O_APPEND、os.O_WRONLY、os.O_CREATE 分别表示追加,只写和创建,这样即便是文件不存在也不会报错,而是会创建一个新文件。

2. 文件内容的追加

追加操作可以使用 file.Write() 来写入字节数组,或者 file.WriteString() 写入字符串:

go 复制代码
file.Write([]byte("hello write byte\n"))
file.WriteString("hello write string\n")

4、os.OpenFile()

这里再单独介绍一下 os.OpenFile() 函数,这个函数在前面追问文件内容的时候已经使用过一次了,这里着重再讲一下。

os.OpenFile() 函数是上面介绍的这些操作的基础函数,也就是说读取文件使用的 os.Open(),写入文件使用的 os.Create(),在底层的逻辑里都是调用的 os.OpenFile(),不过是在具体实现的时候,根据不同的目标,比如读取或者写入来传入不同的参数以实现具体功能。

先来介绍 os.OpenFile() 函数的参数。

1. os.OpenFile() 参数

这个函数接收三个参数,name,flag 和 perm。

1) name

name 就是文件名称,string 类型,表示我们需要操作的目标文件。

2) flag

flag 表示的是操作的目的,比如前面介绍追加文件的时候用到的 os.O_APPEND|os.O_WRONLY|os.O_CREATE

参数类型是 int,在源码里定义了一系列关于文件的操作,如下:

go 复制代码
const (
    // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
    O_RDONLY int = syscall.O_RDONLY // open the file read-only.
    O_WRONLY int = syscall.O_WRONLY // open the file write-only.
    O_RDWR   int = syscall.O_RDWR   // open the file read-write.
    // The remaining values may be or'ed in to control behavior.
    O_APPEND int = syscall.O_APPEND // append data to the file when writing.
    O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
    O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
    O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
    O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
)

比如这里有 O_RDONLY 表示只读,O_WRONLY 表示只写,O_RDWR 表示读和写,在操作文件的时候,这几个参数之一是必传的,用来表示文件操作的目的是读或者写。

下面几个参数则需要和其他上面的几个参数之一合并使用,比如 O_APPEND 追加,O_CREATE 创建等。

上面我们介绍追加功能的时候,就是一个示例,内容是 os.O_APPEND|os.O_WRONLY|os.O_CREATE,这个操作首先通过必传的 os.O_WRONLY 表示是一个写操作,然后通过 os.O_APPEND 表示是追加操作,会在文件的末尾接着写入,而 os.O_CREATE 则表示如果目标文件不存在则创建一个新文件。

通过这种操作叠加的方式使我们追加文件的程序变得更健壮,不会因为文件不存在而报错。

3) perm

perm 表示权限,指的是我们操作文件的时候,对文件赋予的权限,和 Linux 上文件操作的权限是一致的,比如 0644 代表的含义是当前用户拥有可读可写,同用户组和其他用户组只拥有可读权限。

2. os.Open() 和 os.Create()

前面介绍了 os.Open() 和 os.Create() 分别用来读取和写入文件的操作示例,这两个函数背后也是通过调用 os.OpenFile() 来实现的。

1) os.Open()

os.Open() 函数在源代码中的定义如下:

go 复制代码
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

可以看到通过 OpenFile() 给了一个只读的权限实现了读取文件内容的操作。

2) os.Create()

os.Create() 函数的源码如下:

go 复制代码
func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

os.Create() 函数则是通过读写操作,不存在就创建文件操作,存在就对原文件进行截断操作的方式来实现写入。

3. os.ReadFile() 和 os.WriteFile()

除了上面介绍的读写操作,这里还介绍两个以 os.OpenFile() 函数为基础实现的读写操作,不过这两个函数的读和写都是一次性的,也就是会一次性读取文件全部内容,或者一次性写入全部内容。

1) os.ReadFile()

os.ReadFile() 函数操作示例如下:

go 复制代码
    filePath := "a.txt"
    data, err := os.ReadFile(filePath)
    fmt.Println("data: ", string(data), ", err: ", err)

返回的是一个字节数组,如果想要按行进行切割,可以使用 strings.Split(string(data), "\n") 操作。

2) os.WriteFile()

os.WriteFile() 操作示例如下:

go 复制代码
    err = os.WriteFile(filePath, []byte("hello write byte\nok write done\nlast line write\n"), 0644)
    fmt.Println("write error: ", err)

这里将多行数据使用 \n 进行分隔。

5、文件属性

打开一个文件后,我们可以获取这个文件的相关属性。

可以如下操作:

go 复制代码
filePath := "a.txt"
file, _ := os.Open(filePath)

info, err := file.Stat()
if err != nil {
    fmt.Println("error: ", err)
}
defer file.Close()

我们通过 file.Stat() 获取 FileInfo,文件的信息就都在 info 里了:

go 复制代码
fmt.Println("文件名称: ", info.Name())
fmt.Printf("文件大小为 %d bytes\n", info.Size())
fmt.Printf("文件权限:%s, %o \n", info.Mode(), info.Mode())
fmt.Println("文件上次修改时间为:", info.ModTime())

这里文件权限打印出的字符串是 -rw-rw-rw-,然后我们打印其八进制的内容就是常见的 666 形式了。

相关推荐
XHunter4 天前
Golang基础笔记十三之context
golang基础笔记
XHunter8 天前
Golang基础笔记十二之defer、panic、error
golang基础笔记
XHunter10 天前
Golang基础笔记十一之日期与时间处理
golang基础笔记
XHunter15 天前
Golang基础笔记十之goroutine和channel
golang基础笔记
XHunter17 天前
Golang基础笔记九之方法与接口
golang基础笔记
XHunter1 个月前
Golang基础笔记三之数组和切片
golang基础笔记
XHunter1 个月前
Golang基础笔记二之字符串及其操作
golang基础笔记