Golang中的文件编程

文件对象

对文件的操作对象是os.File

1
2
3
4
5
6
7
8
9
10
11
12
type File struct {
*file // os specific
}

type file struct {
pfd poll.FD
name string
dirinfo *dirInfo // nil unless directory being read
nonblock bool // whether we set nonblocking mode
stdoutOrErr bool // whether this is stdout or stderr
appendMode bool // whether file is opened for appending
}

对文件的操作

打开文件和关闭文件

1
2
f, _ := os.Open("1.txt") // f 是一个File指针对象
f.Close()

File拥有的方法实现了io包中的很多接口,例如以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

缓冲对象

对数据I/O接口的缓冲功能,在bufio包中,分为读缓冲和写缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}

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

对文件操作

File的操作可以直接读,也可以通过缓冲来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
f, _ := os.Open("1.txt")
defer f.Close()

// 使用缓存读
//reader := bufio.NewReader(f) // 创建缓存Reader,自带缓存大小
//for {
// str, err := reader.ReadString('\n') // 以 \n 为结尾
// if err == io.EOF { // 判断是否读完
// break
// }
// fmt.Print(str)
//}

// 不使用缓存,直接读。直接读只能读取到固定的容量,后续的容量需要再次读取。无法以固定的一个字符分割。
b := make([]byte, 1<<20) // 容量设置比较大,如果容量较小,也需要循环读
//n, err := reader.Read(b) // 从缓存读
n, err := f.Read(b) // 直接读
if err != nil && err != io.EOF {
log.Fatal(err)
}
fmt.Println(n, string(b))

或者通过ioutil包读全部取出来

1
2
f, _ := os.Open("1.txt")
b, err := ioutil.ReadAll(f) // b 是 []byte

或者

1
b, err := ioutil.ReadFile("1.txt")

从某个位置开始读,设置偏移量

1
(f *File) ReadAt(b []byte, off int64) (n int, err error) // 读取长度为b的数据

创建文件

1
2
3
os.Create("3.txt")  // 创建文件
// 或者
_, err := os.OpenFile("2.txt", os.O_APPEND|os.O_CREATE, 0444) // 以这样的权限打开文件,如果文件不存在,并且有O_CREATE,则创建文件

写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f, err := os.OpenFile("2.txt", os.O_APPEND|os.O_RDWR, 0755)    // O_APPEND 追加写,O_RDWR 可读可写,如果没有可写的flag,写入会报错
if err != nil {
log.Fatal(err)
}
defer f.Close()
writer := bufio.NewWriter(f) // 创建写缓存
_, err = writer.WriteString("abc") // 将string写入缓存
if err != nil {
log.Fatal(err)
}
err = writer.Flush() // 当缓存不满,需要手动将缓存刷到硬盘
if err != nil {
log.Fatal(err)
}

读写模式

1
2
3
4
5
6
7
8
9
10
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only. // 只读
O_WRONLY int = syscall.O_WRONLY // open the file write-only. // 只写
O_RDWR int = syscall.O_RDWR // open the file read-write. // 读写
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing. // 追加写
O_CREATE int = syscall.O_CREAT // create a new file if none exists. // 文件不存在则创建
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. // 和O_CREATE一起使用,文件必须不存在则创建
O_SYNC int = syscall.O_SYNC // open for synchronous I/O. // 同步I/O
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. // 覆盖写入

拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f1 := "1.txt"
f2 := "2.txt"
data, err := ioutil.ReadFile(f1) // 将数据全部读取出来
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(f2, os.O_CREATE|os.O_WRONLY, 0766)
if err != nil {
log.Fatal(err)
}
defer file.Close()
_, err = file.Write(data) // 写入
if err != nil {
log.Fatal(err)
}

通过缓存拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
f1 := "1.txt"
f2 := "2.txt"
file1, err := os.Open(f1)
if err != nil {
log.Fatal(err)
}
defer file1.Close()
reader := bufio.NewReader(file1)

