Go语言之文件与目录处理

一、使用ioutil 包读写文件

鉴于处理文件是一种很常见的任务,标准库提供了ioutil 包,让您能够快速执行众多涉及读写文件的操作。实际上, 这个包几乎就是一个OS 模块包装器,使用它需要编写的代码更少,且无须执行清理操作。如果您需要执行下述任何操作,且无须做细致的控制,则使用ioutil包是不错的选择。

  • 读取文件
  • 列出目录的内容
  • 创建临时目录
  • 创建临时文件
  • 创建文件
  • 写入文件

1、读取文件

读取文件是最常见的操作之一。ioutil 包提供了函数Readfile ,您可使用它来完成这项任务,这个函数将一个文件名作为参数,并以字节切片的方式返回文件的内容,语法格式如下:

func ReadFile(filename string) ([]byte, error)    //ReadFile表示一次性读取全部内容

如果要将文件内容作为字符串使用,则必须将返回的字节切片转换为字符串,linux下的读取如图:

windows环境下文件内容如图:

从上图可以看出,读取文件的路径为相对路径,main.go与test为同级,abc.txt在test目录下,直接结果与上述相同,说明如下:

  • 使用ioutil 包中的函数Readfile 读取文件,返回值是一个字节切片和错误信息
  • 判断如果错误不为空,通过log.Fatal()打印内容
  • 将返回的字节切片转换为字符串,并打印

log.Fatal()函数功能:1、打印输出内容 2、退出程序 3、defer不执行

还有一个函数ReadAll()也具备读取文件的功能,但是此函数容易造成性能问题,读取内容少但是申请了很多的内容,网络请求大的时候容易造成内存浪费或者泄露,因此不建议使用,语法为: func ReadAll(r io.Reader) ([]byte, error)

2、创建文件:

ioutil包还提供了用于创建文件的便利函数WriteFile,这个函数设计用于将数据写入文件,但也可使用它来创建文件,函数WriteFile 接受一个文件名、要写入文件的数据以及应用于文件的权限

文件权限是从UNIX 权限衍生而来的,它们对应于3 类用户:文件所有者、与文件位于同一组的用户、其他用户。处理文件时,理解文件权限是确保安全的重要方面,因为错误地设置权限意味着数据可能被原本不应该让他读取的人获得

Go 语言使用UNIX 权限的数字表示法,也遵循421规则,比如755、644等权限,在UNIX 型系统中, 文件的默认权限为0644 ,即所有者能够读取和写入,而其他人只能读取。在文件系统中创建文件时,应考虑如何给它指定权限。如果不确定该如何指定权限,使用默认的UNIX权限就可以了。

下面例子表示创建一个空文件,设置权限为0644(0表示十进制),如图:

执行代码后,在目录test下可以看到创建的空文件bb.txt,如图:

3、写入文件:

函数WriteFile 也可用来写入文件,要写入文件,只需传入一些值,而不是传入空字节切片,要将字符串写入文件,必须先将其转换为字节切片

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

下面例子表示向文件cc.txt中写入数据,文件不存在将创建,并写入字符串内容,需要先将字符串转成字节切片,如图:

运行后可以看到在test目录下生成了文件cc.txt,并且内容为hello,world,如图:

注意:WriteFile函数接收的文件数据需要为字节切片或者数组类型,写入内容会覆盖原来内容

4、列出目录的内容:

要处理文件系统中的文件,必须知道目录结构。ioutil 包提供了便利函数ReadDir,它接受以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件,文件名的类型为Filelnfo ,包含如下信息:

  • Name :文件的名称
  • Size :文件的长度, 单位为字节
  • Mode : 用二进制位表示的权限
  • ModTime:文件最后一个被修改的时间
  • IsDir:文件是否是目录
  • Sys :底层数据源

语法如下:

func ReadDir(dirname string) ([]os.FileInfo, error)

下面例子表示列出目录test下的内容,并打印名称、权限、以及判断是否为目录,如图:

运行结果如下:

二、通过os包读写文件

ioutil 包可用于执行一些常见的文件处理操作,但要执行更复杂的操作,应使用OS 包,OS包运行在稍低的层级,因此使用它时,必须手工关闭打开的文件,ioutil 包中的很多函数都是OS 包包装器,因此使用时无须显式地关闭文件。

