关于Go语言中文件操作部分

本文章为笔者对于Go语言文件操作知识的粗浅认知,如有不当,烦请指正

 前言

Go语言中关于文件操作一般为以下几种:

  1. 打开和关闭文件
  2. 读取文件
  3. 文件写入

这些操作有一些地方是一样的,类似于模板,是相对固定的代码,比方说我们都是先打开文件才能进行读写,而打开文件之后一定要关闭,否则会造成过度消耗资源,异常安全等问题,说清楚这个之后,下面我们来详细学习

文件路径

首先我们来说明一下文件路径,很简单但很重要

在Go语言中读取文件时,文件的路径是一个字符串,它指定了文件在文件系统中的位置。这个路径可以是相对路径或绝对路径。

相对路径

相对路径是相对于当前工作目录的路径。例如,如果你的程序位于目录 /home/user/myapp,并且你想读取同一目录下的 config.txt 文件,你可以使用相对路径 ./config.txt

绝对路径

绝对路径是从文件系统的根目录开始的完整路径。使用绝对路径可以确保无论当前工作目录在哪里,都能正确找到文件。例如,/home/user/myapp/config.txt 是一个绝对路径。

 打开和关闭文件

        我们使用os.Open()函数来打开文件,使用 Close()函数关闭文件,在实际写代码时,我们一般要使用err来判断是否出现错误以及获取错误信息

 err是必须写的,因为Open()函数内部是这样定义的

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

下面是演示代码

package main

import (
	"fmt"
	"os"
)

func main() {
	// 只读方式打开当前目录下的main.go文件
	file, err := os.Open("src/Practice/test01.txt")
	if err != nil {
		fmt.Println("open file failed!, err:", err)
		return
	}
	// 关闭文件
	defer file.Close()
}

上面这段代码编译通过,但不会有任何结果,因为我们写的是正确的代码。

如果我们被读取的文件路径出现错误,即os.Open()函数括号里的内容不对,则通常会报这样的错误

关于Go语言中文件操作部分_第1张图片
有同学可能发现,在关闭文件时,我们用的是 

defer file.Close()

而不是

file.Close()

在Go语言中,使用defer关键字是为了来延迟函数的执行直到包含它的函数即将返回。

而在本例中,使用defer,即使出现异常或提前返回,也能释放资源,减少重复代码,提高代码可读性和维护性,降低因遗漏关闭操作导致的错误。简单来说,就是这样写代码健壮性更好。

读取文件

file.Read

较为常见的是使用Read()方法,其定义如下

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

这方法接受一个字节切片,并返回读取的字节数和可能的具体错误

package main

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

func main() {
	//以只读方式打开当前目录下的test01.txt文件
	file, err := os.Open("src/Practice/test01.txt")
	if err != nil {
		fmt.Println("file open failed, err:", err)
		return
	}
	//defer关闭文件
	defer file.Close()

	//使用Read方法读取数据
	//声明一个byte类型的切片,
    、、初始化长度为128字节,用来存储读取到的数据
	var tmp = make([]byte, 128)

	//调用文件对象的Read方法,将读取到的数据存入tmp切片中,
    //这里Read方法返回两个值,n表示实际读取到的字节数,err表示在读取过程中是否发生错误
	n, err := file.Read(tmp)

	//如果err值等于io.EOF,则表示文件已经读取到末尾,(EOF:End Of File,文件末尾)
	if err == io.EOF {
		fmt.Println("file read finished")
		return

		//检查err是否为nil,如果是,则打印错误信息并返回,结束执行
	} else if err != nil {
		fmt.Println("read file failed, err:", err)
		return
	}
	/*	打印读取到的数据字节总长度,以及读取到的数据
		因为本例中实际上被读取文件test01.txt有6968个字节,大于我们给tmp切片初始化的128字节,
		故这里实际上只能读取到128个字节病并将其打印,可以通过修改初始化存储上限而读取到全部字节*/
	fmt.Printf("读取了%d字节数据\n", n)
	fmt.Println(string(tmp[:n]))
}

关于代码的解释我都写在注释里

循环读取

上面这个例子中我们初始化一个切片来存放我们读取到的数据,但实际使用时我们会发现,很多时候我们初始化的切片长度不足以读取文件内的所有数据,这个时候我们就要想办法能够读取到文件内的所有数据,一个常见的办法就是循环读取

package main

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

func main() {
	//只读方式打开当前目录下的文件
	file, err := os.Open("src/Practice/test01.txt")
	if err != nil {
		fmt.Println("open file failed, err: ", err)
		return
	}

	//defer关闭文件
	defer file.Close()

	//for循环读取文件内容
	//声明一个content变量,初始化为byte类型的切片,用于存储读取到的所有数据
	var content []byte

	//声明一个byte类型的切片,初始化长度为128字节,
    //用于存储读取到的每一次读取到的数据,for循环为多次读取
	var tmp = make([]byte, 128)
	for {
		n, err := file.Read(tmp)
		if err == io.EOF {
			fmt.Println("finish")
			break
		}
		if err != nil {
			fmt.Println("read file failed ,err:", err)
			return
		}
		/*将每次读取到的数据追加到content切片中,
		这里使用了append函数和扩展语法...来将tmp[:n]作为一个整体追加到content切片中。
		*/
		content = append(content, tmp[:n]...)

	}
	fmt.Println(string(content))

}

上面这个例子中我没有打印读取到的字节数,因为在第一个例子中已经打印过了,这两个例子读取的都是同一个文件。

