golang文件IO操作总结

Go语言中的文件与IO操作也是很丰富的,但是相关操作分散在多个包中,这里就简单总结一下

文件

这一部分主要在os包内

创建文件

func main(){
	f,err:= os.Create("t.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()
}

主要使用os.Create方法,创建一个空文件,如果文件已存在,会覆盖,最后还是会得到一个空文件

修改文件大小

os.Truncate("t.txt",10)

单位是字节,如果文件不存在会创建文件。查看源码发现内部调用的实际是file的Truncate方法。

文件信息

func main(){
	fileInfo, err := os.Stat("t.txt")
	if err != nil {
		log.Fatal(err)
	}
}

主要利用stat方法,返回一个FileInfo对象,表示文件信息,个FileInfo是一个接口提供以下信息

type FileInfo interface {
	Name() string       // 文件名
	Size() int64        // 文件大小
	Mode() FileMode     // 文件读写模式
	ModTime() time.Time // 最近修改时间
	IsDir() bool        // 是否是目录
	Sys() interface{}   // 基础数据
}

由于stat在文件不存在时会返回错误,所以一般可以用来判断文件是否存在

移动

os.Rename("t.txt","s.txt")

既可以重命名还可以移动文件。注意如果新路径已经存在会被覆盖。当然如果旧文件不存在会报错

删除

os.Remove("s.txt")

还有一个RemoveAll方法用于删除一个路径下所有文件

打开和关闭文件

func main(){
	file,err:=os.Open("t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
}

默认以只读的方式打开文件,文件不存在时会报错,利用defer机制关闭文件

func main(){
	file,err:=os.OpenFile("s.txt",os.O_APPEND|os.O_CREATE,0666)
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
}

这是一种比较开放式的方法,可以指定打开的方法以及权限。第二个参数常用的参数如下

const (
	O_RDONLY int = syscall.O_RDONLY // 只读
	O_WRONLY int = syscall.O_WRONLY // 只写
	O_RDWR   int = syscall.O_RDWR   // 可读写

	O_APPEND int = syscall.O_APPEND // 以追加形式写
	O_CREATE int = syscall.O_CREAT  // 文件不存在时创建
	O_EXCL   int = syscall.O_EXCL   // 和O_CREATE一起使用,文件不能存在
	O_SYNC   int = syscall.O_SYNC   // 以同步IO模式打开
	O_TRUNC  int = syscall.O_TRUNC  // 打开时裁剪文件
)

第三个参数在只读时可以直接传0,写的时候要指定文件权限,如0666。

权限

err := os.Chmod("test.txt", 0777)

就是linux的chmod命令。使用时可以利用打开文件的错误来检查读写权限。

func main(){
	os.Chmod("s.txt",0400)
	file,err:=os.OpenFile("s.txt",os.O_WRONLY,0666)
	if err != nil {
		if os.IsPermission(err) {
			fmt.Println("没有写权限")
		}
	}
	defer file.Close()
}

改变文件属性

os.Chown("s.txt", os.Getuid(), os.Getgid()) //改变所有者
//修改最后访问和修改时间
os.Chtimes("test.txt", time.Now().Add(2*time.Minute), time.Now().Add(2*time.Minute)) 

读写

涉及io、os包中的许多方法

复制文件

func main(){
	old,err:=os.Open("s.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer old.Close()

	new,err:=os.Create("s_copy.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer new.Close()

	len,err:=io.Copy(new,old)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("copied %d bytes",len)
}

Copy实际上是操作两个分别实现了读写接口的对象。

跳转到文件指定位置

func main(){
	file,err:=os.Open("s.txt")
	if err!=nil {
		log.Fatalln(err)
	}
	defer file.Close()

	pos,err:=file.Seek(2,0)
	if err!=nil {
		log.Fatalln(err)
	}
	fmt.Println(pos)
}

seek接受两个参数,第一个是offset,表示偏移量。第二个是whence,表示从哪里开始:0:表示文件开始位置;1:表示当前位置;2:表示文件结尾

  • file.Seek(0, 1) 表示文件当前位置
  • file.Seek(0, 0) 表示文件开始位置

写文件

func main(){
	file,err:=os.OpenFile("s.txt",os.O_RDWR,0666)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	_,err=file.Write([]byte("hello world"))
	if err != nil {
		log.Fatal(err)
	}
}

另外在ioutil包中还有一个更方便的方法

ioutil.WriteFile("s.txt",[]byte("hello"),0666)

带缓存的写

一般情况下我们可以自己定义一个buffer用来缓存要写入的数据,等数据够多时一次写入,这样可以提高IO性能,不过标准包里已经给我们封装好了

func main(){
	file,err:=os.OpenFile("t.txt",os.O_CREATE|os.O_RDWR,0666)
	if err!=nil {
		log.Fatalln(err)
	}
	defer file.Close()
	buffer := bufio.NewWriter(file)

	len,err:=buffer.Write([]byte("hello"))
	if err!=nil {
		log.Fatalln(err)
	}
	fmt.Printf("写入 %d 字节\n", len)

	len,err=buffer.WriteString("world")
	if err!=nil {
		log.Fatalln(err)
	}
	fmt.Printf("写入 %d 字节\n", len)

	fmt.Printf("已缓存 %d 字节\n",buffer.Buffered())

	fmt.Printf("还有 %d 字节可用\n",buffer.Available())

	err=buffer.Flush()
	if err!=nil {
		log.Fatalln(err)
	}
}

此外bufio的Writer还有一个reset方法,他接受一个实现Writer接口的对象,然后将buffer中的内容清空,后续的写入都将写到传入的对象中。

bufio的默认缓存长度为4096,可以使用bufio.NewWriterSize去指定。更多详细内容见后文源码分析

读文件

func main(){
	file,err:=os.Open("t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
	b:=make([]byte,5)
	n,err:=file.Read(b)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Printf("读了%d个字节",n)
	fmt.Println(string(b))
}

read方法需要传入一个切片,由于切片长度限制所以最多可以读若干的字节

func main(){
	file,err:=os.Open("t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
	b:=make([]byte,12)
	n,err:=io.ReadFull(file,b)
	if err != nil {
		
	}
	fmt.Printf("读了%d个字节",n)
	fmt.Println(string(b))
}

ReadFull则是要求将切片填满,即最少读多少字节,若不够则会返回错误,但还是会读取到内容。类似的还有func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error),也是指定最少要读的字节数,否则会报错,但是已读内容会保留。

func main(){
	file,err:=os.Open("t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
	data,err:=ioutil.ReadAll(file)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(data))
}

ReadAll是一次读取所有内容

func main(){
	data,err:=ioutil.ReadFile("t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(data))
}

这是专门针对文件设计的一次读取全部内容

带缓存读

func main(){
	file, err := os.Open("t.txt")
	if err != nil {
		log.Fatal(err)
	}
	buffer := bufio.NewReader(file)
	bytes, _ := buffer.Peek(5)
	fmt.Println(string(bytes))


	b,_:=buffer.ReadByte()
	fmt.Println(string(b))
	buf := make([]byte,5)
	buffer.Read(buf)
	fmt.Println(string(buf))

	fmt.Println(buffer.ReadString(byte('r')))
}

注意和几个方法的区别,Peek是读取指定数量的内容,但是读完指针不会变。剩余几个Read方法指针会随着读取后移,ReadString以及未提到的ReadBytes都是从当前指针位置开始读到指定位置为止。

scanner

这是bufio包中的类,它读取时比较特使是按照给定的分隔符读取的,类似于split的功能

func main(){
	file, err := os.Open("t.txt")
	if err != nil {
		log.Fatal(err)
	}
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanWords)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
	err = scanner.Err()
	if err == nil {
		fmt.Println("读取完成")
	} else {
		log.Fatalln(err)
	}
}

上面我们按照空格分隔,循环读取,直到完成。用法的关键是Split方法的参数,他需要一个SplitFunc类型,用来指定划分方法

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

例子中我们用了包中定义好的方法,以空格分隔

func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
	// Skip leading spaces.
	start := 0
	for width := 0; start < len(data); start += width {
		var r rune
		r, width = utf8.DecodeRune(data[start:])
		if !isSpace(r) {
			break
		}
	}
	// Scan until space, marking end of word.
	for width, i := 0, start; i < len(data); i += width {
		var r rune
		r, width = utf8.DecodeRune(data[i:])
		if isSpace(r) {
			return i + width, data[start:i], nil
		}
	}
	// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
	if atEOF && len(data) > start {
		return len(data), data[start:], nil
	}
	// Request more data.
	return start, nil, nil
}

这个逻辑也很简单,首先跳过开头的空格,然后从第一个不是空格位置开始扫描,直到遇到下一个空格为止,返回下一次扫描时起始位置、本次扫描的有效数据。根据这个例子我们可以很简单的修改为其他分隔符功能

其他操作

打包与解包

func main(){
	out,err:=os.Create("test.zip")
	if err!=nil{
		log.Fatalln(err)
	}
	defer out.Close()

	zipWriter := zip.NewWriter(out)
	defer zipWriter.Close()
	m:=make(map[string]string)
	m["t.txt"] = "aaa"
	m["tt.txt"] = "bbb"

	for name,body:=range m{
		file,err:=zipWriter.Create(name)
		if err!=nil{
			log.Fatalln(err)
		}
		_,err=file.Write([]byte(body))
		if err!=nil{
			log.Fatalln(err)
		}
	}
}

流程很简单,就是创建一个文件,然后打开一个与该文件相关的写入对象,然后向里面写入文件,每次写入文件都要手动创建并写入内容。解包如下

func main(){
	zipreader,err:=zip.OpenReader("test.zip")
	if err != nil {
		log.Fatal(err)
	}
	defer  zipreader.Close()

	for _,file:=range zipreader.Reader.File{
		zipfile,err:=file.Open()
		if err != nil {
			log.Fatal(err)
		}
		defer zipfile.Close()
		if file.FileInfo().IsDir() {
			err=os.MkdirAll(file.Name, file.Mode())
			if err != nil {
				log.Fatal(err)
			}
		}else{
			out,err:=os.OpenFile(file.Name,os.O_CREATE|os.O_RDWR|os.O_TRUNC,0666)
			if err != nil {
				log.Fatal(err)
			}
			defer out.Close()
			_,err=io.Copy(out,zipfile)
			if err != nil {
				log.Fatal(err)
			}
		}
	}
}

解包会稍微麻烦一点,先用zip包打开要解包的文件,获得一个reader对象,然后他的Reader.File字段保存着所有文件,需要先用open方法打开,然后在判断是否是文件夹,在确定正确路径,真正的解包是将遍历到的文件使用copy方法复制出来即可。

其他解压缩操作

除了提供zip类型文件操作,标准包还提供了targzipzlib, bz2, flate, lzw支持,而且都有例子,这里就不一一举例了。

临时目录及文件

ioutil还另外提供了两个与临时目录及临时文件有个的方法:TempDir()TempFile()

func main(){
	tempDir,err:=ioutil.TempDir("","tempdir")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("创建临时文件夹:",tempDir)

	tempfile,err:=ioutil.TempFile(tempDir,"t.txt")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("创建临时文件:",tempfile.Name())
	defer func() {
		tempfile.Close()
		os.Remove(tempfile.Name())
		os.Remove(tempDir)
	}()

	n,err:=tempfile.WriteString("hello world")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("写入字节数:",n)

	b,err:=ioutil.ReadFile(tempfile.Name())
	fmt.Println("读取字节数:",n)
	fmt.Println(string(b))
}

不同的操作系统由不同的临时目录,可以通过os.TempDir()查看

下载文件

func main(){
	file,err:=os.Create("pic.jpg")
	if err!=nil{
		log.Fatalln(err)
	}
	defer file.Close()

	resp,err:=http.Get("http://b.hiphotos.baidu.com/image/pic/item/908fa0ec08fa513db777cf78376d55fbb3fbd9b3.jpg")
	defer resp.Body.Close()

	n,err:=io.Copy(file,resp.Body)
	if err!=nil{
		log.Fatalln(err)
	}

	fmt.Println("下载字节数:",n)
}

前面说过,copy操作的是两个实现读写接口的对象,所以也可以用来下载文件。

bufio.writer源码

这个实现比较简单,我们简要看一下源码实现

构造

defaultBufSize = 4096

func NewWriter(w io.Writer) *Writer {
	return NewWriterSize(w, defaultBufSize)
}

func NewWriterSize(w io.Writer, size int) *Writer {
	b, ok := w.(*Writer)
	if ok && len(b.buf) >= size {
		return b
	}
	if size <= 0 {
		size = defaultBufSize
	}
	return &Writer{
		buf: make([]byte, size),
		wr:  w,
	}
}

type Writer struct {
	err error
	buf []byte
	n   int
	wr  io.Writer
}

构造比较简单,最终是创建一个Writer对象,Writer中封装了一个字节切片用来缓存,还封装了我们传入的writer对象,用来最后实际写。切片的默认长度是4096。另外还有一个整数n用来记录已缓存量。

Available & Buffered

func (b *Writer) Available() int { return len(b.buf) - b.n }

func (b *Writer) Buffered() int { return b.n }

Available就是总长度减去已写入长度,Buffered就是返回已写入长度

reset

func (b *Writer) Reset(w io.Writer) {
	b.err = nil
	b.n = 0
	b.wr = w
}

重置Writer,就是将Writer的几个成员根据情况初始化

write

func (b *Writer) Write(p []byte) (nn int, err error) {
	for len(p) > b.Available() && b.err == nil {
		var n int
		if b.Buffered() == 0 {
			n, b.err = b.wr.Write(p)
		} else {
			n = copy(b.buf[b.n:], p)
			b.n += n
			b.Flush()
		}
		nn += n
		p = p[n:]
	}
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], p)
	b.n += n
	nn += n
	return nn, nil
}

首先考虑要写入内容大于可用空间的情况。此时,如果已缓存长度为0,表示这个缓存总长度就是0,所以所有内容直接写入指定的writer。否则,先将缓存填满,并将缓存中的内容写入writer。之后判断剩余的内容是否超过可用空间,是的话循环前面操作。如果可以写入,则直接写入,同时已写入长度标记n对应增长。

func (b *Writer) WriteByte(c byte) error {
	if b.err != nil {
		return b.err
	}
	if b.Available() <= 0 && b.Flush() != nil {
		return b.err
	}
	b.buf[b.n] = c
	b.n++
	return nil
}

写入单个字符就比较简单了,只要可用空间不为0就可以写入,否则就先flush在写入

func (b *Writer) WriteString(s string) (int, error) {
	nn := 0
	for len(s) > b.Available() && b.err == nil {
		n := copy(b.buf[b.n:], s)
		b.n += n
		nn += n
		s = s[n:]
		b.Flush()
	}
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], s)
	b.n += n
	nn += n
	return nn, nil
}

