16.目录与文件

16.目录与文件

    任何编程语言在运行时都依赖于操作系统,所以程序在运行时会对系统文件、目录等执行一些相应操作,例如文件读写、执行系统命令、创建新的目录等。在Go语言中,可以使用内置包os实现了多种操作系统的操作指令,如主机、用户、进程、环境变量、目录与文件操作、终端执行等。

16.1 路径操作

    存储设备保存着各种各样的数据,但需要一种方便快捷的模式让用户可以快速定位到相应数据资源的位置,而操作系统则提供一种基于路径字符串的表达式来解决此类问题,它像一棵倒置的层级目录树,从根开始。

  • 相对路径:不是以根目录开始的路径,例如:../xx、./xx
  • 绝对路径:以斜杆做为开始的路径,例如:/home/、/var/log、C:\windows
  • 路径分隔符:不同的操作系统的路径分隔符也不太一样,Windows的分隔符为 \,而Linux系统则为 /

    为了方便处理,Go语言标准库提供path包和path/filepath来处理路径问题。而path/filepathpath的扩展,使用起来更加方便,一般使用path/filepath即可

16.1.1 路径拼接

    由于路径是字符串,直接使用字符串拼接即可完成,另外也可以使用join方法,示例如下所示:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	pathA := "C:" + "\\surpass" + "\\" + "data"
	pathB := filepath.Join("C:", "\\surpass", "\\", "data")
	fmt.Printf("pathA: %+v\n", pathA)
	fmt.Printf("pathB: %+v\n", pathB)
}

    代码运行结果如下所示:

pathA: C:\surpass\data
pathB: C:\surpass\data

16.1.2 路径分解

    在一些场景下,我们仅需要路径中的一部分数据,这时候则可以使用路径分解功能,示例代码如下所示:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	path := filepath.Join("C:", "\\surpass", "\\", "data", "\\surpass.json")
	// 拆解出路径和文件名
	dir, file := filepath.Split(path)
	fmt.Printf("路径为:%+v,文件名为:%+v\n", dir, file)
	// 功能类似于dirname 和 basename
	fmt.Printf("路径为:%+v,文件名为:%+v,扩展名为:%+v\n", filepath.Dir(path), filepath.Base(path), filepath.Ext(path))
}

    代码运行结果如下所示:

路径为:C:\surpass\data\,文件名为:surpass.json
路径为:C:\surpass\data,文件名为:surpass.json,扩展名为:.json

16.1.3 其他功能

    示例代码如下所示:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	pathA := filepath.Join("C:", "\\surpass", "\\", "data", "\\surpass.json")
	pathB := filepath.Join("..", "\\surpass", "\\data")
	// 判断是否为绝对路径
	fmt.Printf("pathA是否为绝对路径:%+v,pathB是否为绝对路径:%+v\n", filepath.IsAbs(pathA), filepath.IsAbs(pathB))

	// 在使用Clean函数后,返回等价的最短路径
	// 清除当前路径中的 . 和 ..
	// 使用Join方法时,会自动调用Clean方法
	pathC := "\\surpass\\test\\data\\" + "..\\.." + "\\test\\" + "\\code"
	pathD := filepath.Join("\\surpass\\test\\data\\", "..\\..", "\\test", "\\code")
	fmt.Printf("原始pathC:%+v,调用Clean方法之后:%+v\n", pathC, filepath.Clean(pathC))
	fmt.Printf("调用Join方法之后pathD:%+v\n", pathD)

	// 路径匹配
	pathE := filepath.Join("data-1")
	// 匹配0个或多个非 / 字符
	if matchE, err := filepath.Match("*", pathE); err == nil {
		fmt.Printf("pathE: %+v,匹配结果:%+v\n", pathE, matchE)
	}
	if matchF, err := filepath.Match("data-[0-9]", pathE); err == nil {
		fmt.Printf("pathE: %+v,匹配结果:%+v\n", pathE, matchF)
	}
}

    代码运行结果如下所示:

pathA是否为绝对路径:true,pathB是否为绝对路径:false
原始pathC:\surpass\test\data\..\..\test\\code,调用Clean方法之后:\surpass\test\code
调用Join方法之后pathD:\surpass\test\code
pathE: data-1,匹配结果:true
pathE: data-1,匹配结果:true

16.2 目录操作

16.2.1 常规操作

    示例代码如下所示:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 获取当前目录
	if curPath, err := os.Getwd(); err == nil {
		fmt.Printf("当前目录:%+v\n", curPath)
	}

	// 获取当前家目录
	if userDir, err := os.UserHomeDir(); err == nil {
		fmt.Printf("家目录:%+v\n", userDir)
	}

	// 切换目录路径
	os.Chdir("F:\\")
	if curPath, err := os.Getwd(); err == nil {
		fmt.Printf("当前目录:%+v\n", curPath)
	}

	// 创建目录或多级目录
	dirnameA := "F:\\testA"
	dirnameB := "F:\\testA\\surpassA\\surpassB"
	oldDirnameC := "F:\\testA\\surpassC"
	newDirnameC := "F:\\testA\\surpassC-NewDirName"
	os.Mkdir(dirnameA, os.ModePerm)    // 相当于mkdir,要求父路径都已经存在,才能创建目录成功,否则报错
	os.MkdirAll(dirnameB, os.ModePerm) // 相当于mkdir -p
	os.MkdirAll(oldDirnameC, os.ModePerm)

	// 判断目录是否存在
	if _, err := os.Stat(dirnameB); err == nil {
		fmt.Printf("路径: %+v 存在\n", dirnameB)
	}

	_, err := os.Stat(dirnameB)
	fmt.Printf("文件是否不存在?%+v\n", os.IsNotExist(err))

	// 文件夹重命名
	os.Rename(oldDirnameC, newDirnameC)
	// 删除目录
	os.Remove(dirnameB)    // 相当于 rm -f
	os.RemoveAll(dirnameA) // 相当于rm -rf

	if _, err := os.Stat(dirnameB); err != nil {
		fmt.Printf("路径: %+v 不存在\n", dirnameB)
	}

	// 判断目录是否存在
	_, err = os.Stat(dirnameB)
	fmt.Printf("文件是否不存在?%+v\n", os.IsNotExist(err))
}

    在创建目录时,注意事项如下所示:

  • os.Mkdir()  :相当于mkdir,要求父路径都已经存在,才能创建目录成功,否则报错
  • os.MkdirAll() :相当于mkdir -p
  • os.Remove() :相当于 rm -f
  • os.RemoveAll():相当于rm -rf

    代码运行结果如下所示:

当前目录:c:\Users\Surpass\Documents\GolangProjets\src\go-learning-note\02-代码\16\1605-目录操作
家目录:C:\Users\Surpass
当前目录:F:\
路径: F:\testA\surpassA\surpassB 存在
文件是否不存在?false
路径: F:\testA\surpassA\surpassB 不存在
文件是否不存在?true

16.2.2 目录遍历

    在日常开发过程,目录遍历是非常重要的,而遍历目录又可以分为遍历当前目录递归遍历目录,可以使用以下方法对目录进行遍历,示例代码如下所示:

  • 目录结构:
Test/
├── data-1
│   ├── data-11
│   │   └── data-11.json
│   └── data-12
├── data-2
│   └── data-21
│       └── data-21.txt
└── test.txt
  • 目录遍历代码
package main

import (
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
	"path/filepath"
)