bufio读取

接下来再看一个使用for循环的例子,但这个例子的主角是bufio,在Go语言中,bufio 是一个缓冲 I/O 的包,它在I/O操作的基础上增加了缓冲和一些帮助函数用于简化操作

package main

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

func main() {
	//只读方式打开当前目录下的文件
	file, err := os.Open("src/Practice/test01.txt")
	if err != nil {
		fmt.Println("open file failed, err: ", err)
		return
	}

	//defer关闭文件
	defer file.Close()

	/*	创建一个信的bufio.Scanner对象,用于从file中读取数据,
		bufio.Scanner可以逐行读取文件内容*/
	scanner := bufio.NewScanner(file)

	//在循环内部,使用scanner.Text()方法获取当前行的文本,并使用fmt.Println打印出来。
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
	//循环结束后,检查scanner在读取过程中是否遇到了错误。如果有错误,程序将打印错误信息。
	if err := scanner.Err(); err != nil {
		fmt.Println("read file failed, err:", err)
	}
}

那么这两个方法有什么区别呢,简单来讲,使用 bufio.ReaderReadString 方法更适合于逐行读取文件,因为它提供了缓冲和自动处理行结束符的便利。而直接使用 os.FileRead 方法则提供了更多的灵活性,但需要手动处理更多的细节。

读取整个文件

那么有没有不用循环就可以读取整个文件的方法嘞

包有的,os(Go1.16之前io/ioutil)包的ReadFile函数能够读取完整的文件,只需要将文件名作为参数传入。

package main

import (
	"fmt"
	"os"
)

// os.ReadFile 读取整个文件
func main() {
	content, err := os.ReadFile("src/Practice/test01.tx")
	if err != nil {
		fmt.Println("read file failed, err:", err)
		return
	}
	fmt.Println(string(content))
}

文件写入操作

接下来我们来看看文件的写入操作

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。

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

其中:

name:要打开的文件名 flag:打开文件的模式。 模式有以下几种:

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加

perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。

Write和WriteString

package main

import (
	"fmt"
	"os"
)

func main() {
	//使用OpenFile函数,创建,清空,只写一个文件

	file, err := os.OpenFile("src/Practice/test02.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
	if err != nil {
		fmt.Println("failed, err:", err)
		return
	}
	defer file.Close()

	str := "aijkodhoia\n"

	//将str字符串转换为字节切片,并写入文件
	var _, err02 = file.Write([]byte(str))
	if err02 != nil {
		fmt.Println("err:", err)
		return
	}
	//直接将字符串写入文件
	file.WriteString("4365468458")
}

在上面的代码中,我们注意到有一串数字0666,那么这是个啥呢

其实,0666是一个八进制数,表示文件所有者、所属组和其他用户都有读(4)和写(2)权限即:所有者(owner):读 + 写 = 4 + 2 = 6 所属组(group):读 + 写 = 4 + 2 = 6 其他用户(others):读 + 写 = 4 + 2 = 6 如果你需要设置不同的权限,可以修改这个八进制数。

例如: 0644:所有者有读和写权限,所属组和其他用户只有读权限。 0755:所有者有读和写权限,所属组和其他用户有读和执行权限。 0700:只有所有者有读和写权限,所属组和其他用户没有任何权限。

os.WriteFile

os(Go1.16之前io/ioutil)包的os.WriteFile函数可直接向文件写入指定内容。

package main

import (
	"fmt"
	"os"
)

func writeFile() {

	//声明一个变量,标明要写入的内容
	str := "hello,沙河"

	//os.WriteFile()方法可以直接写入字符串内容
	err := os.WriteFile("src/Practice/test02.txt", []byte(str), 0666)
	if err != nil {
		fmt.Println("failed; err:", err)
		return
	}
}

func main() {
	writeFile()
}

bufio.NewWriter

package main

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

func main() {

	//前面创建清空写入操作部分都一样,类比于文件读取操作中的打开和关闭文件都一样
	file, err := os.OpenFile("src/Practice/test02.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
	if err != nil {
		fmt.Println("failed, err:", err)
		return
	}
	defer file.Close()

	//bufio创建一个对象
	writer := bufio.NewWriter(file)

	//调用WriteString方法,for循环写入数据
	for i := 1; i <= 10; i++ {
		writer.WriteString("456123\n")
	}

	writer.Flush()
}

 writer.Flush()

有同学可能注意到,使用bufio.Writer写入时我们在最后写了一个writer.Flush,那么这是个啥呢,其实writer.Flush() 方法是 bufio.Writer 对象的一个方法,用于将缓冲区中的数据强制写入底层的 io.Writer 对象中。bufio.Writer 通常用于提供缓冲的写入功能,这意味着数据首先被写入到内存中的缓冲区里,而不是直接写入文件或其他输出流。

使用writer.Flush() 方法有两个好处:

缓冲bufio.Writer 会将写入的数据缓存起来,直到缓冲区满了或者显式调用 Flush() 方法,才会将数据写入到底层的文件或输出流中。这意味着如果你写入少量数据,它可能不会立即被写入磁盘。

数据完整性:在某些情况下,你需要确保所有数据都被写入到文件中,特别是在程序可能会在写入操作后很快退出时。调用 Flush() 可以确保所有缓冲中的数据都被写入,避免数据丢失。

 

 总结

以上就是笔者对于Go语言文件操作的所有理解,一定有些遗漏和不足之处,欢迎各位读者在评论区指正

你可能感兴趣的:(golang,开发语言,后端)