字符串本质上也是一个字节数组,和写入一个字节数组的方法类型

func (b *Writer) WriteRune(r rune) (size int, err error) {
	if r < utf8.RuneSelf {
		err = b.WriteByte(byte(r))
		if err != nil {
			return 0, err
		}
		return 1, nil
	}
	if b.err != nil {
		return 0, b.err
	}
	n := b.Available()
	if n < utf8.UTFMax {
		if b.Flush(); b.err != nil {
			return 0, b.err
		}
		n = b.Available()
		if n < utf8.UTFMax {
			return b.WriteString(string(r))
		}
	}
	size = utf8.EncodeRune(b.buf[b.n:], r)
	b.n += size
	return size, nil
}

写入一个rune类型数据有些不同。首先判断传入的值是否仅仅是一个字节(r < utf8.RuneSelf),是的话调用WriteByte。否则,先判断可用空间是否小于UTFMax(4个字节),是的话要先将缓存内容flush,然后在判断,若还不够则转为string类型写入。若可以写入则调用EncodeRune将rune类型写入字节数组。

flush

func (b *Writer) Flush() error {
	if b.err != nil {
		return b.err
	}
	if b.n == 0 {
		return nil
	}
	n, err := b.wr.Write(b.buf[0:b.n])
	if n < b.n && err == nil {
		err = io.ErrShortWrite
	}
	if err != nil {
		if n > 0 && n < b.n {
			copy(b.buf[0:b.n-n], b.buf[n:b.n])
		}
		b.n -= n
		b.err = err
		return err
	}
	b.n = 0
	return nil
}

这就是实际写入writer的方法,首先检查写入缓存过程中是否有错,有错的话直接返回错误。之后看是否有数据要写入,有的话调用writer的write的方法一次写入一个数组,接着对具体错误做出处理即可。这里额外考虑了写入的长度不是我们给的长度的情况。另外对于有错的话,对于那些未写入的内容还要予以保留。

题图来自unsplash:https://unsplash.com/photos/salaAJW7Xdg