func main() {
	dir := "F:\\Test"
	fmt.Println("仅遍历当前目录,但不递归-使用os.ReadDir")
	// 仅遍历当前目录,但不递归
	if de, err := os.ReadDir(dir); err != nil {
		fmt.Printf("遍历目录%+v出错,错误信息:%+v\n", dir, err)
	} else {
		for i, v := range de {
			fmt.Printf("索引%+v - 名字: %+v 是否为文件夹: %+v\n", i, v.Name(), v.IsDir())
		}
	}

	fmt.Println("仅遍历当前目录,但不递归-使用ioutil.ReadDir")
	// 仅遍历当前目录,但不递归,但推荐使用os.ReadDir()
	if fileInfo, err := ioutil.ReadDir(dir); err != nil {
		fmt.Printf("遍历目录%+v出错,错误信息:%+v\n", dir, err)
	} else {
		for i, v := range fileInfo {
			fmt.Printf("索引%+v - 名字: %+v 是否为文件夹: %+v\n", i, v.Name(), v.IsDir())
		}
	}

	fmt.Println("递归遍历目录-使用filepath.WalkDir")
	// 递归遍历目录,且包含自身
	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		fmt.Printf("路径: %+v 文件或文件夹: %+v\n", path, d.Name())
		return err
	})
	
	fmt.Println("递归遍历目录-使用filepath.Walk")
	filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
		fmt.Printf("路径: %+v 文件或文件夹: %+v\n", path, info.Name())
		return err
	})
}

    代码运行结果如下所示:

仅遍历当前目录,但不递归-使用os.ReadDir
索引0 - 名字: data-1 是否为文件夹: true
索引1 - 名字: data-2 是否为文件夹: true
索引2 - 名字: test.txt 是否为文件夹: false

仅遍历当前目录,但不递归-使用ioutil.ReadDir
索引0 - 名字: data-1 是否为文件夹: true
索引1 - 名字: data-2 是否为文件夹: true
索引2 - 名字: test.txt 是否为文件夹: false

递归遍历目录-使用filepath.WalkDir
路径: F:\Test 文件或文件夹: Test
路径: F:\Test\data-1 文件或文件夹: data-1
路径: F:\Test\data-1\data-11 文件或文件夹: data-11
路径: F:\Test\data-1\data-11\data-11.json 文件或文件夹: data-11.json
路径: F:\Test\data-1\data-12 文件或文件夹: data-12
路径: F:\Test\data-2 文件或文件夹: data-2
路径: F:\Test\data-2\data-21 文件或文件夹: data-21
路径: F:\Test\data-2\data-21\data=21.txt 文件或文件夹: data=21.txt
路径: F:\Test\test.txt 文件或文件夹: test.txt

递归遍历目录-使用filepath.Walk
路径: F:\Test 文件或文件夹: Test
路径: F:\Test\data-1 文件或文件夹: data-1
路径: F:\Test\data-1\data-11 文件或文件夹: data-11
路径: F:\Test\data-1\data-11\data-11.json 文件或文件夹: data-11.json
路径: F:\Test\data-1\data-12 文件或文件夹: data-12
路径: F:\Test\data-2 文件或文件夹: data-2
路径: F:\Test\data-2\data-21 文件或文件夹: data-21
路径: F:\Test\data-2\data-21\data=21.txt 文件或文件夹: data=21.txt
路径: F:\Test\test.txt 文件或文件夹: test.txt

16.3 文件操作

16.3.1 常规操作

    这里的常规操作是指文件的创建、判断是否存在、重命名、修改权限、属主属组等操作,示例操作如下所示:

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"time"
)

func main() {
	oldFilename := "F:\\Test\\surpass.json"
	newFilename := "F:\\Test\\surpass.json.rename"

	//如果文件不存在,则创建相应的目录和文件
	//如果相应的目录存在,则不创建目录
	if _, err := os.Stat(oldFilename); os.IsNotExist(err) {
		fmt.Printf("文件%+v不存在\n", oldFilename)
		dirName := filepath.Dir(oldFilename)
		if err := os.MkdirAll(dirName, os.ModePerm); err != nil {
			fmt.Printf("创建目录%+v出错,错误信息:%+v\n", dirName, err)
			return
		}
		// 文件创建
		if f, err := os.Create(oldFilename); err == nil {
			defer f.Close()
			fmt.Printf("文件%+v创建成功\n", oldFilename)
		} else {
			fmt.Printf("文件%+v创建失败,错误信息:%+v\n", oldFilename, err)
		}
	}

	// 文件重命名
	// Windows里面注意重命名时,是否有其他进程打开或占用该文件
	if err := os.Rename(oldFilename, newFilename); err != nil {
		fmt.Printf("重命名文件%+v失败,错误信息:%+v\n", oldFilename, err)
	}

	// 修改文件权限,一般适用于Linux
	if err := os.Chmod(newFilename, 0777); err != nil {
		fmt.Printf("修改文件权限%+v失败,错误信息:%+v\n", newFilename, err)
	}

	// 修改访问和修改时间
	now := time.Now().Add(time.Hour * -2)
	if err := os.Chtimes(newFilename, now, now); err != nil {
		fmt.Printf("修改文件时间%+v失败,错误信息:%+v\n", newFilename, err)
	}

	// 修改文件属主和属组,适用于Linux,且需要注意权限,Windows不支持该方法
	if err := os.Chown(newFilename, 0, 0); err != nil {
		fmt.Printf("修改属主和属组时间%+v失败,错误信息:%+v\n", newFilename, err)
	}

	// 删除单个文件
	if err := os.Remove(newFilename); err != nil {
		fmt.Printf("删除文件%+v失败,错误信息:%+v\n", newFilename, err)
	}

	if err := os.RemoveAll(filepath.Dir(newFilename)); err != nil {
		fmt.Printf("删除文件夹%+v失败,错误信息:%+v\n", filepath.Dir(newFilename), err)
	}
}

16.3.2 文件读写

    文件本质上就是存储在存储设备上,并且经过序列化的二进制数据,如果我们需要对一个文件进行读写操作,一般可以拆分为找到文件打开文件执行操作(读写) 等几个步骤。

  • 在打开文件会涉及到很多的读写模式,例如只读、只写、追加等模式
  • 读取文件本质上可以理解为如何读取字节数组中的数据
  • 写文件本质上可以理解为向字节数组中写入新的元素或者覆盖某些元素

16.3.2.1 文件打开与读取

    在Go语言中打开文件常常使用os模块中的OpenOpenFile方法。示例代码如下所示:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 文件内容为:Surpass
	filename := "F:\\Test\\surpass.txt"
	// 以只读方式打开文件
	f, err := os.Open(filename)
	if err != nil {
		fmt.Printf("打开文件%+v出错,错误信息:%+v\n", filename, err)
		panic(err)
	}

	defer f.Close()
	// 读取文件
	var buffer = make([]byte, 3)
	for {
		// 成功读取n字节
		n, err := f.Read(buffer)
		if err != nil {
			// 文件读到结尾时,EOF n=0
			fmt.Printf("读取文件出错:%+v\n", err)
			break
		}
		// 注意事项:
		// 因为每次读取3个字节,直到文件结尾
		// 但由于buffer未做清空处理,当读到文件末尾时,如果不够3个字节时,
		// 则仅拿当前已经读取到内容替换相应位置的字节
		// 而前一次的内容因未替换,也会输出,导致存在数据污染
		fmt.Printf("从%+v成功读取到%+v个字节,转换后结果%+v,未使用切片处理的结果:%+v\n", buffer[:n], n, string(buffer[:n]), buffer)
	}
}

    代码运行结果如下所示:

从[83 117 114]成功读取到3个字节,转换后结果Sur,未使用切片处理的结果:[83 117 114]
从[112 97 115]成功读取到3个字节,转换后结果pas,未使用切片处理的结果:[112 97 115]
从[115]成功读取到1个字节,转换后结果s,未使用切片处理的结果:[115 97 115]
读取文件出错:EOF

16.3.2.2 文件带定位读取

    通过前面文件打开可以看出,不管文件有多大,但在读取时都是一点一点读取到内存中,且该操作是从前向后依次读取的。这种方式也称之为顺序读取,但若现在有一个超大的文件,只需要读取其中一段数据,该如何读取呢?

    在前面,我们提到过文件实际是经过序列化的数据,既意味着文件可以看成是一个非常大的字节数组。读取操作实际上是从字节数组里面获取部分或全部数据。因此,读取文件可以理解为,有一个指针指向特定的位置,其随着指针不断向后移动,从而实现读取数据的功能。既然这样,那我们在读取文件的时候,提前指定指针的位置,就可以读取任意位置的数据了,而在Go语言中提供了Seek方法来实现该功能,其用法如下所示:

Seek(offset int64, whence int) (ret int64, err error)

    whence功能如下所示:

  • whence=0:相对于文件开头位置,offset只能为正,否则报错
  • whence=1:相对于文件当前位置,offset可以为正,也可以为负,但在为负时,不能超过左边界,即文件开头
  • whence=2:相对于文件结尾位置,offset可以为正,也可以为负,但在为负时,不能超过左边界,即文件开头

    特殊的用法如下所示:

// 将文件指针移动至文件开头
Seek(0,0)
// 将文件指针移动到文件末尾
Seek(0,2)

    示例代码如下所示:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 文件内容为:SurpassGoTestFileSeek
	filename := "F:\\Test\\surpass.txt"
	// 以只读方式打开文件
	if f, err := os.Open(filename); err == nil {
		defer f.Close()
		buffer := make([]byte, 5)
		var n int
		if n, err = f.Read(buffer); err != nil {
			fmt.Printf("读取文件:%+v出错,错误信息:%+v\n", filename, err)
		}
		fmt.Printf("文件已经读取到位置:%v,内容为:%+v\n", n, string(buffer[:n]))
		// seek
		// whence=0 文件开头
		// whence=1 当前位置
		// whence=2 文件结尾
		ret, _ := f.Seek(2, 0)
		fmt.Printf("文件当前偏移量为: %v\n", ret)
		n, _ = f.Read(buffer)
		fmt.Printf("使用seek,相对于文件头,文件%+v从%+v开始读取,结果为%+v\n", filename, ret, string(buffer[:n]))
		// whence=1正偏移量
		ret, _ = f.Seek(2, 1)
		fmt.Printf("文件当前偏移量为: %v\n", ret)
		n, _ = f.Read(buffer)
		fmt.Printf("使用seek,相对于当前位置正偏移量,文件%+v从%+v开始读取,结果为%+v\n", filename, ret, string(buffer[:n]))
		// whence=1负偏移量
		ret, _ = f.Seek(-3, 1)
		fmt.Printf("文件当前偏移量为: %v\n", ret)
		n, _ = f.Read(buffer)
		fmt.Printf("使用seek,相对于当前位置负偏移量,文件%+v从%+v开始读取,结果为%+v\n", filename, ret, string(buffer[:n]))

		// whence=2 正偏移量,因为相对于文件末尾,再偏移是读取不到数据的
		ret, _ = f.Seek(1, 2)
		fmt.Printf("文件当前偏移量为: %v\n", ret)
		n, _ = f.Read(buffer)
		fmt.Printf("使用seek,相对于文件末尾正偏移量,文件%+v从%+v开始读取,结果为%+v\n", filename, ret, string(buffer[:n]))

		// whence=2 负偏移量
		ret, _ = f.Seek(-12, 2)
		fmt.Printf("文件当前偏移量为: %v\n", ret)
		n, _ = f.Read(buffer)
		fmt.Printf("使用seek,相对于文件末尾负偏移量,文件%+v从%+v开始读取,结果为%+v\n", filename, ret, string(buffer[:n]))

		// 另外类似于Seek的方法为ReadAt,偏移量相对于文件开头,但不影响当前文件指针
		var offset int64 = 10
		n, _ = f.ReadAt(buffer, offset)
		fmt.Printf("使用ReadAt,相对于文件开头偏移,文件%+v从%+v开始读取,结果为%+v\n", filename, offset, string(buffer[:n]))

		n, _ = f.Read(buffer)
		fmt.Printf("使用Read,从文件%+v读取到的结果为%+v\n", filename, string(buffer[:n]))
	}
}

    文件运行结果如下所示:

文件已经读取到位置:5,内容为:Surpa
文件当前偏移量为: 2
使用seek,相对于文件头,文件F:\Test\surpass.txt从2开始读取,结果为rpass
文件当前偏移量为: 9
使用seek,相对于当前位置正偏移量,文件F:\Test\surpass.txt从9开始读取,结果为TestF
文件当前偏移量为: 11
使用seek,相对于当前位置负偏移量,文件F:\Test\surpass.txt从11开始读取,结果为stFil
文件当前偏移量为: 22
使用seek,相对于文件末尾正偏移量,文件F:\Test\surpass.txt从22开始读取,结果为
文件当前偏移量为: 9
使用seek,相对于文件末尾负偏移量,文件F:\Test\surpass.txt从9开始读取,结果为TestF
使用ReadAt,相对于文件开头偏移,文件F:\Test\surpass.txt从10开始读取,结果为estFi
使用Read,从文件F:\Test\surpass.txt读取到的结果为ileSe

16.3.2.3 文件使用缓冲读取

    使用Read读取,是非常偏向于底层的,但操作起来不太方便。在Go语言同时也提供了bufio包,实现了对文件的二进制或文本文件的处理方法。

    如果要使用带buffer的方式读取文件,bufio.Reader构造函数为:

func NewReader(rd io.Reader) *Reader {
	return NewReaderSize(rd, defaultBufSize)
}

    在以上构造函数中,要求传入的参数为io.Reader,其定义的类型为接口类型,如下所示:

type Reader interface {
	Read(p []byte) (n int, err error)
}

    也就意味着,传入的参数需要实现接口Reader方法,而os.File实现了该接口的Read方法。默认为4096字节。示例代码如下所示:

package main

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