file2, err := os.OpenFile(f2, os.O_CREATE|os.O_WRONLY, 0766)
if err != nil {
log.Fatal(err)
}
defer file2.Close()

// 第一种方法,使用缓存
//bufio.NewWriter(file2)
//_, err = reader.WriteTo(file2)
//if err != nil {
// log.Fatal(err)
//}

// 第二种方法
writer := bufio.NewWriter(file2)
_, err = io.Copy(writer, reader)
if err != nil {
log.Fatal(err)
}

或者使用io.util

1
2
3
4
5
bytes, err := ioutil.ReadFile(f1)
if err != nil {
log.Fatal(err)
}
ioutil.WriteFile(f2, bytes, 0755)

判断数字、英文、符号、汉字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
f, _ := os.Open("1.txt")
defer f.Close()

reader := bufio.NewReader(f)
var han int
var english int
var num int
var other int
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
for _, s := range str {
switch {
case unicode.Is(unicode.Han, s):
han++
case unicode.IsLetter(s):
english++
case unicode.IsNumber(s):
num++
default:
other++

}
}
}
fmt.Printf("han %d,english %d,num %d,other %d\n", han, english, num, other)

通过unicode包可以判断字符属性。

判断文件状态等属性

1
2
3
4
5
stat, err := os.Stat("100.txt")
if os.IsNotExist(err) {
log.Fatal("file not exist")
}
fmt.Println(stat.Name(), stat.IsDir(), stat.Mode(), stat.Size(), stat.ModTime())

如果文件存在,打印文件名称,是否是目录,文件权限,文件大小,修改时间

1
1.txt false -rw-r--r-- 1171 2022-08-01 17:54:06.072619357 +0800 CST

删除文件

1
os.Remove("1.txt") // 如果文件不存在,则会报错

对目录操作

创建目录

1
os.Mkdir("123", 0666)

重命名

1
os.Rename("313", "abc")

列举文件夹中的文件

1
2
3
4
5
6
7
8
9
files, _ := ioutil.ReadDir("file")
for _, file := range files {
if file.IsDir() {
fmt.Print("目录")
} else {
fmt.Print("文件")
}
fmt.Println(file.Name())
}

便利目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func getFiles(path string, deep string) {
files, _ := ioutil.ReadDir(path)
emp := ""
for i := 0; i < len(deep); i++ {
emp += " "
}
deep += "-"
for _, file := range files {
if file.IsDir() {
fmt.Println(emp + "|" + deep + string(os.PathSeparator) + file.Name())
getFiles(path+string(os.PathSeparator)+file.Name(), deep)
} else {
fmt.Println(emp + "|" + deep + file.Name())
}
}
}

删除目录

1
os.RemoveAll("456")

获取程序运行的目录

1
os.Getwd()

修改目录

1
os.Chdir("727")

创建多层级目录

1
os.MkdirAll("a/b/c", 0777)

操作绝对路径

1
filepath.Abs("abc")

ioutil包

1
2
3
4
5
6
7
ReadDir(dirname string) ([]fs.FileInfo, error)    // 便利目录下的所有文件
ReadFile(filename string) ([]byte, error) // 读取文件内容
ReadAll(r io.Reader) ([]byte, error) // 读取Reader的所有内容
WriteFile(filename string, data []byte, perm fs.FileMode) error // 将data写入文件,当文件不存在则创建
TempFile(dir string, pattern string) (f *os.File, err error) // 创建以这个前缀为名的临时文件
TempFile(dir string, pattern string) (f *os.File, err error) // 创建以这个前缀为名的临时目录
NopCloser(r io.Reader) io.ReadCloser // 将r包装为一个ReadCloser类型

读取内存

1
2
3
4
s := "abc"
reader := strings.NewReader(s)
bytes, _ := ioutil.ReadAll(reader)
fmt.Println(string(bytes))

I/O操作

输出到Stdout

1
2
fmt.Fprintln(os.Stdout, "abc")
os.Stdout.WriteString("abc")

或者使用缓存

