【go笔记】文件读写


前言

文件读写在所有编程语言中都属于常见操作。

文件读取

读取文件有三种常用方法:

  • 整块读取
  • 分字节读取
  • 逐行读取

整块读取

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
    // 读取当前目录下的hello.txt
	content,err := ioutil.ReadFile("hello.txt")
	if err != nil {
		fmt.Println("read file fail:",err)
		return
	}
    // 读取到的文件字节需要手动进行类型转换
	fmt.Println(string(content))
}

分字节读取

整块读取会将文件内容整块加载到内存中。小文件还行,大文件会占用大量内存,甚至可能导致程序崩溃。

分字节可以一次读取指定数量的字节,然后进行相应的处理,再继续读取后续的文件内容,这样占用的内存将大大降低。

通过标准库的bufio实现分字节读取。

package main

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

func main() {
	file, err := os.Open("hello.txt")
	if err != nil {
        // 相当于Print()和os.Exit(1)
		log.Fatal(err)
	}

    // 关闭文件,释放资源
	defer func() {
		if err = file.Close(); err != nil {
			fmt.Println("文件操作失败: ",err)
		}
	}()

    // NewReader返回一个具有默认缓冲区大小(4096)的Reader对象
	r := bufio.NewReader(file)
    // 创建一个字节切片
	buffer := make([]byte, 6)
	fmt.Println("文件内容为: ")
    // 循环读取字节切片
	for {
		n, err := r.Read(buffer)
		if err != nil {
			break
		}
		fmt.Print(string(buffer[:n]))
	}
}
  • 分字节读取文件时,最后打印的时候应该调用string(buffer[:n]),而不是调用string(buffer),否则可能会打印出额外的字节。

逐行读取

package main

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

func main() {
	file, err := os.Open("hello.txt")
	if err != nil {
		log.Fatal(err)
	}

	defer func() {
		if err = file.Close(); err != nil {
			fmt.Println("文件操作失败: ",err)
		}
	}()

	sc := bufio.NewScanner(file)
    // 使用sc.Scan()读取文件的下一行,如果可以读取则调用Text()获取读到的内容
	for sc.Scan() {
		fmt.Println(sc.Text())
	}
	if err = sc.Err(); err != nil {
		fmt.Println("文件操作失败: ",err)	
	}
}

文件写入

整块写入

package main

import (
	"fmt"
	"os"
)

func main() {
	// os.Create() 若用于已存在的文件,会把文件内容清空,而非追加模式
	f, err := os.Create("hello.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	// f.WriteString()将返回一个写入的字节数
	content := "hello world,hello gopher"
	n, err := f.WriteString(content)
	if err != nil {
		fmt.Println(err)
		f.Close()
		return
	}
	if n > 0 {
		fmt.Println("write done")
	}
	// 关闭文件对象
	defer func() {
		if err = f.Close();err != nil {
			fmt.Println(err)
		}
	}()
}

分字节写入

和分字节读取同理,为了避免大文件耗尽内存,需要分批写入。

package main

import (
	"fmt"
	"os"
)

func main() {
	// os.Create() 若用于已存在的文件,会把文件内容清空,而非追加模式
	f, err := os.Create("hello.txt")
	// 关闭文件对象
	defer func() {
		if err = f.Close();err != nil {
			fmt.Println(err)
		}
	}()
	if err != nil {
		fmt.Println(err)
		return
	}

	content := "hello world,hello go, hello python"
	// 将文本内容转换为字节
	b := ([]byte)(content)
	// 8字节大小的切片
	var byteLen int = 8
	var i int = 0
	for {
		if i * byteLen > len(b) {
			break
		}
		// 若切片大小大于要写入的字节长度
		// 由于内容长度不一定是每批字节数的整数倍,所以对最后一段切片要单独处理
		if (i+1) * byteLen > len(b) {
			n,err := f.Write(b[i * byteLen : len(b)])
			if err != nil {
				fmt.Println(err)
				break
			}
			if n > 0 {
				fmt.Printf("1 %d 写入字节: %d \n",i,n)
			}
			// 正常切片写入
			} else {
				n, err := f.Write(b[i * byteLen : (i+1) * byteLen])
				if err != nil {
					fmt.Println(err)
					break
				}
				if n > 0 {
					fmt.Printf("2 %d 写入字节: %d \n",i,n)
				}

		}
		i++
	}
	fmt.Println("write done")
}

逐行写入

package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	// os.Create() 若用于已存在的文件,会把文件内容清空,而非追加模式
	// os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE,0644) 可设置为追加模式
	f, err := os.OpenFile("hello.txt",os.O_APPEND,0644)
	// 关闭文件对象
	defer func() {
		if err = f.Close();err != nil {
			fmt.Println(err)
		}
	}()
	if err != nil {
		fmt.Println(err)
		return
	}

	content := "hello world;hello go;hello python;hello linux"
	slice := strings.Split(content, ";")
	for _, line := range slice {
		_,err := fmt.Fprintln(f,line)
		if err != nil {
			fmt.Println(err)
			break
		}
	}

	fmt.Println("write done")
}

参考文章

  • 汪明 - 《Go并发编程》清华大学出版社