func main() {
	// 文件内容为:Surpass测试GoTestFileSeek\n测试\n学习Go
	filename := "F:\\Test\\surpass.txt"
	f, err := os.Open(filename)
	defer f.Close()
	if err != nil {
		fmt.Printf("读取文件错误: %v\n", err)
	}
	// 复制了f的内指针,相当自己再另外打开一次
	reader := bufio.NewReader(f)
	buffer := make([]byte, 5)
	n, _ := reader.Read(buffer)
	fmt.Printf("Read方法 - 读取文件%+v,读取了%+v字节,读取结果:%+v\n", filename, n, string(buffer[:n]))

	// 按字节进行读取
	b1, _ := reader.ReadByte()
	fmt.Printf("ReadByte方法 - 读取文件%+v,读取结果:%+v\n", filename, string(b1))

	// 这里调整文件指针无用,带buffer内部有自己一个文件指针
	f.Seek(0, 0)

	// 遇到分隔符后停止,同时包含分隔符,适用于需要按指定分隔符的读取操作,比例按行读取\n
	b2, _ := reader.ReadBytes('s')
	fmt.Printf("ReadBytes方法 - 读取文件%+v,读取结果:%+v\n", filename, string(b2))

	// 适用于读取非英文字符的文件内容
	r, size, _ := reader.ReadRune()
	fmt.Printf("ReadRune方法 - 读取文件%+v,读取结果:%+v,读取长度:%+v\n", filename, r, size)

	line, _ := reader.ReadSlice('\n')
	fmt.Printf("ReadSlice方法 - 读取文件%+v,读取结果:%+v\n", filename, string(line))

	str, _ := reader.ReadString('\n')
	fmt.Printf("ReadString方法 - 读取文件%+v,读取结果:%+v\n", filename, str)
}

    代码运行结果如下所示:

Read方法 - 读取文件F:\Test\surpass.txt,读取了5字节,读取结果:Surpa
ReadByte方法 - 读取文件F:\Test\surpass.txt,读取结果:s
ReadBytes方法 - 读取文件F:\Test\surpass.txt,读取结果:s
ReadRune方法 - 读取文件F:\Test\surpass.txt,读取结果:27979,读取长度:3
ReadSlice方法 - 读取文件F:\Test\surpass.txt,读取结果:试GoTestFileSeek
ReadString方法 - 读取文件F:\Test\surpass.txt,读取结果:测试

16.3.2.4 文件打开Flag

    我们来看看os.OpenFile的方法定义:

func OpenFile(name string, flag int, perm FileMode) (*File, error)

    OpenFile方法共有3个参数,其中参数flag为int类型,代表文件读写模式,参数perm是FileMode类型,代表文件的相应权限,默认为0或0666。而内置的os.Open定义如下所示:

func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

    通过其定义可以看出,也是基于OpenFile定义,而相应的参数Flag设置为O_RDONLY,代表以只读方式打开。在Go语言中,os默认定义了以下几种文件读写模式,且每一种都胡详细的说明,源码如下所示:

const (
	// 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_CREATE一起使用,则要求文件必须不存在,否则则报错
	O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
	// 同步IO,等待前面一次IO完成后再进行
	O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
	// 打开文件时,先清空再写入,即覆盖模式
	O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
)
  • O_RDONLY、O_WRONLY、O_RDWR:单独使用,如果文件不存在会报错,即要求在读写文件时,该文件必须存在
  • O_RDONLY:以只读模式打开,一般较少使用
  • O_RDONLY | O_APPEND 等价于 O_RDWR | O_APPEND 前者的写法存在歧义,推荐使用后者写法

    文件权限的源码定义如下所示:

const (
	// The single letters are the abbreviations
	// used by the String method's formatting.
	ModeDir        FileMode = 1 << (32 - 1 - iota) // d: is a directory
	ModeAppend                                     // a: append-only
	ModeExclusive                                  // l: exclusive use
	ModeTemporary                                  // T: temporary file; Plan 9 only
	ModeSymlink                                    // L: symbolic link
	ModeDevice                                     // D: device file
	ModeNamedPipe                                  // p: named pipe (FIFO)
	ModeSocket                                     // S: Unix domain socket
	ModeSetuid                                     // u: setuid
	ModeSetgid                                     // g: setgid
	ModeCharDevice                                 // c: Unix character device, when ModeDevice is set
	ModeSticky                                     // t: sticky
	ModeIrregular                                  // ?: non-regular file; nothing else is known about this file

	// Mask for the type bits. For regular files, none will be set.
	ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular

	ModePerm FileMode = 0777 // Unix permission bits
)

    示例代码如下所示:

package main

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

func main() {
	// 文件内容为:Surpass测试GoTestFileSeek\n测试\n学习Go
	filename := "F:\\Test\\surpass.txt"
	flag := os.O_WRONLY                            // 文件必须存在
	flag = os.O_WRONLY | os.O_CREATE               // 文件不存在则在创建后写入,且从头开始覆盖写入
	flag = os.O_WRONLY | os.O_CREATE | os.O_TRUNC  // 文件不存在则在创建后写入,且从头开始覆盖写入;如果文件存在,则先清空文件,再从头开始写入
	flag = os.O_WRONLY | os.O_APPEND               //追加写入模式,但文件必须存在
	flag = os.O_APPEND                             // 相当于 os.O_WRONLY | os.O_APPEND
	flag = os.O_CREATE | os.O_APPEND               // 文件不存在,则创建文件 ,认为末尾再追加写入
	flag = os.O_WRONLY | os.O_APPEND | os.O_CREATE //追加写入模式,若文件不存在,则先创建文件
	flag = os.O_EXCL                               // 一般不会单独使用,但文件必须不存在
	flag = os.O_WRONLY | os.O_EXCL | os.O_CREATE   // 文件存在,则报错,不存在时,则自动创建文件
	flag = os.O_RDWR                               // 读写模式,从头开始进行读写,但要求文件必须存在
	flag = os.O_RDONLY                             // 文件必须存在

	if f, err := os.OpenFile(filename, flag, 0644); err == nil {
		defer f.Close()
		reader := bufio.NewReader(f)
		for {
			str, err := reader.ReadString('\n')
			if err == nil {
				fmt.Printf("读取结果:%+v\n", str)
			} else {
				fmt.Printf("读取错误:%+v\n", err)
				break
			}
		}
	}
}

    代码运行结果如下所示:

读取结果:Surpass测试GoTestFileSeek
读取结果:测试
读取结果:学习Go
读取错误:EOF

16.3.2.5 文件写入

    在文件中写入数据可以使用方法WriteWriteStringWriteAt等方法。三者差异如下所示:

    Write方法的参数是字节切片,最终将字节切片写入文件,并返回已经写入的长度和相应的错误信息,其定义如下所示:

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

    WriteString方法的参数是字符串类型,最终将字符串类型的数据写入文件,并返回已写入的长度信息和错误信息,其定义如下所示:

func (f *File) WriteString(s string) (n int, err error)

    WriteAt方法的参数是字节切片和偏移量,既在文件中按相对于文件开头的偏移量写入数据,并返回已写入的长度信息和错误信息,其定义如下所示:

func (f *File) WriteAt(b []byte, off int64) (n int, err error)

    示例代码如下所示:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 文件位置 Surpass测试GoTestFileSeek
	filename := "F:\\Test\\surpass.txt"
	flag := os.O_CREATE | os.O_RDWR
	f, err := os.OpenFile(filename, flag, os.ModePerm)
	defer f.Close()
	if err != nil {
		fmt.Printf("打开文件%+v出错,错误信息:%+v\n", filename, err)
	}

	buffer := make([]byte, 1024)
	n, err := f.Read(buffer)
	if err != nil {
		fmt.Printf("读取文件%+v出错,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("读取到的文件内容为:%+v\n", string(buffer[:n]))

	// 写入文件,仅写入缓冲区,还没有flush到存储设备中
	n, err = f.Write([]byte{'\n', 97, 98, 99, '\n'})
	if err != nil {
		fmt.Printf("第一次写入文件%+v出错,错误信息:%+v\n", filename, err)
	}
	// EOF之后往后写入了4个字节,并跳转到新的EOF
	fmt.Printf("第一次写入长度: %v\n", n)
	// 为安全起见,建议同步一资
	f.Sync()

	// Seek在移动文件指针时,会强行同步一资
	f.Seek(-4, 2)
	n, err = f.Read(buffer)
	if err != nil {
		fmt.Printf("读取文件%+v出错,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("读取到的文件内容为:%+v\n", string(buffer[:n]))

	n, err = f.WriteString("我是Surpass\n")
	if err != nil {
		fmt.Printf("第二次写入文件%+v错误,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("第二次写入长度: %v\n", n)
	// 为安全起见,建议同步一资
	f.Sync()

	f.Seek(-14, 2)
	n, err = f.Read(buffer)
	if err != nil {
		fmt.Printf("读取文件%+v出错,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("读取到的文件内容为:%+v\n", string(buffer[:n]))

	n, err = f.WriteAt([]byte{'A', 'B', 'C'}, 7)
	if err != nil {
		fmt.Printf("第三次写入文件%+v错误,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("第三次写入长度: %v\n", n)
	// 为安全起见,建议同步一资
	f.Sync()

	f.Seek(0, 0)
	n, err = f.Read(buffer)
	if err != nil {
		fmt.Printf("读取文件%+v出错,错误信息:%+v\n", filename, err)
	}
	fmt.Printf("读取到的文件内容为:%+v\n", string(buffer[:n]))
}

    运行结果如下所示:

读取到的文件内容为:Surpass测试GoTestFileSeek
第一次写入长度: 5
读取到的文件内容为:abc
第二次写入长度: 14
读取到的文件内容为:我是Surpass
第三次写入长度: 3
读取到的文件内容为:SurpassABC试GoTestFileSeek
abc
我是Surpass

文件默认以读或写方式打开时,默认文件指针在文件开头,当flag为os.O_APPEND时,默认文件指针会处于文件结尾。如果没有文件,
使用O_CREATE创建文件时,文件指针会默认从文件开头/文件结尾开始(头和尾是一样的),如果文件已经存在,则不创建文件,只要不使用O_APPEND,均从文件头开始
使用O_TRUNC,则先清空文件,文件指针从头开始写入

16.3.2.6 文件使用缓冲写入

    如果对文件I/O读写比较频繁的场景中,通过使用bufio可以提高读写性能。在bufio中定义了``NewWriter`方法来完成文件写入操作,与NewReader定义相似,仅仅是功能不一样。其定义如下所示:

func NewWriter(w io.Writer) *Writer 

    示例代码如下所示:

package main

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

func main() {
	// 文件位置 Surpass测试GoTestFileSeek
	filename := "F:\\Test\\surpass.txt"
	flag := os.O_CREATE | os.O_RDWR | os.O_APPEND
	f, err := os.OpenFile(filename, flag, os.ModePerm)
	if err != nil {
		fmt.Printf("打开文件%+v出错,错误信息:%+v\n", filename, err)
	}
	defer f.Close()
	reader := bufio.NewReader(f)
	writer := bufio.NewWriter(f)
	// 写入文件
	n, err := writer.WriteString("\nBufio测试写入")
	if err != nil {
		fmt.Printf("文件%+v写入错误,错误信息:%+v", filename, err)
	}
	fmt.Printf("文件成功写入字节数量: %v\n", n)
	writer.Write([]byte{'\n', 97, 98, 99, '\n'}) // \nabc\n
	writer.WriteRune(27979)                      //测
	writer.WriteRune(35797)
	writer.WriteRune(10) //试
	writer.Flush()
	f.Seek(0, 0)

	// 读取文件
	for {
		str, err := reader.ReadString('\n')
		if err != nil {
			fmt.Printf("文件%+v读取错误,错误信息:%+v", filename, err)
			break
		}
		fmt.Printf("文件读取结果: %+v\n", str)
	}
}

    代码运行结果如下所示:

文件成功写入字节数量: 18
文件读取结果: Surpass测试GoTestFileSeek
文件读取结果: Bufio测试写入
文件读取结果: abc
文件读取结果: 测试
文件F:\Test\surpass.txt读取错误,错误信息:EOF

    在使用bufio进行文件写入时,在完成数据写入操作之后,必须调用Flush方法将数据保存到文件,因为bufio包提供了一些缓冲操作,在数据写入时,会将数据存放某个变量中,最后通过Flush方法将所有数据一次性写入文件。

16.4 文件读写

16.4.1 CSV 文件读写

    CSV(Comman-Separated Values)文件是以逗号或制表符做为分隔符的纯文本形式存储的表格数据文件。可以使用Excel、文本编辑器等常用程序打开。在Go语言中,可以使用包encoding/csv打开,并且提供了按行读取全部读取按行写入全部写入等操作,分别对应的方法为ReadReadAllWriteWriteAll,其读写示例代码如下所示:

  • CSV示例文件
时间,姓名,分数
2024-07,Surpass,100
2024-08,Surmount,92
2024-09,Kevin,56
2024-10,Jenni,67
package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
)

func main() {
	filename := "F:\\Test\\surpass.csv"
	flag := os.O_CREATE | os.O_RDWR | os.O_APPEND
	f, err := os.OpenFile(filename, flag, 0)
	if err != nil {
		fmt.Printf("打开文件%+v出错 %v\n", filename, err)
		return
	}
	defer f.Close()

	csvWriter := csv.NewWriter(f)
	// 设置分隔符
	csvWriter.Comma = ','
	// 设置\r\n做为换行符
	csvWriter.UseCRLF = true
	// 按行写入
	writerData := []string{"2024-11", "Nick", "123"}
	err = csvWriter.Write(writerData)
	if err != nil {
		fmt.Printf("按行写入文件出错%+v\n", err)
	}
	// 将数据写入文件
	csvWriter.Flush()

	// 一次写入多行数据
	writeDatas := [][]string{
		[]string{"2024-12", "Test", "3000"},
		[]string{"2024-09", "Jerry", "5000"},
		[]string{"2024-01", "Abc", "1200"},
	}

	if err := csvWriter.WriteAll(writeDatas); err == nil {
		csvWriter.Flush()
	}

	// 按行读取文件
	// 注意这里如果CSV不是使用UTF-8编码,读取出来的数据为乱码
	f.Seek(0, 0)
	csvReader := csv.NewReader(f)
	for {
		row, err := csvReader.Read()
		if err == io.EOF || err != nil {
			fmt.Printf("按行读取文件出错%+v", err)
			break
		}
		fmt.Printf("读取到的文件内容: %v\n", row)
	}

	fmt.Println()

	// 一次读取所有内容
	f.Seek(0, 0)
	rows, err := csvReader.ReadAll()
	if err != nil {
		fmt.Printf("一次性读取文件出错%+v", err)
	}
	for idx, row := range rows {
		fmt.Printf("当前读取%+v行数据,内容为:%+v\n", idx, row)
	}

}

    代码运行结果如下所示:

读取到的文件内容: [时间 姓名 分数]
读取到的文件内容: [2024-07 Surpass 100]
读取到的文件内容: [2024-08 Surmount 92]
读取到的文件内容: [2024-09 Kevin 56]
读取到的文件内容: [2024-10 Jenni 67]
读取到的文件内容: [2024-11 Nick 123]
读取到的文件内容: [2024-12 Test 3000]
读取到的文件内容: [2024-09 Jerry 5000]
读取到的文件内容: [2024-01 Abc 1200]
按行读取文件出错EOF
当前读取0行数据,内容为:[时间 姓名 分数]
当前读取1行数据,内容为:[2024-07 Surpass 100]
当前读取2行数据,内容为:[2024-08 Surmount 92]
当前读取3行数据,内容为:[2024-09 Kevin 56]
当前读取4行数据,内容为:[2024-10 Jenni 67]
当前读取5行数据,内容为:[2024-11 Nick 123]
当前读取6行数据,内容为:[2024-12 Test 3000]
当前读取7行数据,内容为:[2024-09 Jerry 5000]
当前读取8行数据,内容为:[2024-01 Abc 1200]

16.4.2 JSON文件读写

16.4.2.1 序列化和反序列化

    内存中的map、slice、array以及各种对象,如何保存到一个文件中,如果是一个结构体又如何保存到一个文件中呢?在保存成文件中,又该如何从文件中读取数据,并再次在内存中再次恢复为对应类型的实例呢?因此,需要设计一套协议,按照一定规则,将内存中的数据保存到文件中。而文件是一个字节序列,因此需要把数据转换为字节序列并输出到文件,称这为序列化。如果是从文件中将字节序列恢复为原来的类型就是反序列化,详细如下所示:

  • 序列化(Seriallization): 将内存中的对象转换为字节序列的文件
  • 反序列化(Deserialization): 将字节序列的文件恢复为内存中对象

    通过将数据序列化后并持久化之后,可以通过网络传输、或者将接收到的文件反序列化等。如果是数据与二进制序列之间的相互转换称为二进制序列化和反序列化。如果是数据和字符之间是的相互转换称之为字符序列化和反序列化。示例如下所示:

  • 二进制序列化: Protocol Buffers、MessagePack等
  • 字符序列化: JSON、XML

JSON的相关知识,可查阅官网:https://www.json.org/json-en.html

16.4.2.2 JSON包

    Go语言中提供了encoding/json包,常用的序列化方法如下所示:

  • json.Marshal(v any) ([]byte, error):将v序列化字符序列,这个过程也称为Encode
  • json.Unmarshal(data []byte, v any) error:将字符序列data反序列化为v,这个过程也称为Decode

16.4.2.3 基本类型序列化

    示例代码如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	data := []any{
		1, 1.5, true, nil, "Surpass", //基本数据类型
		[3]int{97, 98, 99}, //数组
		[]int{65, 66, 67},  // 切片
		map[string]int{ // Map
			"surpass":  97,
			"surmount": 98,
			"kevin":    99,
		},
	}

	targetObj := make([][]byte, 0, len(data))
	// 序列化
	for i, v := range data {
		b, err := json.Marshal(v)
		if err != nil {
			continue
		}
		fmt.Printf("第%d元素值: %+v 序列化后结果:%+v,序列化后数据类型%[3]T,转换为string结果:%+v\n", i, v, b, string(b))
		targetObj = append(targetObj, b)
	}
	fmt.Printf("最终序列化结果: %v\n", targetObj)
	ret, _ := json.Marshal(data)
	fmt.Printf("直接序列化结果: %v\n", ret)

	// 反序列化
	for i, v := range targetObj {
		var t any
		err := json.Unmarshal(v, &t)
		if err != nil {
			continue
		}
		fmt.Printf("第%d个元素值:%+v,数据类型%[2]T,反序列化结果:%+v,反序列化后类型%[3]T\n", i, v, t)
	}
}

    代码运行结果如下所示:

第0元素值: 1 序列化后结果:[49],序列化后数据类型[]uint8,转换为string结果:1
第1元素值: 1.5 序列化后结果:[49 46 53],序列化后数据类型[]uint8,转换为string结果:1.5
第2元素值: true 序列化后结果:[116 114 117 101],序列化后数据类型[]uint8,转换为string结果:true
第3元素值: <nil> 序列化后结果:[110 117 108 108],序列化后数据类型[]uint8,转换为string结果:null
第4元素值: Surpass 序列化后结果:[34 83 117 114 112 97 115 115 34],序列化后数据类型[]uint8,转换为string结果:"Surpass"
第5元素值: [97 98 99] 序列化后结果:[91 57 55 44 57 56 44 57 57 93],序列化后数据类型[]uint8,转换为string结果:[97,98,99]
第6元素值: [65 66 67] 序列化后结果:[91 54 53 44 54 54 44 54 55 93],序列化后数据类型[]uint8,转换为string结果:[65,66,67]
第7元素值: map[kevin:99 surmount:98 surpass:97] 序列化后结果:[123 34 107 101 118 105 110 34 58 57 57 44 34 115 117 114 109 111 117 110 116 34 58 57 56 44 34 115 117 114 112 97 115 115 34 58 57 55 125],序列化后数据类型[]uint8,转换为string结果:{"kevin":99,"surmount":98,"surpass":97}
最终序列化结果: [[49] [49 46 53] [116 114 117 101] [110 117 108 108] [34 83 117 114 112 97 115 115 34] [91 57 55 44 57 56 44 57 57 93] [91 54 53 44 54 54 44 54 55 93] [123 34 107 101 118 105 110 34 58 57 57 44 34 115 117 114 109 111 117 110 116 34 58 57 56 44 34 115 117 114 112 97 115 115 34 58 57 55 125]]
直接序列化结果: [91 49 44 49 46 53 44 116 114 117 101 44 110 117 108 108 44 34 83 117 114 112 97 115 115 34 44 91 57 55 44 57 56 44 57 57 93 44 91 54 53 44 54 54 44 54 55 93 44 123 34 107 101 118 105 110 34 58 57 57 44 34 115 117 114 109 111 117 110 116 34 58 57 56 44 34 115 117 114 112 97 115 115 34 58 57 55 125 93]
第0个元素值:[49],数据类型[]uint8,反序列化结果:1,反序列化后类型float64
第1个元素值:[49 46 53],数据类型[]uint8,反序列化结果:1.5,反序列化后类型float64
第2个元素值:[116 114 117 101],数据类型[]uint8,反序列化结果:true,反序列化后类型bool
第3个元素值:[110 117 108 108],数据类型[]uint8,反序列化结果:<nil>,反序列化后类型<nil>
第4个元素值:[34 83 117 114 112 97 115 115 34],数据类型[]uint8,反序列化结果:Surpass,反序列化后类型string
第5个元素值:[91 57 55 44 57 56 44 57 57 93],数据类型[]uint8,反序列化结果:[97 98 99],反序列化后类型[]interface {}
第6个元素值:[91 54 53 44 54 54 44 54 55 93],数据类型[]uint8,反序列化结果:[65 66 67],反序列化后类型[]interface {}
第7个元素值:[123 34 107 101 118 105 110 34 58 57 57 44 34 115 117 114 109 111 117 110 116 34 58 57 56 44 34 115 117 114 112 97 115 115 34 58 57 55 125],数据类型[]uint8,反序列化结果:map[kevin:99 surmount:98 surpass:97],反序列化后类型map[string]interface {}

    根据以上运行结果,总结如下所示:

  • 根据序列化结果,说明各种数据类型都被序列化字节序列或者转换为字符串(可查阅ASCII来对照看)
  • 根据把序列化结果,会将各类数据类型反序列化Go的某类型数据,但JSON字符串中,数值类型均被反序列化为Go中的float64类型,true/false反序列化为bool类型、null反序列化为nil,字符串反序列化为string类型,数组反序列化为[]interface{}

16.4.2.4 结构体序列化

    示例代码如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name  string
	Age   int
	Score float32
	addr  string
}

func main() {
	p := Person{
		Name:  "Surpass",
		Age:   28,
		Score: 90.8,
		addr:  "Shanghai",
	}
	// 序列化
	b, err := json.Marshal(p)
	if err != nil {
		fmt.Printf("序列化对象出错: %v\n", err)
	}

	fmt.Printf("原始数据:%+v,\n序列化原始数据:%+v\n转换为string结果:%+v\n", p, b, string(b))

	// 反序列化
	data := []byte(`{"Name":"Surpass","Age":28,"Score":90.8,"addr":"Shanghai"}`)
	var p1 Person

	fmt.Println("反序列化-已知数据类型")
	err = json.Unmarshal(data, &p1)
	if err != nil {
		fmt.Printf("反序列化出错%+v\n", err)
	}

	fmt.Printf("反序列化数据为%+v,\n序列化结果:%+v,数据类型为%[2]T\n", data, p)

	// 如果不知道类型
	fmt.Println("反序列化-不知道数据类型")
	var p2 any
	err = json.Unmarshal(data, &p2)
	if err != nil {
		fmt.Printf("反序列化出错%+v\n", err)
	}
	fmt.Printf("反序列化数据为%+v,\n序列化结果:%+v,数据类型为%[2]T", data, p2)
}

    代码运行结果如下所示:

原始数据:{Name:Surpass Age:28 Score:90.8 addr:Shanghai},
序列化原始数据:[123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 56 125]
转换为string结果:{"Name":"Surpass","Age":28,"Score":90.8}
反序列化-已知数据类型
反序列化数据为[123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 56 44 34 97 100 100 114 34 58 34 83 104 97 110 103 104 97 105 34 125],
序列化结果:{Name:Surpass Age:28 Score:90.8 addr:Shanghai},数据类型为main.Person
反序列化-不知道数据类型
反序列化数据为[123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 56 44 34 97 100 100 114 34 58 34 83 104 97 110 103 104 97 105 34 125],
序列化结果:map[Age:28 Name:Surpass Score:90.8 addr:Shanghai],数据类型为map[string]interface {}

    根据以上运行结果,总结如下所示:

  • 在结构体中进行序列化时,如果对应的字段不是导出标识符,在序列化时会做丢弃处理
  • 对结构体进行反序列化时,若不知道相应的数据类型,反序列化之后,则为Map类型

    在Go语言中,结构体在序列化为JSON格式时,默认会使用字段名称做为JSON字段的key,也可以在结构体中添加字段标签,从而做到在序列化时,使用字段标签做为JSON的key,语法如下所示:

Name string `json:"name,omitempty"`
  • json表示引用json库,"name"用来指定序列化后使用的名称,多个参数使用逗号分隔
  • omitempty:表示序列化时忽略空值,即当该值为空值时,该字段不序列化
  • 如果使用 - 则表示该字段将被忽略
  • 如果存在多字段标签,可以使用空格间隔,例如:Name string json:"name,omitempty" msgpack:"name"

    示例代码如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name  string  `json:"name,omitempty"`
	Age   int     `json:"age"`
	Score float32 `json:"score"`
	Addr  string  `json:"addr"`
}

func main() {
	p := Person{
		Name:  "Surpass",
		Age:   28,
		Score: 90.8,
		Addr:  "Shanghai",
	}
	// 序列化
	b, err := json.MarshalIndent(p, "", "\t")
	if err != nil {
		fmt.Printf("序列化对象出错: %v\n", err)
	}

	fmt.Printf("原始数据:%+v,\n序列化原始数据:%+v\n转换为string结果:%+v\n", p, b, string(b))

	// 反序列化
	data := []byte(`{"Name":"Surpass","Age":28,"Score":90.8,"Addr":"Shanghai"}`)
	var p1 Person

	fmt.Println("反序列化-已知数据类型")
	err = json.Unmarshal(data, &p1)
	if err != nil {
		fmt.Printf("反序列化出错%+v\n", err)
	}

	fmt.Printf("反序列化数据为%+v,\n序列化结果:%+v,数据类型为%[2]T\n", data, p)

	// 如果不知道类型
	fmt.Println("反序列化-不知道数据类型")
	var p2 any
	err = json.Unmarshal(data, &p2)
	if err != nil {
		fmt.Printf("反序列化出错%+v\n", err)
	}
	fmt.Printf("反序列化数据为%+v,\n序列化结果:%+v,数据类型为%[2]T", data, p2)
}

    代码运行结果如下所示:

原始数据:{Name:Surpass Age:28 Score:90.8 Addr:Shanghai},
序列化原始数据:[123 10 9 34 110 97 109 101 34 58 32 34 83 117 114 112 97 115 115 34 44 10 9 34 97 103 101 34 58 32 50 56 44 10 9 34 115 99 111 114 101 34 58 32 57 48 46 56 44 10 9 34 97 100 100 114 34 58 32 34 83 104 97 110 103 104 97 105 34 10 125]
转换为string结果:{
	"name": "Surpass",
	"age": 28,
	"score": 90.8,
	"addr": "Shanghai"
}
反序列化-已知数据类型
反序列化数据为[123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 56 44 34 65 100 100 114 34 58 34 83 104 97 110 103 104 97 105 34 125],
序列化结果:{Name:Surpass Age:28 Score:90.8 Addr:Shanghai},数据类型为main.Person
反序列化-不知道数据类型
反序列化数据为[123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 56 44 34 65 100 100 114 34 58 34 83 104 97 110 103 104 97 105 34 125],
序列化结果:map[Addr:Shanghai Age:28 Name:Surpass Score:90.8],数据类型为map[string]interface {}

16.4.2.5 切片序列化

    示例代码如下所示:

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name  string
	Age   int
	Score float32
}

func main() {
	// 序列化
	ps := []Person{
		{Name: "Surpass", Age: 28, Score: 90.98},
		{Name: "Surmount", Age: 27, Score: 98.76},
		{Name: "Kevin", Age: 36, Score: 89.098},
	}
	b, err := json.Marshal(ps)
	if err != nil {
		fmt.Printf("序列化出错%+v", err)
	}
	fmt.Printf("序列化后结果:%+v,数据类型%T,\n转换为string结果:%+v\n", b, b, string(b))

	fmt.Println("反序列化-已知数据类型")
	var p1 []Person
	err = json.Unmarshal(b, &p1)
	if err != nil {
		fmt.Printf("反序列化出错%+v", err)
	}
	fmt.Printf("已知数据类型-反序列化结果:%+v,数据类型:%[1]T\n", p1)

	fmt.Println("反序列化-不知道数据类型")
	var p2 []any
	err = json.Unmarshal(b, &p2)
	if err != nil {
		fmt.Printf("反序列化出错%+v", err)
	}
	fmt.Printf("已知数据类型-反序列化结果:%+v,数据类型:%[1]T\n", p2)
}

    代码运行结果如下所示:

序列化后结果:[91 123 34 78 97 109 101 34 58 34 83 117 114 112 97 115 115 34 44 34 65 103 101 34 58 50 56 44 34 83 99 111 114 101 34 58 57 48 46 57 56 125 44 123 34 78 97 109 101 34 58 34 83 117 114 109 111 117 110 116 34 44 34 65 103 101 34 58 50 55 44 34 83 99 111 114 101 34 58 57 56 46 55 54 125 44 123 34 78 97 109 101 34 58 34 75 101 118 105 110 34 44 34 65 103 101 34 58 51 54 44 34 83 99 111 114 101 34 58 56 57 46 48 57 56 125 93],数据类型[]uint8,
转换为string结果:[{"Name":"Surpass","Age":28,"Score":90.98},{"Name":"Surmount","Age":27,"Score":98.76},{"Name":"Kevin","Age":36,"Score":89.098}]
反序列化-已知数据类型
已知数据类型-反序列化结果:[{Name:Surpass Age:28 Score:90.98} {Name:Surmount Age:27 Score:98.76} {Name:Kevin Age:36 Score:89.098}],数据类型:[]main.Person
反序列化-不知道数据类型
已知数据类型-反序列化结果:[map[Age:28 Name:Surpass Score:90.98] map[Age:27 Name:Surmount Score:98.76] map[Age:36 Name:Kevin Score:89.098]],数据类型:[]interface {}

16.4.2.6 JSON文件读取

  • JSON示例文件如下所示:
[
	{
		"date": "2024-07",
		"name": "Surpass",
		"score": 100
	},
	{
		"date": "2024-08",
		"name": "Surmount",
		"score": 92
	},
	{
		"date": "2024-09",
		"name": "Kevin",
		"score": 56
	}
]
  • 读取示例代码如下所示:
package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type PersonScore struct {
	Date  string  `json:"date"`
	Name  string  `json:"name"`
	Score float64 `json:"score"`
}

func main() {
	filename := "F:\\Test\\surpass.json"
	flag := os.O_RDONLY | os.O_CREATE
	f, err := os.OpenFile(filename, flag, 0)
	if err != nil {
		fmt.Printf("打开文件出错:%+v\n", err)
	}
	defer f.Close()

	var ps []PersonScore
	decoder := json.NewDecoder(f)
	err = decoder.Decode(&ps)
	if err != nil {
		fmt.Printf("Decoder 失败:%+v", err)
	}
	fmt.Printf("解析JSON后的结果为:%+v,数据类型为%[1]T", ps)
}

    代码运行结果如下所示:

解析JSON后的结果为:[{Date:2024-07 Name:Surpass Score:100} {Date:2024-08 Name:Surmount Score:92} {Date:2024-09 Name:Kevin Score:56}],数据类型为[]main.PersonScore

16.4.2.6 JSON文件写入

    示例代码如下所示:

package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"os"
)

type PersonScore struct {
	Date  string  `json:"date"`
	Name  string  `json:"name"`
	Score float64 `json:"score"`
}

func main() {
	filename := "F:\\Test\\surpass.json"
	flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
	f, err := os.OpenFile(filename, flag, 0)
	if err != nil {
		fmt.Printf("打开文件出错:%+v\n", err)
	}
	defer f.Close()

	var ps []PersonScore = []PersonScore{
		{Date: "2024-01", Name: "Surpass", Score: 99.99},
		{Date: "2024-02", Name: "Surmount", Score: 10.23},
		{Date: "2024-03", Name: "Kevin", Score: 98.765},
	}
	decoder := json.NewEncoder(f)
	err = decoder.Encode(&ps)
	if err != nil {
		fmt.Printf("Decoder 失败:%+v", err)
	}
	// fmt.Printf("解析JSON后的结果为:%+v,数据类型为%[1]T", ps)

	f1, _ := os.OpenFile(filename, os.O_RDONLY, 0)
	reader := bufio.NewReader(f1)
	for {
		str, err := reader.ReadString('\n')
		if err != nil || err == io.EOF {
			break
		}
		fmt.Printf("读取的数据为:%+v\n", str)
	}
}

    代码运行结果如下所示:

读取的数据为:[{"date":"2024-01","name":"Surpass","score":99.99},{"date":"2024-02","name":"Surmount","score":10.23},{"date":"2024-03","name":"Kevin","score":98.765}]

16.5 其他功能

16.5.1 获取系统信息

    示例代码如下所示:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 获取主机名
	if hostname, err := os.Hostname(); err == nil {
		fmt.Printf("主机名为:%+v\n", hostname)
	}

	// 获取用户ID
	fmt.Printf("UID: %+v GID: %+v\n", os.Geteuid(), os.Getegid())

	// 获取进程ID
	fmt.Printf("PID:%+v PPID: %+v\n", os.Getpid(), os.Getppid())

	// 获取环境变量
	fmt.Printf("获取到的环境变量值为:%+v\n", os.Getenv("GOPROXY"))
	// 添加环境变量
	if err := os.Setenv("GO_TEST_NAME", "Surpass"); err == nil {
		fmt.Printf("设置的环境变量值: %+v\n", os.Getenv("GO_TEST_NAME"))
	}

	// 删除环境变量
	if err := os.Unsetenv("GO_TEST_NAME"); err == nil {
		fmt.Printf("删除后环境变量值: %+v\n", os.Getenv("GO_TEST_NAME"))
	}

	// 获取主机所有环境变量
	fmt.Printf("主机所有环境变量为:%+v", os.Environ())

	// 删除所有环境变量,慎用
	// os.Clearenv()

}

    代码运行结果如下所示:

主机名为:Surpass-Laptop
UID: 1000 GID: 1000
PID:29128 PPID: 30500
获取到的环境变量值为:https://goproxy.cn,direct
设置的环境变量值: Surpass
删除后环境变量值: 
主机所有环境变量为:[LC_TIME=C.UTF-8 USER=surpass ... ]

16.5.2 执行终端命令

    执行终端命令是指模拟人工在系统终端输入命令并执行相应的命令,特别是在Linux系统中运行非常方便。但若在Windows系统中执行终端命令,查看返回结果时,可能会出现乱码,这是因为返回结果是以字节方式表示的。如果想要解决这类问题,需要借用第三方包,示例代码如下所示:

package main

import (
	"fmt"
	"os"
	"os/exec"

	"golang.org/x/text/encoding/simplifiedchinese"
)

type Charset string

const (
	UTF8    = Charset("UTF-8")
	GB18030 = Charset("GB18030")
)

func ConvertByte2String(data []byte, charset Charset) string {
	switch charset {
	case GB18030:
		decodeBytes, _ := simplifiedchinese.GB18030.NewDecoder().Bytes(data)
		return string(decodeBytes)
	case UTF8:
		fallthrough
	default:
		return string(data)
	}
}

func main() {
	if curPath, err := os.Getwd(); err == nil {
		os.Chdir(curPath)
	}
	// 执行系统命令
	cmd := exec.Command("cmd", "/C", "tree", "..")
	// 获取命令执行结果
	if output, err := cmd.Output(); err != nil {
		fmt.Println("执行命令出错,错误信息: %+v\n", err)
	} else {
		fmt.Printf("未转换前结果:%+v\n", string(output))
		fmt.Printf("转换后结果为:%+v\n", ConvertByte2String(output, GB18030))
	}
}

乱码转换参考文档:https://blog.csdn.net/qq_37493556/article/details/107541084

    代码运行结果如下所示:

1601-执行终端命令.png

本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

posted @ 2025-09-07 17:11  Surpassme  阅读(19)  评论(0)    收藏  举报