• Go学习第十章——文件操作,Json和测试


    1 文件

    1.1 基本介绍

    文件在程序中是以流的形式来操作的。

    image-20231026145329310

    **流:**数据在数据源(文件)和程序(内存)之间经历的路径。

    **输入流(读文件):**数据从数据源(文件)到程序(内存)的路径。

    **输出流(写文件):**数据从程序(内存)到数据源(文件)的路径。

    在Golang里,os.File封装所以文件相关操作,File是一个结构体。

    // File represents an open file descriptor.
    type File struct {
    	*file // os specific
    }
    
    • 1
    • 2
    • 3
    • 4
    1.2 读取的基本操作

    方法一:使用带缓存的方式读取,适用于大文件读取

    读取文件需要先了解下面的几个方法函数,需要这四步才算是一个完整的读取操作。

    1. 使用 os.Open() 函数打开文件

      • 函数原型:func Open(name string) (*os.File, error)
      • 示例代码:
      file, err := os.Open("test.txt")
      if err != nil {
          log.Fatal(err)
      }
      
      • 1
      • 2
      • 3
      • 4

      完整代码:

      func main() {
      	// 1.file 被叫做 file对象,file指针 ,file文件句柄
      	file, err := os.Open("D:\\Desktop\\test.txt")
      	if err != nil {
      		log.Fatal(err)
      	}
      
      	// 输出文件
      	fmt.Printf("file=%v", file)
      
      	// 关闭文件
      	err = file.Close()
      	if err != nil {
      		fmt.Println("close file err=", err)
      	}
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      输出结果:file=&{0xc00009aa00}

    2. 使用 bufio.NewReader() 函数创建读取缓冲区

      • 函数原型:func NewReader(rd io.Reader) *bufio.Reader
      • 示例代码:
      reader := bufio.NewReader(file) // 默认缓冲区为4096
      
      • 1
    3. 使用 ReadString() 函数读取文件内容

      • 函数原型:func (b *bufio.Reader) ReadString(delim byte) (string, error)
      • 示例代码:
      content, err := reader.ReadString('\n')
      if err != nil && err != io.EOF {
          log.Fatal(err)
      }
      
      • 1
      • 2
      • 3
      • 4
    4. 关闭文件

      • 示例代码:
      file.Close()
      
      • 1

    将上面的步骤合起来,就是读取文件的全过程:

    func main() {
    	// 1.file 被叫做 file对象,file指针 ,file文件句柄
    	file, err := os.Open("D:\\Desktop\\test.txt")
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// 输出文件
    	fmt.Printf("file=%v \n", file)
    
    	// 关闭文件(defer 最后结束再执行)
    	defer func(file *os.File) {
    		err := file.Close()
    		if err != nil {
    			fmt.Println("close file err=", err)
    		}
    	}(file)
    	
    	// 创建读取缓冲区
    	reader := bufio.NewReader(file)
    	
    	// 读取缓冲区内容,也就是文件内容
    	content, err := reader.ReadString('\n')
    	if err != nil && err != io.EOF {
    		log.Fatal(err)
    	}
    	
    	// 打印
    	fmt.Printf("content=%v", content)
    }
    
    • 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

    输出结果:

    file=&{0xc000078a00}
    content=你好,Hello Go File!!
    
    • 1
    • 2

    方法二:一次性将文件读取到内存中,适用于文件不大的情况

    1. 使用 ReadFile() 函数读取文件内容,读取整个文件的操作已经封装在函数内,不用手动打开或关闭文件。。
    • 函数原型:func ReadFile(name string) ([]byte, error)
    • 示例代码:
    data, err := io.ReadFile("test.txt")
    if err != nil {
        log.Fatal(err)
    }
    
    • 1
    • 2
    • 3
    • 4

    代码示例:

    func main() {
        // 一次性读取文件
        data, err := os.ReadFile("D:\\Desktop\\test.txt")
        if err != nil {
           log.Fatal(err)
        }
    
        fmt.Println(string(data))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出结果:

    你好,Hello Go File!!
    
    • 1
    1. 使用 io.ReadAll() 时,需要先手动打开文件,并在读取完成后手动关闭。(不推荐,已经被舍弃
    • 函数原型:func ReadFile(name string) ([]byte, error)
    • 示例代码:
    func main() {
        file, err := os.Open("D:\\Desktop\\test.txt")
        if err != nil {
           log.Fatal(err)
        }
        defer file.Close()
    
        data, err := ioutil.ReadAll(file)
        if err != nil {
           log.Fatal(err)
        }
    
        fmt.Println(string(data))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出结果:

    你好,Hello Go File!
    
    • 1
    1.3 写入的基本操作
    1. 使用 os.Create() 函数创建文件

      • 函数原型:func Create(name string) (*os.File, error)
      • 示例代码:
      file, err := os.Create("output.txt")
      if err != nil {
          log.Fatal(err)
      }
      
      • 1
      • 2
      • 3
      • 4
    2. 使用 file.Write() 函数将字符串写入文件

    • 函数原型:func (f *File) Write(b []byte) (n int, err error)
    • 示例代码:
    _, err := file.Write([]byte("Hello, world!\n"))
    if err != nil {
        log.Fatal(err)
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 关闭文件
    • 示例代码:
    file.Close()
    
    • 1

    将上面的步骤合起来,就是创建并写入文件的全过程:

    func main() {
    	// 创建文件
    	file, err := os.Create("D:\\Desktop\\output.txt")
    	if err != nil {
    		log.Fatal("创建文件错误",err)
    	}
    	// 将字符串写入文件
    	_, err = file.Write([]byte("Hello, world!\n"))
    	if err != nil {
    		log.Fatal("文件写入错误:", err)
    	}
    	err = file.Close()
    	if err != nil {
    		fmt.Println("关闭文件错误:", err)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出结果:无。打开文件,就能发现写入成功啦~~~~

    1.4 使用案例(三个)

    **案例一:**将一个文件的内容,写到另一个文件,注意,两个文件都已经存在。

    方案一:通过缓存的方式

    func main() {
        // 打开原文件
        inputFile, err := os.Open("D:\\Desktop\\input.txt")
        if err != nil {
           log.Fatal(err)
        }
        defer inputFile.Close()
    
        // 创建目标文件
        outputFile, err := os.Create("D:\\Desktop\\output.txt")
        if err != nil {
           log.Fatal(err)
        }
        defer outputFile.Close()
    
        // 创建缓冲区
        buffer := make([]byte, 1024)
    
        // 读取原文件并写入目标文件
        for {
           // 从原文件读取数据到缓冲区
           n, err := inputFile.Read(buffer)
           if err != nil && err != io.EOF {
              log.Fatal(err)
           }
           if n == 0 {
              break
           }
    
           // 将数据从缓冲区写入目标文件
           _, err = outputFile.Write(buffer[:n])
           if err != nil {
              log.Fatal(err)
           }
        }
    
        log.Println("文件内容写入成功!")
    }
    
    • 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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    方法二:使用io.Copy()复制io流

    func main() {
        // 打开原文件
        inputFile, err := os.Open("D:\\Desktop\\input.txt")
        if err != nil {
           log.Fatal(err)
        }
        defer inputFile.Close()
    
        // 创建目标文件
        outputFile, err := os.Create("D:\\Desktop\\output.txt")
        if err != nil {
           log.Fatal(err)
        }
        defer outputFile.Close()
    
        // 将原文件内容写入目标文件
        _, err = io.Copy(outputFile, inputFile)
        if err != nil {
           log.Fatal(err)
        }
    
        log.Println("文件内容写入成功!")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    输出结果:2023/10/26 16:29:29 文件内容写入成功!

    **案例二:**将一个图片拷贝到另一个文件夹下

    func main() {
        srcPath := "D:\\Desktop\\img.jpg"
        destPath := "D:\\Desktop\\img\\image.jpg"
    
        err := copyFile(srcPath, destPath)
        if err != nil {
           fmt.Println("Failed to copy file:", err)
           return
        }
    
        fmt.Println("File copied successfully!")
    }
    
    func copyFile(srcPath, destPath string) error {
        srcFile, err := os.Open(srcPath)
        if err != nil {
           return err
        }
        defer srcFile.Close()
    
        destFile, err := os.Create(destPath)
        if err != nil {
           return err
        }
        defer destFile.Close()
    
        _, err = io.Copy(destFile, srcFile)
        if err != nil {
           return err
        }
    
        return nil
    }
    
    • 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
    • 33

    输出结果:File copied successfully!

    上述代码中,srcPath和destPath分别表示源图片文件的路径和目标文件夹的路径。

    在copyFile函数中,首先使用os.Open打开源图片文件,并使用os.Create创建目标文件。然后使用io.Copy将源图片文件的内容拷贝到目标文件中。最后返回nil表示拷贝成功,或者返回拷贝过程中遇到的错误。

    **案例三:**统计一个文件内容里的英文、数字、空格和其他字符数量

    func main() {
        filePath := "D:\\Desktop\\input.txt" // 文件路径
    
        data, err := os.ReadFile(filePath)
        if err != nil {
           fmt.Printf("读取文件失败:%s\n", err)
           return
        }
    
        charsCount := make(map[string]int)
        for _, ch := range string(data) {
           switch {
           case unicode.IsLetter(ch):
              charsCount["英文"]++
           case unicode.IsDigit(ch):
              charsCount["数字"]++
           case unicode.IsSpace(ch):
              charsCount["空格"]++
           default:
              charsCount["其他字符"]++
           }
        }
    
        fmt.Printf("英文字符数量:%d\n", charsCount["英文"])
        fmt.Printf("数字字符数量:%d\n", charsCount["数字"])
        fmt.Printf("空格数量:%d\n", charsCount["空格"])
        fmt.Printf("其他字符数量:%d\n", charsCount["其他字符"])
    }
    
    • 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

    输出结果:

    英文字符数量:652
    数字字符数量:67
    空格数量:133
    其他字符数量:107
    
    • 1
    • 2
    • 3
    • 4

    2 Go语言的Json使用

    JSON易于机器解析和生成,并有效地提升网络传输效率通常程序在网络传输时会先将数据(结构体、map等)序列化成son字符串到接收方得到ison字符串时,在反序列化恢复成原来的数据类型(结构体、map等)。这种方式已然成为各个语言的标准。

    image-20231026165248054

    2.1 序列化案例
    type Person struct {
    	Name string `json:"name"`
    	Age  int    `json:"age"`
    }
    
    func main() {
    	people := []map[string]interface{}{
    		{
    			"name": "Alice",
    			"age":  25,
    		},
    		{
    			"name": "Bob",
    			"age":  30,
    		},
    	}
    
    	data, err := json.Marshal(people)
    	if err != nil {
    		fmt.Printf("序列化失败: %s", err)
    		return
    	}
    
    	err = ioutil.WriteFile("people.json", data, 0644)
    	if err != nil {
    		fmt.Printf("写入文件失败: %s", err)
    		return
    	}
    
    	fmt.Println("JSON数据已写入文件")
    }
    
    • 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

    在代码中,我们定义了一个Person结构体,表示每个人的姓名和年龄,这里主要添加tag,不然序列化后的是大写,不符合公共规范。然后,我们创建了一个包含多个map的切片people,每个map对应一个人的信息。

    使用json.Marshal()函数将切片people序列化为JSON数据。json.Marshal()函数会返回一个[]byte类型的字节切片,表示JSON数据。

    然后,我们使用ioutil.WriteFile()函数将JSON数据写入一个名为people.json的文件。

    最后,我们输出一个提醒信息,表示JSON数据已成功写入文件。

    运行以上代码,会在当前目录下生成一个名为people.json的文件,其中包含以下JSON数据:

    [
        {"name":"Alice","age":25},
        {"name":"Bob","age":30}
    ]
    
    • 1
    • 2
    • 3
    • 4

    这个JSON数组包含了两个map,每个map对应一个人的姓名和年龄。

    2.2 反序列化案例

    将上面的代码反过来,json格式转换成对应的数据

    type Person struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    
    func main() {
        filePath := "D:\\Desktop\\people.json" // JSON文件路径
    
        // 读取JSON文件内容
        data, err := os.ReadFile(filePath)
        if err != nil {
           fmt.Printf("读取文件失败:%s\n", err)
           return
        }
    
        var people []Person
    
        // 反序列化JSON数据
        err = json.Unmarshal(data, &people)
        if err != nil {
           fmt.Printf("反序列化失败: %s\n", err)
           return
        }
    
        fmt.Printf("解析到%d个人的信息:\n", len(people))
        for _, p := range people {
           fmt.Printf("姓名:%s\t年龄:%d\n", p.Name, p.Age)
        }
    
        // 将JSON数据反序列化为map类型
        var peopleMap []map[string]interface{}
    
        err = json.Unmarshal(data, &peopleMap)
        if err != nil {
           fmt.Printf("反序列化为map失败: %s\n", err)
           return
        }
    
        fmt.Printf("解析到%d个人的信息:\n", len(peopleMap))
        for _, p := range peopleMap {
           fmt.Printf("姓名:%s\t年龄:%v\n", p["name"], p["age"])
        }
    }
    
    • 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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    输出结果:

    解析到2个人的信息:
    姓名:Alice     年龄:25
    姓名:Bob       年龄:30
    解析到2个人的信息:
    姓名:Alice     年龄:25
    姓名:Bob       年龄:30
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3 单元测试

    3.1 先看个需求

    在我们工作中,我们会遇到这样的情况,就是去确认一个函数,或者一个模块的结果是否正确,如下:

    func addUpper(n int) int {
    	res := 0
    	for i := 1; i <= n; i++ {
    		res += i
    	}
    	return res
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致,则说明函数正确,否则函数有错误,然后修改错误.

    传统方式的缺点

    1. 不方便,我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
    2. 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路
    1. 引出单元测试。-> testing 测试框架 可以很好解决问题。
    3.2 快速入门

    Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命今来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基
    于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

    1. 确保每个函数是可运行,并且运行结果是正确
    2. 确保写出来的代码性能是好的,
    3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,

    image-20231026171938573

    1. 创建一个cal.go文件,把需要测试的代码放在里面

    package test01
    
    func AddUpper(n int) int {
        res := 0
        for i := 1; i <= n; i++ {
           res += i
        }
        return res
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2. 创建cal_test.go文件,在里面写测试案例

    import (
    	"testing"
    )
    
    func TestAddUpper(t *testing.T) {
    	res := AddUpper(10)
    	if res != 55 {
    		// fmt.Printf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
    		t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
    	}
    
    	// 如果正确,输出日志
    	t.Logf("AddUpper(10) 执行正确。。。")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    执行后的结果如图所示,我使用的GoLand,就比较方便:

    image-20231026173326882

    3.3 入门总结
    1. 测试用例文件名必须以 test.go 结尾。 比如 cal test.go ,cal 不是固定的。

    2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper

    3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T

    4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub

    5. 运行测试用例指令
      (1) cmd>go test [ 如果运行正确,无日志,错误时,会输出日志 ]

    (2) cmd>go test-v [ 运行正确或是错误,都输出日志 ]

    1. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

    2. t.Logf 方法可以输出相应的日志

    3. 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处

    4. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

    5. 测试单个文件,一定要带上被测试的原文件
      go test -v cal test.go cal.go

    6. 测试单个方法
      go test -v -test.runTestAddUpper

  • 相关阅读:
    vue3+vite+ts中的@的配置
    Linux 服务器环境搭建
    3D Gaussian Splatting for Real-Time Radiance Field Rendering
    前端工具宝库,帮你解决99%的业务需求难题
    二十八、java版 SpringCloud分布式微服务云架构之Java 包(package)
    Android Studio for Platform (ASfP) 使用教程
    Linux提权的几种常用方法
    面试官问我TCP三次握手和四次挥手,我真的是
    bzip2原理分享
    带你彻底搞懂缓冲区
  • 原文地址:https://blog.csdn.net/Hai_Helloyou/article/details/134061449