4、文件编程

常用结构体、接口

os.File

在 Golang 中内置包os中,打开文件使用 Open 函数(只读)和OpenFile函数(可切换模式),关闭文件使用 Close 函数,打开文件、关闭文件以及大多数文件操作都涉及到一个很重要的结构体 os.File 结构体。

实现了io.Readerio.Writerio.Closer等接口

type File struct {
	*file // file 指针
}
type file struct {
    //是一个FD结构体类型,是一个文件的唯一标志,每一个被打开的文件在操作系统中,都会有一个文件标志符来唯一标识一个文件
	pfd        poll.FD
    //文件名
	name       string
    //文件的路径信息,也是一个结构体
	dirinfo    *dirInfo
    //是一个 bool 类型,表明该文件是否可以被追加写入内容。
	appendMode bool
}

fs.FileInfo

type FileInfo interface {
    Name() string       // 文件的名字
    Size() int64        // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
    Mode() FileMode     // 文件的模式位 (例-rw-rw-r--)
    ModTime() time.Time // 文件的修改时间
    IsDir() bool        // 等价于Mode().IsDir()
    Sys() interface{}   // 底层数据来源(可以返回nil)
}

os.fileStat实现了FileInfo接口的所有方法,使用os.Stat(path)函数可以获取path对应文件的fileStat实例

func Stat(name string) (FileInfo, error)
package main

import (
	"fmt"
	"os"
)

func main() {
	info, err := os.Stat("./test.txt")
	if err != nil {
		fmt.Println(err)
	}
  //判断文件是否存在
  if os.IsNotExist(err) {
      fmt.Println("file not exist")
  }
	fmt.Println("name:", info.Name())
	fmt.Println("size:", info.Size())
	fmt.Println("mode:", info.Mode())
	fmt.Println("modtime:", info.ModTime())
	fmt.Println("isDir:", info.IsDir())
	/*
		name: test.txt
		size: 21
		mode: -rw-rw-rw-
		modtime: 2022-03-28 14:09:32.6151527 +0800 CST
		isDir: false
	*/
}

io.EOF

var EOF = errors.New("EOF")

是 error 类型。根据 reader 接口的说明,在 n > 0 且数据被读完了的情况下,返回的 error 有可能是 EOF 也有可能是 nil。

路径解析

路径相关的函数有两个包,pathpath/filepath, 两个包内有一些相同的函数,如IsAbs()Join()Dir() filepath中的函数兼容各个操作系统,涉及到windows系统路径操作时,应该使用filepath

filepath.Rel(basepath, targpath string) (string, error) //获取tarpath相对于basepath的相对路径
filepath.Abs(path string) (string, error) //获取绝对路径,如果path不是绝对路径,会加入当前工作目录以使之成为绝对路径。
path.Join(elem ...string) string//路径拼接
path.IsAbs(path string) bool//判断文件是否是绝对路径
path.Dir(path string) string//获取目录
package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	p1, _ := filepath.Rel("E:\\go-project\\demo\\", "E:\\go-project\\1.txt")
	fmt.Println(p1) // ..\1.txt
	p2, _ := filepath.Abs("./test.txt")
	fmt.Println(p2) //E:\go-project\demo\test.txt
	p3 := filepath.Join("E:\\go-project\\demo", "test\\hello.txt")
	fmt.Println(p3) //E:\go-project\demo\test\hello.txt
	p4 := filepath.IsAbs("E:\\go-project\\demo\\")
	fmt.Println(p4) //true
	p5 := filepath.Dir("E:\\go-project\\demo\\test.txt")
	fmt.Println(p5) //E:\go-project\demo
}

文件操作

常用方法

//创建
os.Mkdir(name string, perm FileMode) error //创建目录,只创建一层,相当于mkdir
os.MkdirAll(path string, perm FileMode) error //创建多层目录,相当于mkdir -p
os.Create(name string) (file *File, err error) //创建文件,如果存在则覆盖

//移动
func Rename(oldpath, newpath string) error //重命名、移动文件

//删除
func Remove(name string) error //删除单个
func RemoveAll(path string) error //递归删除目录

打开、关闭文件

Open

Open 函数接受一个字符串类型的文件名做为参数,如果打开成功,则返回一个 File 结构体的指针,否则就返回error错误信息。

底层使用的是OpenFile(name, O_RDONLY, 0),只读

os.Open(name string) (*File, error)

OpenFile

os.OpenFile(name string, flag int, perm FileMode) (file *File, err error)
//name:文件路径
//flag:文件打开方式,多个可使用|分隔开
//perm:打开文件的模式,即权限,例如0066、0777

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操作完成再进行。

Close