1
2
3
4
5
6
7
buffer := bytes.NewBufferString("abc") // 读取内存
fmt.Fprintln(buffer, "def")
fmt.Println(buffer.String())

writer := bufio.NewWriter(os.Stdout)
writer.WriteString("anc")
writer.Flush()

读取

从输入中读取

1
2
3
4
var num int
fmt.Fscan(os.Stdin, &num)
fmt.Scanln(&num)
fmt.Println(num)

这里有个共同的特点,都是操作io.Writerio.Reader

读取文件内容到内存

1
2
3
4
5
6
7
8
9
10
11
12
f, err := os.Open("2.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()

buf := make([]byte, 10) // 只读10个byte的内容
n, err := f.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Println(n, string(buf))

ReadByte:读一个字节

1
2
3
4
5
6
reader := strings.NewReader("abc")
readByte, err := reader.ReadByte()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(readByte))

从文件的某个位置开始读,修改文件指针。在断点续传的时候就可以通过Seek实现。

1
2
3
4
5
6
7
func (f *File) Seek(offset int64, whence int) (ret int64, err error)

// 例如
file.Seek(2,io.SeekStart) // 从开始偏移2个字节
// SeekStart = 0 // seek relative to the origin of the file
// SeekCurrent = 1 // seek relative to the current offset
// SeekEnd = 2 // seek relative to the end

断点续传原理就是通过一个临时文件,记录偏移量,实现记录断点。

例如文件A,文件B和一个临时文件C。

当开始将文件A拷贝到文件B,创建临时文件C;从文件A读取一段内容,写入文件B,写入成功,将写入字节数记录到临时文件C中。断电时,先获取临时文件C的内容,获得已拷贝容量,通过Seek移动偏移量到读取文件和写入文件的位置,继续读取文件A,写入文件B,写入之后,将写入的总字节数相加,记录到文件C。

缓存操作,ReadBytes

1
2
3
4
5
6
7
8
9
reader := strings.NewReader("abc")
newReader := bufio.NewReader(reader) // 使用缓存
readByte, err := newReader.ReadBytes('b')
if err != nil && err != io.EOF {
log.Fatal(err)
}
fmt.Println(string(readByte))
reader.Reset("abc") // 回到开头
newReader.Reset(reader) // 回到缓冲的开头

读一行

1
2
3
4
5
6
7
    s := `abc
def
ghi`
reader := strings.NewReader(s)
newReader := bufio.NewReader(reader)
line, _, _ := newReader.ReadLine()
fmt.Println(string(line))

读取一个中文字符,Rune

1
line, _, _ := newReader.ReadRune()

读取分段,通过\n分割

1
newReader.ReadSlice('\n')

直接读出字符串,通过\n分割

1
newReader.ReadString('\n')

获取缓冲区的可读字节数

1
newReader.Buffered()

按照某个字节长度读取缓冲区的数据,但是不会从缓冲区将数据拿出来

1
newReader.Peek(2)        // 取出2个字节,如果超过缓冲区可读长度或者超过缓冲区总长度,都会报错

写入

创建一个writer

1
2
3
4
5
wr := bytes.NewBuffer(nil)            // 创建一个缓冲区,如果nil是[]byte(),则缓冲区已存在该数据
writer := bufio.NewWriter(wr) // 创建缓存写对象
writer.WriteString("abc\n") // 在缓存中写入字符串
writer.Flush() // 将缓存中的数据写入到缓冲区wr
fmt.Println(wr.String()) // 获取缓冲区数据

写入

1
2
3
4
Write(p []byte) (nn int, err error)                // 写入字节切片
WriteString(s string) (int, error) // 写入字符串
WriteByte(c byte) error // 写入字符型
WriteRune(r rune) (size int, err error) // 写入ASC码型

获取缓存状态

1
2
3
writer.Available()        // 获取可缓存字节数
writer.Buffered() // 获取已缓存字节数
writer.Size() // 获取缓存总字节数

将缓存中的数据写入底层io.WriterFlush