1、通过0pen()函数来读取文件:

//打开一个需要被读取的文件,如果成功读取,返回的文件对象将可用被读取,该函数默认的权限为O_RDONLY,也就是只对文件有只读权限。如果有错误,将返回*PathError类型
func Open(name string) (*File, error)  

os.Open()和os.OpenFile()方法都返回一个*File文件对象,该文件对象默认有很多方法,其中读取文件的方法如下:

  • func (f *File) Read(b []byte) (n int, err error) :从文件对象中读取长度为b的字节,返回当前读到的字节数以及错误信息。因此使用该方法需要先初始化一个符合内容大小的空的字节列表。读取到文件的末尾时,该方法返回0,io.EOF
  • func (f *File) ReadAt(b []byte, off int64) (n int, err error) :从文件的off偏移量开始读取长度为b的字节。返回读取到字节数以及错误信息。当读取到的字节数n小于想要读取字节的长度len(b)的时候,该方法将返回非空的error。当读到文件末尾时,err返回io.EOF

下面例子表示通过Open()函数打开文件,并通过Read()方法来获取文件对象的内容,如图:

执行结果如下:

2、通过os.OpenFile()来读取文件

OpenFile有三个参数:

  • 参数1:打开文件的路径
  • 参数2:打开模式
  • 参数3:表示权限,取值范围(0-7),和linux里操作文件那个读写一样,例如0666表示可读可写

常见的模式有:

参数说明
O_RDONLY只读方式打开
O_WRONLY只写方式打开
O_RDWR读写方式打开
O_APPEND追加方式打开
O_CREATE不存在,则创建
O_EXCL如果文件存在,且标定了O_CREATE的话,则产生一个错误
O_TRUNC如果文件存在,将文件清空
O_NOCTTY如果文件名代表一个终端设备,则不把该设备设为调用进程的控制设备
O_NONBLOCK如果文件名代表一个FIFO,或一个块设备,字符设备文件,则在以后的文件及I/O操作中置为非阻塞模式
O_SYNC当进行一系列写操作时,每次都要等待上次的I/O操作完成再进行
func OpenFile(name string, flag int, perm FileMode) (*File, error)

下面例子表示打开文件aa.txt,如果不存在就创建,文件权限为655,并向其追加内容beijing ,如图:

文件内容和权限如图:

注意:os.Open()函数只能用来读取文件,os.OpenFile()函数可以读取也能以读写方式打开文件,功能更强大一些

3、复制文件

只需结合使用OS 包中的几个函数。以编程方式复制文件的步骤如下:

  • 打开要复制的文件
  • 读取其内容
  • 创建井打开要将这些内容复制到其中的文件
  • 将内容写入这个文件
  • 关闭所有己打开的文件

下面例子中表示读取一个现有文件cc.txt的内容,并将其复制到bb.txt这个文件中,如图:

运行后可以看到bb.txt中已经复制过来了内容,如图:

  • 第11行:使用OS 包中的函数Open 来读取磁盘文件
  • 第15行:使用defer 语句在程序完成其他所有操作后关闭文件
  • 第16行:使用函数OpenFile打开文件,第一个参数是要打开的文件(不存在会创建),第二个参数是用于文件的标志,在这里指定的是读写文件, 并在文件不存在时创建它; 最后一个参数设置文件的权限
  • 第21行:使用io 包中的函数Copy 复制源文件的内容,并将其写入目标文件

4、删除文件

OS 包提供了函数Remove,使用这个函数时, 不会发出警告, 也无法将删除的文件恢复,如下:

执行这段代码后,将删除test目录下名字为bb.txt的文件

5、使用os.Open()和bufio.Reader()读取文件内容

bufio包实现了缓存IO,它本身包装了io.Readerio.Writer对象,创建了另外的Reader和Writer对象,该种方式是带有缓存的,对于文本I/O来说,该包使用起来更加便利

bufio模块内部定义了一个用来缓冲io.Reader对象的结构体,同时该结构体拥有以下相关的方法:

(1)、NewReader函数用来返回一个默认大小buffer的Reader对象,等同于NewReaderSize(rd,4096),语法格式如下:

func NewReader(rd io.Reader) *Reader

下面例子表示通过NewReader函数来获取缓冲器,并通过Read()函数读取文件内容,如图:

(2)、NewReaderSize()函数返回一个指定大小buffer(size最小为16)的Reader对象,如果 io.Reader参数已经是一个足够大的Reader,它将返回该Reader

func NewReaderSize(rd io.Reader, size int) *Reader

(3)、Buffered()方法返回从当前buffer中能被读到的字节数

func (b *Reader) Buffered() int

(4)、Discard()方法跳过后续的 n 个字节的数据,返回跳过的字节数。如果0 <= n <= b.Buffered(),该方法将不会从io.Reader中成功读取数据

func (b *Reader) Discard(n int) (discarded int, err error)

(5)、Peekf方法返回缓存的一个切片,该切片只包含缓存中的前n个字节的数据

func (b *Reader) Peek(n int) ([]byte, error)

(6)、Read方法把Reader缓存对象中的数据读入到[]byte类型的p中,并返回读取的字节数。读取成功,err将返回空值

func (b *Reader) Read(p []byte) (n int, err error)

(7)、ReadByte方法返回单个字节,如果没有数据返回err

func (b *Reader) ReadByte() (byte, error)

(8)、ReadBytes方法返回数据的拷贝

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

(9)、ReadString功能同ReadBytes,返回字符串

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

(10)、ReadRune方法读取单个UTF-8字符并返回一个rune和字节大小

func (b *Reader) ReadRune() (r rune, size int, err error)

(11)、ReadLine()方法用于从Reader中读取一行内容,如果数据数据过长而无法完全放入缓冲区中,会将isPrefix设置为true,此方法已过时,不推荐使用了,推荐使用ReadBytes和ReadString

func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)

6、通过os.ReadDir()读取目录内容

7、使用bufio包的Scanner对数据进行读取(推荐)—逐行读取

实际使用中,更推荐使用Scanner方法对数据进行读取,而非直接使用Reader类。Scanner可以通过splitFunc将输入数据拆分为多个token,然后依次进行读取。

和Reader类似,Scanner需要绑定到某个io.Reader上,通过NewScannner进行创建,函数声明如下:

func NewScanner(r io.Reader) *Scanner

在使用之前还需要设置splitFunc(默认为ScanLines),splitFunc用于将输入数据拆分为多个token。bufio模块提供了几个默认splitFunc,能够满足大部分场景的需求,根据实际业务需求进行选择,包括:

  • ScanBytes,按照byte进行拆分
  • ScanLines,按照行(“\n”)进行拆分
  • ScanRunes,按照utf-8字符进行拆分
  • ScanWords,按照单词(” “)进行拆分

通过Scanner的Split方法,可以为Scanner指定splitFunc。使用方法如下:

scanner := bufio.NewScanner(os.StdIn)
scanner.Split(bufio.ScanWords)

Scanner初始化之后,通过Scan方法读取数据,使用Text和Bytes方法获取数据,Text返回一个字符串,Bytes返回字节数组。方法声明如下:

func (s *Scanner) Scan() bool        //读取数据
func (s *Scanner) Text() string      //获取数据,返回字符串
func (s *Scanner) Bytes() []byte      //获取数据,返回字节数组

下面例子表示通过NewScanner()方法并以默认的ScanLines读取数据,并以字符串的形式输出,如图:

8、通过io包中的相关函数写入文件:

io包中有一个WriteString()函数,用来将字符串写入一个Writer对象中,函数结构如下:

//将字符串s写入w(可以是一个[]byte),如果w实现了一个WriteString方法,它可以被直接调用。否则w.Write会再一次被调用
func WriteString(w Writer, s string) (n int, err error)

下面例子表示通过 WriteString()函数 将字符串w的内容写入到文件中,如图:

9、使用bufio包中的相关函数写入文件:

bufioio包中很多操作都是相似的,唯一不同的地方是bufio提供了一些缓冲的操作,如果对文件I/O操作比较频繁的,使用bufio还是能增加一些性能的

bufio包中,提供了一个Writer空结构体,一般需要使用NewWriter或者NewWriterSize来初始化一个结构体对象,如下:

(1)、NewWriter方法返回默认缓冲大小的Writer对象(默认是4096)

func NewWriter(w io.Writer) *Writer    //创建写入对象,实际也是调用的NewWriterSize

(2)、NewWriterSize指定缓冲大小创建一个Writer对象

func NewWriterSize(w io.Writer, size int) *Writer    //创建写入对象

Writer对象相关的写入数据的方法如下:

//把p中的内容写入buffer,返回写入的字节数和错误信息。如果nn<len(p),返回错误信息中会包含为什么写入的数据比较短
func (b *Writer) Write(p []byte) (nn int, err error)
//将buffer中的数据写入 io.Writer
func (b *Writer) Flush() error

以下三个方法可以直接写入到文件中:

//写入单个字节
func (b *Writer) WriteByte(c byte) error
//写入单个Unicode指针返回写入字节数错误信息
func (b *Writer) WriteRune(r rune) (size int, err error)
//写入字符串并返回写入字节数和错误信息
func (b *Writer) WriteString(s string) (int, error)

下面例子表示使用Writer对象的方法进行写入数据,如图:

下面例子表示通过WriteString()将字符串写入到文件中,如图:

注意:使用bufio在写入到内存的缓冲区后,需要通过Flush()将内容刷新到硬盘中,否则文件中没有数据

三、使用文件来管理配置

在编程中常使用文件来管理配置。考虑到代码可能在不同的环境中执行,可使用一个文件来设置各种用于启动程序的配置参数。

在开发过程中,应用程序将从开发环境移到生成环境中,而使用文件是管理环境差异的有效方式。这种环境差别包括如下几个方面:

  • Web 服务的URL
  • 访问密钥
  • 环境变量
  • 端口号

使用JSON 文件:

在声明可存储在文件中并在必要时读取的配置方面,JSON 是一种卓有成效的标准方式。将配置存储在文件中的另一个优点是,可对其进行版本控制井集成到自动构建过程中,JSON 是一种声明键值的简单方式,您可将这些键值解码为Go 结构体,再使用它们

下面例子在test目录中定义json文件config.json,添加内容如下:

通过ioutil.Readfile函数读取json,并将其解码为一个配置结构体,作为配置数据使用,如图:

  • 第17行:ioutil.ReadFile读取文件后返回类型为切片类型
  • 第22行:通过json.Unmarshal将json数据解码到结构体中,解码到的结构体必须是指针

注意:Json.Unmarshal是将json字符串解码到相应的数据结构中。 json字符串解析时,需要一个”接收体”接收解析后的数据,并且这个接收体必须是传递指针(例如:&productresult),否则解析虽然不报错,但数据无法赋值到接收体中

使用TOML 文件

TOML ( Tom ’s Obvious, Minimal Language )是一种专为存储配置文件而设计的格式,相比于JSON ,其表达能力更强,且更容易映射Go 类型。

TOML格式非常灵活,可以是数字、字符串、布尔等简单类型,也可以是数组、map等等复杂的类型

JSON 是为序列化数据而设计的,而TOML 是专门为存储配置文件而设计的,因此TOML 相比于JSON有一定的优势,如更容易阅读、具备JSON 没有的特性(如注释),在基本层面, TOML的语法非常简单,可像JSON 那样指定键和值,config.toml内容 如下:

name = "zhangsan"
age = 100
weight = "100kg"

与JSON 一样, TOML 也不是Go 的组成部分,它可用于任何语言。Go 标准库中没有支持TOML 的包,虽然您可编写一些代码来分析TOML文件,但有一些卓越的第三方TOML包也可以使用,最流行的TOML包之一是BurntSushi 编写的。这个包能够让您轻松地在Go 语言中将TOML用作配置文件格式,但由于它并不包含在标准库中,因此您必须单独安装,执行如下命令:

go get github.com/BurntSushi/toml

安装后在GOPATH的src目录下可以看到安装之后的包,如图:

TOML包提供了函数DecodeFile ,这个函数接受一个文件名以及一个要将TOML 解码到其中的结构体

修改上面的例子,使其从toml中读取数据,然后解码到结构体中,如图:

  • 第18行:toml.DecodeFile返回的值类型为toml.MetaData,并不需要因此将接收值设置为匿名变量”_”

标签