使用 File 指针来调用 Close 函数,如果关闭失败会返回 error 错误信息。

func (file *File) Close() error

Seek

Seek设置下一次读/写的位置。

offset为相对偏移量

whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。

它返回新的偏移量(相对开头)和可能的错误。

func (f *File) Seek(offset int64, whence int) (ret int64, err error)

Truncate

Truncate改变文件的大小,它不会改变I/O的当前位置。 如果截断文件,多出的部分就会被丢弃,也就是从头开始保留size个字节。如果出错,错误底层类型是*PathErrorsize = 0则清空文件。

func (f *File) Truncate(size int64) error

也可以使用os包提供的方法,一样的作用,name为文件名

func Truncate(name string, size int64) error

读写文件

http://image.iswbm.com/20211228231043.png

1、io包

由于os.File实现了io.Reader、io.Writer接口,所以这里实际调用的是File中实现的Read和Write方法

常用方法

//读,文件读取结束的标志是返回的 n 等于 0,并且err会返回io.EOF,单次读取的大小和b的大小一样
func (f *File) Read(b []byte) (n int, err error) 
//写,以字节数组写入
func (f *File) Write(b []byte) (n int, err error)
//写,以字符串写入
func (f *File) WriteString(s string) (n int, err error)
//复制文件
io.Copy(dst io.Writer, src io.Reader) (written int64, err error)

func (f *File) Read(b []byte) (n int, err error),其中b为保存读到数据的字节切片,n为读到的字节长度,如果读到文件末尾,方法会返回0,io.EOF

package main

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

func main() {
	file, err := os.Open("./test.txt")
	if err != nil {
		fmt.Println(err)
	}
	defer file.Close()
	//定义切片存放每次读取字节
	data := make([]byte, 1024)
	//声明切片,将每次读到的数据存放起来
	var result []byte
	for {
		i, err := file.Read(data)
		if err != nil && err != io.EOF {
			fmt.Println(err)
			break
		}
		if i == 0 {
			break
		}
		result = append(result, data[:i]...)
	}
	fmt.Println(string(result))
}

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

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.OpenFile("./test.txt", os.O_RDWR|os.O_CREATE, 0777)
	if err != nil {
		fmt.Println(err)
	}
    defer file.Close()
	s := "hello world\nhello golang"
	//写入文件,将字符串转字节数组,也可以直接使用WriteString
	n, err := file.Write([]byte(s))
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("write success,byte count =", n)
	}
}

复制

func Copy(dst Writer, src Reader) (written int64, err error),此处注意,由于File已经实现了Writer、Reader接口,所以可以直接传入File实例

package main

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

func main() {
	file, err := os.Open("./test.txt")
	if err != nil {
		fmt.Println(err)
	}
    defer file.Close()
	fileCp, err := os.OpenFile("./testCp.txt", os.O_CREATE|os.O_WRONLY, 0777)
	if err != nil {
		fmt.Println(err)
	}
    defer fileCp.Close()
	n, err := io.Copy(fileCp, file)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("copy success,n =", n)
}

2、ioutil包

常用方法

//读,由于这两个方法都是一次读取文件全部数据,所以不适合大文件
ioutil.ReadFile(filename string) ([]byte, error) 
//读,这个方法需要打开一个文件,并将文件句柄传入参数
ioutil.ReadAll(r io.Reader) ([]byte, error) 
//读取单层目录下所有文件(夹),返回FileInfo切片
ioutil.ReadDir(dirname string) ([]fs.FileInfo, error) 
//写,文件不存在则创建文件,存在则清空文件,FileMode可以直接用0666或者0777
ioutil.WriteFile(filename string, data []byte, perm os.FileMode) 

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	data, err := ioutil.ReadFile("./test.txt")
	if err != nil {
		fmt.Println(err)
	}
	//将字节数组转为string类型
	fmt.Println(string(data))
}

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	s := "hello world\nhello golang"
	err := ioutil.WriteFile("./test.txt", []byte(s), 0777)
	if err != nil {
		fmt.Println(err)
	}
}

3、bufio包

bufio 包实现了缓存IO。它包装了 io.Reader 和 io.Writer 对象,创建了另外的Reader和Writer对象,它们也实现了 io.Reader 和 io.Writer 接口,不过它们是有缓存的。

io操作本身的效率并不低,低的是频繁的访问本地磁盘的文件。所以bufio就提供了缓冲区(分配一块内存),读和写都先在缓冲区中,最后再读写文件,来降低访问本地磁盘的次数,从而提高效率。

缓冲区原理

常用方法

bufio.Reader