1
2
3
fmt.Println(writer.Available(), writer.Size())        // 4092 4096 
writer.Flush() // 写入io.Writer之后,缓存区为空
fmt.Println(writer.Available(), writer.Size()) // 4096 4096

将数据内容进行稳定存储,刷到硬盘中

1
func (f *File) Sync() error

二进制读写

通过encoding/gob包实现二进制编码和解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
file, _ := os.OpenFile("110.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
defer file.Close()
type User struct {
Name string
Age int
}
u := User{
Name: "mitaka",
Age: 18,
}
buffer := bytes.NewBuffer(nil)
encoder := gob.NewEncoder(buffer)
encoder.Encode(u)
buffer.WriteTo(file)

解码

1
2
3
4
5
6
7
8
9
10
11
file, _ := os.OpenFile("110.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
defer file.Close()
type User struct {
Name string
Age int
}
u := User{}
reader := bufio.NewReader(file)
decoder := gob.NewDecoder(reader)
decoder.Decode(&u)
fmt.Println(u)

在操作很大的文件时,例如GB以上文档,需要统计文档中的内容,例如统计行数,可以通过bufio包的功能实现

统计文件行数

1
2
3
4
5
6
7
8
9
file, _ := os.Open("1.json")
defer file.Close()

scanner := bufio.NewScanner(file)
line := 0
for scanner.Scan() {
line++
}
fmt.Println(line)

压缩和打包

在源码包中的compress目录中,有gzipbzip2lzw等压缩算法和压缩实现方式,这里主要实现zip的使用:

文件压缩和解压缩可以通过源码中的archive/zip实现

雅座

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 file, _ := os.Create("1.zip")                            // 创建压缩文件
defer file.Close()

zipW := zip.NewWriter(file) // 压缩文件写入句柄
defer zipW.Close()

f1, _ := os.Stat("2.txt")
header, _ := zip.FileInfoHeader(f1) // 写入压缩文件header信息,如果没有,压缩时为空文件
header.Name = f1.Name() + "use zip"
header.Method = zip.Deflate

f2, _ := os.Open("2.txt") // 获取被压缩的文件Reader
defer f2.Close()

writer, _ := zipW.CreateHeader(header) // 获取压缩包的Writer
io.Copy(writer, f2) // 压缩

f3, _ := os.Open("3.txt") // 另一个文件
defer f3.Close()

f3info, _ := f3.Stat()
header, _ = zip.FileInfoHeader(f3info)
header.Name = f3info.Name() + "f3 zip"
header.Method = zip.Deflate

writer, _ = zipW.CreateHeader(header)
io.Copy(writer, f3)

解压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
reader, _ := zip.OpenReader("1.zip")                // 获取压缩文件Reader对象
defer reader.Close()

outputPath := "out2" // 解压路径
_, err := os.Stat(outputPath)
if os.IsNotExist(err) {
os.MkdirAll(outputPath, 0766) // 解压路径需要可写入权限和执行权限
}
for _, f := range reader.File { // 便利压缩包中的文件
fpath := filepath.Join(outputPath, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(outputPath)+string(os.PathSeparator)) {
log.Fatal("illegal file path: ", fpath) // 判断路径是否合法
}
if f.FileInfo().IsDir() {
os.Mkdir(fpath, f.Mode()) // 如果是目录,则创建子目录
continue
}
outfile, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_RDWR, f.Mode()) // 新建空白文件
if err != nil {
log.Fatal(err)
}
defer outfile.Close()
rc, err := f.Open() // 将压缩包中的文件打开
if err != nil {
log.Fatal(err)
}
defer rc.Close()
_, err = io.Copy(outfile, rc) // 将压缩包中的文件拷贝到空白文件中
if err != nil {
log.Fatal(err)
}
}

通过tar打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
t, err := os.Create("2.tar")                        // 创建tar包
if err != nil {
log.Fatal(err)
}
defer t.Close()
tw := tar.NewWriter(t) // 获取tar包Writer对象
defer tw.Close()

f1, err := os.Open("1.zip") // 获取被tar的文件句柄
if err != nil {
log.Fatal(err)
}
defer f1.Close()

info1, err := f1.Stat()
if err != nil {
log.Fatal(err)
}
header1, err := tar.FileInfoHeader(info1, "") // 获取被tar文件的header信息
if err != nil {
log.Fatal(err)
}
err = tw.WriteHeader(header1) // 将被tar文件的header信息写入tar文件writer对象
if err != nil {
log.Fatal(err)
}
io.Copy(tw, f1) // tar操作

解打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
t, err := os.Open("2.tar")                                // 获取untar对象句柄
if err != nil {
log.Fatal(err)
}
defer t.Close()
tr := tar.NewReader(t) // 创建untar Reader对象

for hdr, err := tr.Next(); err != io.EOF; hdr, err = tr.Next() { // 便利
if err != nil {
log.Fatal(err)
return
}
fileinfo := hdr.FileInfo()
file, err := os.Create(fileinfo.Name())
if err != nil {
log.Fatal(err)
return
}
defer file.Close()
io.Copy(file, tr) // 解打包
}

各项格式的文件读写

xml,原生encoding/xml包支持xml读写。

写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var u = User{
Name: "mitaka",
Age: 18,
Describe: "这是一个人",
}

file, err := os.OpenFile("1.xml", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
log.Fatal(err)
}
defer file.Close()

encoder := xml.NewEncoder(file) // 创建编码器
err = encoder.Encode(u) // 编码
if err != nil {
log.Fatal(err)
}

写入之后,文件格式化之后:

1
2
3
4
5
<User>
<Name>mitaka</Name>
<Age>18</Age>
<Describe>这是一个人</Describe>
</User>

读取xml文件

1
2
3
4
5
6
7
8
9
10
11
12
file, err := os.Open("1.xml")
if err != nil {
log.Fatal(err)
}
defer file.Close()
var u User
decoder := xml.NewDecoder(file) // 创建解码器
err = decoder.Decode(&u) // 解码
if err != nil {
log.Fatal(err)
}
fmt.Println(u)

json编码和解码的使用方式,与xml一致,使用golang自带的序列化包即可。

xml、二进制格式的文件解析,则需要引入第三方包。

操作文件内容

读写文件内容的作用之一是可以对内容进行操作

字符串操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s := "[email protected]"
println(strings.Contains(s, "123.com1")) // 判断字符串是否存在,返回bool
split := strings.Split(s, "@") // 将字符串分割成数组,用@分割
fmt.Println(split)
println(strings.Join(split, "%%")) // 将数组组合成字符串,用%%连接起来
println(strings.Index(s, "123.com")) // 查询字符出现的索引,-1代表没找到
println(strings.Repeat("Golang", 4)) // 字符串重复
println(strings.Replace(s, "123.com", "163.com", -1)) // 将字符串中的字符替换成其他,-1代表替换所有,大于0代表个数
println(strings.ReplaceAll(s, "c", "%^")) // 将字符串中所有的字符都替换
println(strings.Trim(s, "a")) // 将字符串开头和结尾的字符去掉
println(strings.Trim(" abc ", " ")) // 去掉了开头和结尾的空格,返回abc
fmt.Println(strings.Fields("a abc ")) // 将字符串通过空格分割成切片
println(strings.HasPrefix("abc", "ab")) // 判断字符串是否有ab的前缀
println(strings.HasSuffix("abc", "bc")) // 判断字符串是否有bc的后缀

拼音模糊匹配

1
go get github.com/axgle/pinyin
1
println(pinyin.Convert("hello,世界!"))      // hello,ShiJie!

解决中文乱码

出现中文乱码的原因一般是因为字符编码有问题导致,主要是GBK和UTF8编码导致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// GBK to UTF8
func GbkToUtf8(s []byte) ([]byte, error) {
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
d, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
return d, err
}

// UTF8 to GBK
func Utf8ToGbk(s []byte) ([]byte, error) {
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewEncoder())
d, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
return d, err
}

推荐阅读:

bufio包系列之读取原理