在上一讲中,我们介绍了net、fmt、strconv及json包。并通过对上述四个包的运用,实现了简单的服务器软件。不过它并不完美,因为每次运行都是从一张“白纸”开始的。因为添加的数据都在内存中,程序一旦停止,数据便会消失。所以,我们不得不寻找将数据保存下来的方法。这类将数据保存下来的方法,就是数据的持久化。
一般来说,无论数据的格式如何(json/xml/docx/xlsx/jpg/mp4……),以何种方式进行(数据库/文件/网络……),持久化的本质还是文件。所以本讲就先从这个“本质”开始,介绍Go语言中的文件读写方法。
为了更贴近实际应用,帮助大家更好地理解,本讲的案例是“生日祝福卡”程序。编写的生日祝福和署名通过命令行输入,保存到文本文件中。接受祝福的人可以直接运行程序查看这些祝福和署名(当然也可以直接打开文本文件查看)。
该示例使用到的知识点是文件读(查看祝福)、写(保存祝福),这个知识点是本讲的核心。还有一个知识点是如何在Go程序中读取命令行参数,算是附赠内容。鉴于示例代码并不复杂,我把完整的代码放在了文章的末尾。
# 获取命令行参数
在某些时候,我们想通过命令行参数来告知程序一些信息。就拿本讲的示例来说,假如有个网名为“兔八哥”的网友送来了祝福:“祝你生日快乐!”,我们便可通过下面的命令行将上述信息告知给程序:
shell
复制代码main "祝你生日快乐!" "兔八哥"
这里,main就是编译好的可执行程序。而在main程序中,默认是不理会这些参数的,只有使用os包中的Args变量值才可拿到上述信息。
在程序调试阶段,我们还可以通过下面的命令行进行测试:
shell
复制代码go run main.go "祝你生日快乐" "兔八哥"
该命令行的作用就是先编译出可执行的main程序,然后执行main程序,并将参数传递进去。
Go语言中读取命令行参数是通过读取
go
复制代码os.Args
变量的值来实现的。Args本身的类型是[]string,我们先在main()函数中输出os.Args的值,代码如下:
go
复制代码func main() {
fmt.Println(os.Args)
}
然后打开命令行,执行:
shell
复制代码go run main.go "祝你生日快乐" "兔八哥"
观察控制台的输出结果,可以发现Args中包含了3个值,其中第一个参数是可执行文件本身,后面的才是我们真正想要的信息。
据此,我们便可实现如下逻辑:
- 当传入的参数个数为3时,读取后两个参数的内容作为祝福和署名,并调用写文件的函数记录内容;
- 当传入的参数个数不为3时,读取文件的内容,并输出到控制台上。
实现代码如下:
go
复制代码var filepath = "./content.txt"
func main() {
if len(os.Args) >= 3 {
appendContent(os.Args[1], os.Args[2])
} else {
fmt.Println(outputContent())
}
}
其中,appendContent()负责追加文件内容,outputContent()以string形式返回文件中的内容。此外,为了方便起见,我还声明了一个filePath变量,指明了文件的路径,将在后面的代码中使用它。
# 获取文件信息
若要向文件中写入数据,一个必须的前提就是存在目标文件,而要判断文件是否存在,就要通过获取这个文件的信息来进行了。
在Go语言中,获取文件信息和创建文件都通过os包中的函数来进行,具体函数定义格式如下:
go
复制代码// 获取文件信息
func Stat(name string) (FileInfo, error)
// 判断错误信息的类型是否属于文件不存在
func IsNotExist(err error) bool
// 创建文件
func Create(name string) (*File, error)
一般创建文件的具体流程是先获取文件信息,如果文件不存在或给定的路径是目录,则创建文件。
判断路径是否是目录需要调用FileInfo类型中的IsDir()函数,它将返回布尔值。
💡 提示:等等,什么叫做“给定的路径是目录”呢?在文件系统中,同名的文件和目录是允许同时存在的。但写数据只能向文件中写,而不能直接向目录中写。当给定的路径是目录时,os.IsNotExist()函数依然会返回false。所以要让程序正确运行,就要排除给定的路径是目录的干扰。另一方面,在其它需求中,很可能还要求给定的路径必须是目录才能进行后续的操作。总之要具体需求,具体分析,具体使用了。
参考代码片段如下:
go
复制代码// 创建文件,若文件存在则不创建
func createFile() {
if fileInfo, err := os.Stat(filepath); err != nil {
if os.IsNotExist(err) {
// 文件不存在,则创建文件
file, err := os.Create(filepath)
if err != nil {
fmt.Println("创建文件失败,错误信息:", err)
return
}
// 关闭文件操作
file.Close()
} else {
// 其它错误
panic(err)
}
} else {
if fileInfo.IsDir() {
// 件是目录,则创建文件
file, err := os.Create(filepath)
if err != nil {
fmt.Println("创建文件失败,错误信息:", err)
return
}
// 关闭文件操作
file.Close()
}
}
}
在获取文件信息,执行os.Stat()函数时,出现任何错误都会返回不为nil的err,文件不存在就包含其中。所以当err不是nil时,要首先判断错误的具体类型,当错误为文件不存在时,创建文件。另一方面,当err是nil且给定的路径是目录时,依然需要创建文件。
# 写文件
到此,文件已经准备好了,接下来就是向文件写入内容了。在Go语言中,写文件依然是通过os包中的函数来进行。具体说来,分为三步:首先打开文件,接着是写入数据,最后是关闭文件。
# 打开文件
打开文件的函数定义如下:
go
复制代码func OpenFile(name string, flag int, perm FileMode) (*File, error)
显然,该函数需要3个参数,首先是string类型的name,表示文件的路径。其次是int型的flag,表示打开文件的方式(创建/追加/只读等,可以结合使用)。最后是FileMode型的perm,当要创建文件时,该参数指定了文件的权限信息。
本例已经在前面的步骤中创建好了文件,且文件的内容要求追加写入(后一个人的祝福不允许覆盖掉钱一个人的祝福),因此打开文件的代码片段如下:
go
复制代码file, err := os.OpenFile(filepath, os.O_APPEND, os.ModePerm)
if err != nil {
panic(err)
}
# 写入数据,关闭文件
写文件的函数定义如下:
go
复制代码func (f *File) Write(b []byte) (n int, err error)
这一步,我们便可通过OpenFile()函数返回的*File类型值,调用该函数,实现数据的写入了。具体代码如下:
go
复制代码defer file.Close()
_, err = file.Write([]byte(content + "\n"))
if err != nil {
panic(err)
} else {
fmt.Println("你的祝福即将抵达!")
}
💡 提示:将string型转为[]byte型,只需使用[]byte(),将string型的变量传入其中即可。反过来,将[]byte型转为string型,只需使用string(),将[]byte型变量传入其中即可,在稍后的读文件部分会使用。
注意,这里面我用了断言来执行文件的关闭。这是一个较为稳妥的策略。如此便可当文件被打开,但发生写入错误时,仍能正常关闭文件。
写文件的过程到此告一段落,我们接下来开始读文件的实现。
# 读文件
在io包中,提供了较为方便的io读写方式。当需要读取某个文件的内容时,只需调用ioutil.ReadFile(),便可获取文件的数据了。该函数的具体定义格式如下:
go
复制代码func ReadFile(filename string) ([]byte, error)
该函数的使用非常简单,只需传入string型的文件路径即可。最终将返回[]byte类型的数据。当然,还有喜闻乐见的error类型值,用来判断和获取读文件错误与否以及具体的错误信息。
对于本例而言,读文件的函数实现如下:
go
复制代码// 输出文件现有内容
func outputContent() string {
fileData, err := ioutil.ReadFile(filepath)
if err != nil {
panic(err)
}
return string(fileData)
}
好了,文件的读写都实现了,最后把它们串在一起,就可完成本例的需求了!完整的代码在文末,请大家参考。
# 总结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
- os及io包(ioutil)的使用
本讲是包系列专题的第四篇。在本讲中,我们创建了一个“生日祝福卡”应用。具体涉及文件的读/写操作和获取命令行参数的技巧。读文件使用了io/ioutil中的函数;写文件则包含了文件信息的获取、文件类型的判断(目录还是文件)、文件的创建以及写入数据的方法。
需要说明的是,os和io/ioutil都具备读写文件的能力。感兴趣的朋友可以自行尝试使用io/ioutil写文件,os读文件。不过,io/ioutil似乎不能实现文件的追加写入,只能清空之前的内容,然后写入新的。随着更新,可能日后会支持追加。当然也有可能是我没有发现它正确的使用方法,欢迎大家一起讨论交流。
好了,本讲就到这里。
➡️ 在下次课程中,我们会介绍Go语言中包的更多使用技巧,具体内容是:
- Go语言中的持久化存储之数据库
# 示例完整代码
go
复制代码package main
import (
"fmt"
"io/ioutil"
"os"
)
var filepath = "./content.txt"
func main() {
if len(os.Args) >= 3 {
appendContent(os.Args[1], os.Args[2])
} else {
fmt.Println(outputContent())
}
}
// 追加内容到文件
func appendContent(content string, name string) {
createFile()
appendContentToFile("来自" + name + "的祝福:" + content)
}
// 输出文件现有内容
func outputContent() string {
fileData, err := ioutil.ReadFile(filepath)
if err != nil {
panic(err)
}
return string(fileData)
}
// 创建文件,若文件存在则不创建
func createFile() {
if fileInfo, err := os.Stat(filepath); err != nil {
if os.IsNotExist(err) || fileInfo.IsDir() {
// 文件不存在,或文件是目录,则创建文件
file, err := os.Create(filepath)
if err != nil {
fmt.Println("创建文件失败,错误信息:", err)
return
}
// 关闭文件操作
file.Close()
} else {
// 其它错误
panic(err)
}
}
}
// 向文件追加内容,每次追加以换行间隔
func appendContentToFile(content string) {
file, err := os.OpenFile(filepath, os.O_APPEND, os.ModePerm)
if err != nil {
return
}
defer file.Close()
_, err = file.Write([]byte(content + "\n"))
if err != nil {
fmt.Println("写文件操作出现错误,异常信息:", err)
return
} else {
fmt.Println("你的祝福即将抵达!")
}
}