//NewReader 函数用来返回一个默认大小 buffer 的 Reader 对象(默认大小是 4096)
func NewReader(rd io.Reader) *Reader
//NewReaderSize 返回一个指定大小 buffer(size 最小为 16)的 Reader 对象,如果 io.Reader 参数已经是一个足够大的 Reader,它将返回该 Reader
func NewReaderSize(rd io.Reader, size int) *Reader
//Peek 返回缓存的一个切片,该切片引用缓存中前 n 个字节的数据,该操作不会将数据读出,只是引用
func (b *Reader) Peek(n int) ([]byte, error)
//Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误
func (b *Reader) Read(p []byte) (n int, err error)
//Buffered 返回缓存中未读取的数据的长度
func (b *Reader) Buffered() int
//ReadSlice 从输入中读取,直到遇到第一个界定符(delim)为止,返回一个指向缓存(也就意味着随着缓存改变而改变,所以一般用ReadBytes和ReadString)中字节的 slice,在下次调用读操作(read)时,这些字节会无效,如果在找到界定符之前缓存已经满了,ReadSlice 会返回 bufio.ErrBufferFull 错误
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
//ReadBytes 和ReadSlice区别就是返回的是当前缓存中字节slice的一个拷贝
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
//ReadString 和ReadBytes一样,只不过返回的是字符串,底层调用的是ReadBytes
func (b *Reader) ReadString(delim byte) (string, error)
//ReadLine 逐行读取,底层调用的是ReadSlice('\n')
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
//读取单个UTF-8编码的Unicode字符,并返回字符及其字节大小
func (b *Reader) ReadRune() (r rune, size int, err error) 

bufio.Writer

//返回默认缓冲大小的 Writer 对象(默认是4096),关闭文件前一定要Flush,将缓冲区的数据写入w
func NewWriter(w io.Writer) *Writer
//指定缓冲大小创建一个 Writer 对象,关闭文件前一定要Flush,将缓冲区的数据写入w
func NewWriterSize(w io.Writer, size int) *Writer
//写入单个字节,直接写入文件
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)

package main

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

func main() {
	file, err := os.Open("./test.txt")
	if err != nil {
		fmt.Println(err)
	}
	defer file.Close()
	fileBuf := bufio.NewReader(file)
	data := make([]byte, 1024)
	var result []byte
	for {
		i, err := fileBuf.Read(data)
		if err != nil && err != io.EOF {
			fmt.Println(err)
			break
		}
		if i == 0 {
			break
		}
		result = append(result, data[:i]...)
	}
	fmt.Println(string(result))
}

逐字读取utf8字符

package main

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

func main() {
	file, err := os.OpenFile("./test.txt", os.O_RDWR, 777)
	if err != nil {
		fmt.Errorf("%+v", err)
	}
	reader := bufio.NewReader(file)
	r, i, err := reader.ReadRune()
	for i > 0 && err == nil {
		fmt.Print(string(r))
		r, i, err = reader.ReadRune()
	}

	file.Close()
}

注意:关闭文件前一定要Flush()

package main

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

func main() {
	file, err := os.OpenFile("./test.txt", os.O_WRONLY|os.O_CREATE, 0777)
	if err != nil {
		fmt.Println(err)
	}
	defer file.Close()
	fileBuf := bufio.NewWriter(file)
	s := "hello world\nhello golang"
	fileBuf.Write([]byte(s))
	//关闭文件前一定要fulsh,将缓冲区内的数据写入文件
	fileBuf.Flush()
}

终端操作

package main

import "os"

func main() {
    var buf [16]byte
    os.Stdin.Read(buf[:])
    os.Stdin.WriteString(string(buf[:]))
}

临时文件

ioutil提供了两个函数: TempDir() 和 TempFile()。使用完毕后,调用者负责删除这些临时文件和文件夹。有一点好处就是当你传递一个空字符串作为文件夹名的时候,它会在操作系统的临时文件夹中创建这些项目(/tmp on Linux)。os.TempDir()返回当前操作系统的临时文件夹。

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

func main() {
	// 在系统临时文件夹中创建一个临时文件夹
	tempDirPath, err := ioutil.TempDir("", "myTempDir")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Temp dir created:", tempDirPath)

	// 在临时文件夹中创建临时文件
	tempFile, err := ioutil.TempFile(tempDirPath, "myTempFile.txt")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Temp file created:", tempFile.Name())

	// ... 做一些操作 ...

	// 关闭文件
	err = tempFile.Close()
	if err != nil {
		log.Fatal(err)
	}

	// 删除我们创建的资源
	err = os.Remove(tempFile.Name())
	if err != nil {
		log.Fatal(err)
	}
	err = os.Remove(tempDirPath)
	if err != nil {
		log.Fatal(err)
	}
}