首页 > 其他分享 >03_错误处理与测试

03_错误处理与测试

时间:2023-10-08 13:25:28浏览次数:41  
标签:03 测试 err defer fmt Println func test 错误处理

1. 错误处理机制

Go语言内置了一些错误接口,包括有 error 接口,内置 Error() 方法返回一个字符串;我们可以通过自定义异常处理结构体来实现自定义异常信息。比如自定义除法运算的除0异常:

package main

import "fmt"

// 定义一个DividerError结构
type DividerError struct {
	dividee int
	divider int
}

// 实现error接口的内部函数 Error
func (de *DividerError) Error() string {
	strFormat := `
      cannot proceed, the divider is zero.
      dividee: %d
      divider: 0`
	return fmt.Sprintf(strFormat, de.dividee)
}

// 实现两数相除运算
func Divide(varDividee int, varDivider int) (result int, msg string) {
	if varDivider == 0 {
		dData := DividerError{
			dividee: varDividee,
			divider: varDivider,
		}
		msg = dData.Error()
		return
	} else {
		return varDividee / varDivider, ""
	}
}

func main() {
	if result, msg := Divide(100, 10); msg == "" {
		fmt.Println("100/10 = ", result)
	}
	if _, msg := Divide(100, 0); msg != "" {
		fmt.Println("100/0 = ", msg)
	}
}

// 输出
100/10 =  10
100/0 =                                   
      cannot proceed, the divider is zero.
      dividee: 100                        
      divider: 0  

2. panic、recover、defer 的原理

2.1 原理解释

Go语言追求简洁,所以不支持 try...catch...finally 这种形式的异常处理,它认为将异常与控制结构混在一起容易使代码变得混乱。因此它引入了 panic(抛出异常,相当于throw)、recover(捕获异常,相当于catch)、defer(在函数返回前,简化函数清理工作)。

  • Panic

(1). 内建函数;

(2). 加入函数 F 中书写了 panic 语句,会终止其后要执行的代码,在 panic 所在函数 F 内如果存在要执行的 defer 函数列表,按照 defer 的逆序执行;

(3). 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer 有点类似 try-catch-finally 中的 finally

(4). 直到整个 goroutine 退出,并报告错误

  • recover

(1). 内建函数

(2). 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为

(3). 在defer函数中,通过recover来终止一个gojroutine的panicking过程,从而恢复正常代码的执行; 同时能够获取通过 panic 传递的 error

(4). go中抛出一个 panic 异常,然后在 defer中通过 recover 捕获这个异常,然后进行处理

(5). recover只有在延迟调用内直接调用才能终止错误,否则错误会向外传递。

  • 注意

(1). 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则 panic 时,recover 无法捕获到 panic,无法防止 panic 扩散。

(2). recover 处理异常后,逻辑不会恢复到 panic 之后,而是恢复到 defer 之后的点;

  • defer

defer 是 Go 中很重要的关键字,它常用于 关闭文件流、解锁一个加锁的资源、打印最终报告、关闭数据库链接等操作

(1). defer 的触发时机,包括以下三种情况:包裹defer的函数返回时、包裹defer的函数执行到末尾时、所在的goroutine发生panic时

(2). defer在执行时将延迟方法进行压栈,最后将所有压栈方法出栈执行,所以顺序是 LIFO的。

(3). 如果包括defer的函数调用了 os.Exit,对应的defer 不会被执行。

(4). defer 后面必须是函数调用语句,不能是其他语句(比如 n++,它是指令而不是语句)。

2.2 defer运用的一些坑

(1). defer在匿名返回值和命名返回值函数中的不同表现

func returnValues() int {                      //返回0
    var result int
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

func namedReturnValues() (result int) {        //返回1
    defer func() { 
        result++
        fmt.Println("defer")
    }()
    return result
}

首先defer 的执行顺序和 return是同时的,第一个例子中函数没有指定返回值名称,因此Go会自动创建一个返回值retValue,然后将 result赋值给 retValue;接着检查是否有 defer,如果有就执行,最后返回 retValue。可见defer 虽然改变了 result的值,但不变 retValue。

第二个例子明确指定了 result,就没有创建 retValue的过程,所以defer修改的result最后被返回。

(2). 在for循环中使用defer可能会导致性能问题

每次defer的执行都有额外开销,多个defer执行时它会对后需要的参数进行内存拷贝,还要进行压栈出栈操作,循环次数过多可以去掉defer来减少额外开销。

func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

(3). 判断没有err之后,再defer 释放资源

如果资源没有获取成功,那么就没有必要再对资源进行释放操作,因此 defer 释放资源的过程应该放到 err 判断之后。

resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

(4). defer 常见的应用场景 --- 文件关闭/对象锁释放

// file对象打开后自动关闭
func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    // other codes
    return io.Copy(dst, src)
}

// mutex对象锁住后的自动释放
func foo(...) {
    mu.Lock()
    defer mu.Unlock()

    // code logic
}

(5). 如何让defer函数在宿主函数的执行中间执行

场景: defer 是在该指令所在函数运行结束后执行的,如果有一个对象锁,我们在开始入口处获得了锁资源,而后半段函数运行时间过长,不需要保留该锁资源,我们希望在中间过程释放锁。

解决方法: ①逻辑拆分,把宿主函数分成两部分,一半需要锁一半不需要锁

func foo (){
    mu.lock()
    defer mu.Unlock()
    
    //逻辑拆分
    object, ok := map[key]
    if (!ok) {
        return
    }
    
    //剩余不需要锁资源的操作
    ......
}

②使用匿名函数

func lock() {
	mu.Lock()
	log.Printf("lock")
}

func unlock() {
	mu.Unlock()
	log.Printf("unlock")
}

func foo() int {
	lock()

	// 匿名函数定义,这里defer的宿主函数是匿名函数,匿名函数执行完defer开始执行,因此能释放锁
	func() {
		log.Printf("entry inner")
		defer unlock()
		log.Printf("exit inner")
	}()

	time.Sleep(1 * time.Second)
	log.Printf("return")
	return 0
}

func main(){
    r := foo()
	log.Println("r=", r)
}

// 打印
2022/04/04 14:22:14 lock
2022/04/04 14:22:14 entry inner
2022/04/04 14:22:14 exit inner
2022/04/04 14:22:14 unlock
2022/04/04 14:22:15 return
2022/04/04 14:22:15 r= 0

(6). defer 函数参数的计算时间点

defer 函数的参数是在defer语句出现的位置做计算的,而不是在函数运行时做计算的(即宿主函数运行结束的时候开始执行defer);

//例1
func foo(n int) int {
    log.Println("n1=", n)
    defer log.Println("n=", n)
    n += 100
    log.Println("n2=", n)
    return n
}
func main() {
    var i int = 100
    foo(i)
}
//例1out
2017/09/30 19:25:10 n1= 100
2017/09/30 19:25:10 n2= 200
2017/09/30 19:25:10 n= 100


//例2
func foo(n int) int {
    log.Println("n1=", n)
    defer func() {
        n += 100
        log.Println("n=", n)
    }()
    n += 100
    log.Println("n2=", n)
    return n
}
func main() {
    var i int = 100
    foo(i)
}
//例2out
2017/09/30 19:30:58 n1= 100
2017/09/30 19:30:58 n2= 200
2017/09/30 19:30:58 n= 300

例1中的n是作为defer函数的参数出现的,所以它的计算节点应该从出现的位置算起;

例2中的n不是defer的参数,是一个局部变量,所以它的计算节点应该从宿主函数foo() 执行完毕开始算起;

// 例1
package main

import "fmt"

func main() {
	fmt.Println("c")
	defer func() { // 注意 defer 需要先申明在panic之前不然不能捕获; recover 需要申明在 defer函数中
		fmt.Println("d")
		if err := recover(); err != nil {
			fmt.Println(err)
		}
		fmt.Println("e")
	}()
	f() // 开始调用 f()
	fmt.Println("f")
}

func f() {
	fmt.Println("a")
	panic("异常信息")
	fmt.Println("b")
}

// 输出
c
a       
d       
异常信息
e 
// 例2
package main

import "fmt"

func main() {
	fmt.Println("外层开始")
	defer func() {
		fmt.Println("外层准备recover")
		if err := recover(); err != nil { //panic已经在内层被处理了,这里没有异常,只是例行执行defer函数,然后执行后边的代码
			fmt.Printf("%#v - %#v\n", "外层", err)
		} else {
			fmt.Println("外层没做啥事")
		}
		fmt.Println("外层完成recover")
	}()

	fmt.Println("外层即将异常")
	f()
	fmt.Println("外层异常后")
	defer func() {
		fmt.Println("外层异常后的defer")
	}()
}

// 内层函数,包括抛出异常及内部recover处理
func f() {
	fmt.Println("内层开始")
	defer func() {
		fmt.Println("内层recover前的defer")
	}()

	defer func() {
		fmt.Println("内层准备recover")
		if err := recover(); err != nil { //这里对内层的panic首先进行了处理
			fmt.Printf("%#v - %#v\n", "内层", err)
		}
		fmt.Println("内层完成recover")
	}()

	defer func() {
		fmt.Println("内层完成recover后的defer")
	}()

	panic("异常信息")

	defer func() {
		fmt.Println("内层异常后的defer")
	}()

	fmt.Println("内层异常后的语句....")
}

通过例2可以发现,panic 在没有用 recover捕获前以及在 recover 捕获的那一级函数栈,panic 之后的代码都不会运行;一旦被 recover 捕获后,外层的函数栈代码恢复正常,所有代码都会得到执行。

(7) defer 后语句可能报错,采用闭包处理

//普通处理
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer f.Close()   //这里可能f.close() 也会报错
    }
    // ..code...
    return nil
}

//改进后
func do() (err error) {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {  //命名的变量返回defer的错误
                err = ferr
            }
        }()
    }
    // ..code...
    return nil
}

//使用相同的变量释放不同的资源,会报错,因为当延迟函数执行时只有一个变量会被用到,因此变量f会成为最后那个资源 "another-book.txt"
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }
    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }

    return nil
}

//终极版,给闭包传递参数,这样就使用到多个局部变量,而不是都使用外部的同一个引用变量
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }
    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }

    return nil
}

(8). recover只有在延迟调用内直接调用才会终止错误

func test() {
    defer func() {
        fmt.Println(recover()) //有效
    }()
    defer recover()              //无效!
    defer fmt.Println(recover()) //无效!
    defer func() {
        func() {
            println("defer inner")
            recover() //无效!
        }()
    }()

    panic("test panic")
}

func main() {
    test()
}

//output
defer inner
<nil>
test panic

(9). 将需要保护的代码重构成匿名函数,保证后续代码的执行

func test(x, y int) {
    var z int

    func() {                          //匿名函数中出错会被recover捕获处理,不会影响后续的逻辑执行
        defer func() {
            if recover() != nil {
                z = 0
            }
        }()
        panic("test panic")
        z = x / y
        return
    }()

    fmt.Printf("x / y = %d\n", z)
}

func main() {
    test(2, 1)
}

(10). 利用标准库 errors.New 和 fmt.Errorf 函数创建实现 error 接口的错误对象,通过判断错误对象来确定具体的错误类型:

var ErrDivByZero = errors.New("division by zero")   //具体的错误类型

func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}

func main() {
    defer func() {
        fmt.Println(recover())
    }()
    switch z, err := div(10, 0); err {    //通过判断错误对象实例来确定具体错误类型
    case nil:
        println(z)
    case ErrDivByZero:
        panic(err)
    }
}

(11). 实现类似Java Try-Catch 的错误处理模型

/**
  实现Try-Catch异常处理模型
  func:待执行业务函数
  handler:出错后的处理函数, interface{} 表示参数为任意类型
*/
func Try(fun func(), handler func(interface{})) {
	defer func() {
		if err := recover(); err != nil {
			handler(err)
		}
	}()

	fun()
}

func main() {
	Try(func() {
		x := 10
		y := 0
		fmt.Println(x / y)
	}, func(err interface{}) {
		fmt.Println(err)
	})
}

参考:

https://www.cnblogs.com/zhangweizhong/p/10999386.html

https://www.jianshu.com/p/79c029c0bd58

https://www.jianshu.com/p/5b0b36f398a2

3. 闭包处理错误的模式

每当函数返回时,我们都需要检查是否有错误发生,但是这样会导致重复的代码,结合 defer/panic/recover 机制和闭包我们可以得到一个更加优雅的解决模式。

假设所有的函数都有这样的签名:func f(a type1, b type2)

这里我们给这个函数类型一个名字:fType1 = func f(a type1, b type2)

然后在代码中定义两个帮助函数:

  • check:用来检查是否有错误和 panic 发生的函数:
func check(err error) {
    if err != nil {
        panic(err) 
    }
}
  • errorhandler:这是一个包装函数,接收一个 fType1类型的函数 fn 并返回一个调用 fn 的函数。里面就包含有 defer/recover 机制。
func errorHandler(fn fType1) fType1 {
    return func(a type1, b type2) {
        defer func() {
            if e,ok := recover().(error);ok{
                log.Printf(“run time panic: %v”, err)
            }
        }()
        fn(a, b)
    }
}

这样当我们在代码中执行多个返回函数时,通过多个check来捕获出错时的异常数据err并抛出panic异常。接着通过包含了闭包函数的 errorHandler 中的recover 捕获处理。理论上所有的错误都会被 recover,并且简化了代码中调用函数后的错误检查。

func f1(a type1, b type2) {
    ...
    f, _, err := // call function/method
    check(err)
    t, err := // call function/method
    check(err)
    _, err2 := // call function/method
    check(err2)
    ...
}

错误处理例子:

func g(i int) {
	if i > 3 {
		fmt.Println("Panicking !")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g", i)
	fmt.Println("Printing in g", i)
	g(i + 1)
}
func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	fmt.Println("Calling g.")
	g(0)
	fmt.Println("Returned normally from g.")
}
func Test08() {
	f()
	fmt.Println("Returned normally from f.")
}

/* 输出
Calling g.
Printing in g 0          
Printing in g 1          
Printing in g 2          
Printing in g 3          
Panicking !              
Defer in g 3             
Defer in g 2             
Defer in g 1             
Defer in g 0             
Recovered in f 4         
Returned normally from f.
*/

4. 启动外部命令和程序

  • 通过 os 包下的 StartProcess 函数可以调用或者启动外部系统命令和二进制可执行文件;它的第一个参数是要运行的进程,第二个参数用来传递选项或者参数,第三个参数是含有系统环境基本信息的结构体。
package main
import (
    "fmt"
    "os/exec" 
    "os"
)

func main() {
// 1) os.StartProcess //
/*********************/
/* Linux: */
env := os.Environ()
procAttr := &os.ProcAttr{
            Env: env,
            Files: []*os.File{
                os.Stdin,
                os.Stdout,
                os.Stderr,
            },
        }
// 1st example: list files 
pid, err := os.StartProcess("/bin/ls", []string{"ls", "-l"}, procAttr)  
if err != nil {
        fmt.Printf("Error %v starting process!", err)  // 
        os.Exit(1)
}
fmt.Printf("The process id is %v", pid) 
  • 通过 exec 包中的函数 exec.Command(name string, arg ...string)Run() 来启动外部命令。它首先需要通过系统命令或者可执行文件的名字创建一个 Command 对象,然后用这个对象作为接收者调用 Run()。
// 2) exec.Run //
/***************/
// Linux:  OK, but not for ls ?
// cmd := exec.Command("ls", "-l")  // no error, but doesn't show anything ?
// cmd := exec.Command("ls")        // no error, but doesn't show anything ?
    cmd := exec.Command("gedit")  // this opens a gedit-window
    err = cmd.Run()
    if err != nil {
        fmt.Printf("Error %v executing command!", err) 
        os.Exit(1)
    }
    fmt.Printf("The command is %v", cmd)
// The command is &{/bin/ls [ls -l] []  <nil> <nil> <nil> 0xf840000210 <nil> true [0xf84000ea50 0xf84000e9f0 0xf84000e9c0] [0xf84000ea50 0xf84000e9f0 0xf84000e9c0] [] [] 0xf8400128c0}
}
// in Windows: uitvoering: Error fork/exec /bin/ls: The system cannot find the path specified. starting process!

5. Go 中的单元测试和基准测试

5.1 测试文件 *_test.go

Go 中单元测试通过 go test 测试工具进行,注意测试文件名必须满足 *_test.go 这种形式。

*_test.go 文件中有三种类型的函数,单元测试函数、基准函数和示例函数

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档
  • 要开始一个单元测试,需要准备一个 go 源码文件,在命名时注意必须 _test 结尾。
  • 单元测试文件可以由多个测试用例组成,每个测试用例函数需要以 Test 作为前缀。 func TestXXX( t *testing.T )
  • 测试用例文件不会参与原来正常源码编译,不会包含到可执行文件中。

5.2 go test 详解

go test是go语言自带的测试工具,其中包含的是两类,单元测试性能测试

通过go help test可以看到go test的使用说明:

格式形如:
go test [-c] [-i] [build flags] [packages] [flags for test binary]

参数解读:

-c : 编译go test成为可执行的二进制文件,但是不运行测试。

-i : 安装测试包依赖的package,但是不运行测试。

关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空

关于packages,调用go help packages,这些是关于包的管理,一般设置为空

关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数

-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。

-test.run pattern: 只跑哪些单元测试用例

-test.bench patten: 只跑那些性能测试用例

-test.benchmem : 是否在性能测试的时候输出内存情况

-test.benchtime t : 性能测试运行的时间,默认是1s

-test.cpuprofile cpu.out : 是否输出cpu性能分析文件

-test.memprofile mem.out : 是否输出内存性能分析文件

-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件

-test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,那就是不做打点了。

你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。

-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下

-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。

-test.timeout t : 如果测试用例运行时间超过t,则抛出panic

-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理

-test.short : 将那些运行时间较长的测试用例运行时间缩短

5.3 测试实例

//实例1
func TestHello(t *testing.T) {
   a := "Hello World"
   if a != "Hello World" {
      t.Error("测试不通过")
   }
}

/* 输入: go test -run TestHello .\hello_test.go
   输出: ok      command-line-arguments  0.160s
*/
//实例2
func TestA(t *testing.T) {
	t.Log("A")
}

func TestAK(t *testing.T) {
	t.Log("AK")
}

func TestB(t *testing.T) {
	t.Log("B")
}

func TestC(t *testing.T) {
	t.Log("C")
}
/* 输入: go test -v -run TestFail .\hello_test.go
   输出:
=== RUN   TestA
    hello_test.go:16: A
--- PASS: TestA (0.00s)
=== RUN   TestAK
    hello_test.go:20: AK
--- PASS: TestAK (0.00s)
PASS
ok      command-line-arguments  0.165s

当测试失败时,通过以下函数来通知测试失败:

  • func(t *T) Fail(),标记测试函数失败,然后继续执行剩下的测试;
  • func(t *T) FailNow(),表示测试函数失败并中止执行,文中别的测试也被略过,继续执行下一个文件;
  • func(t *T) Log(args ...interface{}), args 被用默认的格式格式化并打印到错误日志中。
  • func(t *T) Fatal(args ...interface{}),先执行3后执行2 的效果。

单元测试还可以用来测试用例的覆盖率,通过指定 --cover 来指定:

image-20220508233623226

//实例3

// split/split.go 文件
package split
import "strings"
// 字符串分割函数
func Split(s, sep string) (result []string) {
	i := strings.Index(s, sep)
	for i > -1 {
		result = append(result, s[:i])
		s = s[i+1:]
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}



// testGo/Split_test.go 文件
package testGo
import (
	"reflect"
	"test3/split"
	"testing"
)
//测试函数1
func TestOneSplit(t *testing.T) {
	got := split.Split("a:b:c", ":")
	want := []string{"a", "b", "c"}
	if !reflect.DeepEqual(got, want) {   // slice不能直接比较,通过反射包进行比较
		t.Errorf("expected:%v, got:%v", want, got)
	}
}
//测试函数2
func TestMoreSplit(t *testing.T) {
	got := split.Split("abcd", "bc")
	want := []string{"a", "d"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("expected:%v, got:%v", want, got)
	}
}

在 testGo 目录下运行 go test 命令,默认只会打印出错的测试情况:

可以发现由于分割时未考虑 sep 多个字符的情况,所以不通过,修改后代码为:

func Split(s, sep string) (result []string) {
	i := strings.Index(s, sep)
	for i > -1 {
		result = append(result, s[:i])
		s = s[i+len(sep):]        //每次往后移动一个sep字串长度
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}
image-20220923163539800

输入命令 go test -v ,默认会打印所有情况(包括出错和未出错的):

image-20220923163622380

输入命令 go test -run More ,指定正则表达式匹配上测试函数名然后执行:

image-20220923163726945

5.4 测试组和自测试

​ 可以一次定义多个测试用例来测试函数准确性,采用 slice 存储测试用例,然后利用 range-for 遍历各条测试数据进行测试:

//测试组
func TestSplitGroup(t *testing.T) {
	// 定义一个测试用例类型
	type test struct {
		input string
		sep   string
		want  []string
	}
	// 定义一个存储测试用例的切片
	tests := []test{
		{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		{input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		{input: "abcd", sep: "bc", want: []string{"a", "d"}},
		{input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
	}
	// 遍历切片,逐一执行测试用例
	for _, tc := range tests {
		got := split.Split(tc.input, tc.sep)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("excepted:%v, got:%v", tc.want, got)
		}
	}
}

​ 执行 go test -v 命令后如下:

image-20220923165729959

​ 但是测试组在出现错误时不容易辨别提示信息,比如:

    split $ go test -v
    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:42: excepted:[]string{"枯藤", "树昏鸦"}, got:[]string{"", "枯藤", "树昏鸦"}
    FAIL
    exit status 1
    FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

​ 为了更明显观察到测试信息,使用自测试进行,自测试利用 Map 存储各条测试用例,在测试用例出错时能够通过 key 快速观察到:

//子测试
func TestSplitChild(t *testing.T) {
	// 定义一个测试用例类型
	type test struct {
		input string
		sep   string
		want  []string
	}
	// 子测试测试用例用map存储
	tests := map[string]test{
		"simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		"wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		"more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
		"leading sep": {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
	}
	// 遍历切片,逐一执行测试用例
	for name, tc := range tests {
        t.Run(name, func(t *testing.T) {   //使用 t.Run()进行子测试
			got := split.Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("excepted:%#v, got:%#v", tc.want, got)
			}
		})
	}
}
image-20220923165948798

5.5 测试覆盖率

Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

    split $ go test -cover
    PASS
    coverage: 100.0% of statements
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.005s 

5.6 基准测试

​ 基准测试及在一定的工作负载下测试程序的性能,Go 语言中通过 *testing.B 类型参数进行测试,测试需要执行 b.N 次,b.N 是系统根据实际情况去调整的。

基准测试示例

//基准测试
func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        split.Split("枯藤老树昏鸦", "老")
    }
}

​ 基准测试并不会自动执行,需要增加 -bench 参数,通过 go test-bench=待测试函数名 执行测试:

image-20220923193121544

​ 其中的参数解释:

  • BenchmarkSplit-16:18792752表示调用 Split 函数的次数,62.55 ns/op 表示每次调用 Split 函数耗时 62.55ns。

​ 测试时加上 -benchmem 参数用来获得内存分配的统计数据:

image-20220923193423298
  • 32B/op :表示每次操作内存分配了 32字节;
  • 1 allocs/op:表示每次操作进行了1次内存分配

参考:

https://www.topgoer.cn/docs/golang/chapter05-8

标签:03,测试,err,defer,fmt,Println,func,test,错误处理
From: https://www.cnblogs.com/istitches/p/17748637.html

相关文章

  • 软件检测如何收费,有哪些测试类型?
    软件测评的费用软件检测收费因不同的测试类型、测试标准、测试难度、测试时间等因素而有所不同。一般而言,软件检测费用会根据测试点数量或者项目大小来报价。软件测试类型包括功能测试、性能测试、兼容性测试、安全性测试、易用性测试等。功能测试是测试软件的基本功能是否......
  • bugku渗透测试2 WP
    bugku渗透测试2WPbugku渗透测试1WP有详细的frp,proxifier配置https://www.cnblogs.com/thebeastofwar/p/17744965.html注意这次因为有三次frp代理,所以注意代理命名和端口号要不同(frpc1.ini,frpc2.ini,frpc3.ini样例)先fscan一段扫,无果然后nmap扫全端口,无果之后......
  • ubuntu下mysql有表却提示table doesn't exist
    linux里面的mysql是区分大小写的,windows下的mysql不区分。在mysql的安装目录里面找到mysqld.cnf文件,在[mysqld]的下面(可以看到还有别的配置信息)添加  lower_case_table_names=1  就行了。我的这个配置文件的目录是/etc/mysql/mysql.conf.d文件夹下。......
  • AttributeError: 'NoneType' object has no attribute 'dtype'
     ---------------------------------------------------------------------------AttributeErrorTraceback(mostrecentcalllast)/tmp/ipykernel_23207/4182898696.pyin<module>45......
  • P1003 [NOIP2011 提高组] 铺地毯
    第一思路:开一个N*N的数组,每次都扫一遍地毯范围并标记编号然后你会发现:喜提MLE为什么呢?我们来看看数据范围0≤n≤1e4n的范围是1e4,数组总大小为1e16,大约需要4000TB的内存空间服务器也不带这么玩的正解:将地毯信息用结构体存储structnode{ intx1,y1,x2,y2;//x1......
  • 关于训练集、验证集、测试集的理解
    我们在一般深度学习的实验中,经常使用到的是训练集和测试集。训练集自不必说,是用来训练网络参数的,如网络权重W,b。要进行区分的是验证集和测试集。验证集的作用是用来调整超参数,如网络层数、学习率等等。而测试集是用来测试你所训练的网络(包括网络参数和超参数)的效果。之所以不能用......
  • 20231306 gcc测试
    通过homebrew安装gcc2.检测gcc安装成功3.创建文件夹“my_program.c"并编写代码4.创建文件“my_program"并用gcc进行预处理......
  • 03-链表常见六个操作
    我的想法:问题:正确思路:适用场景:代码//题目:/**学习到:*写代码过程中:*1.类成员变量使用'_',变量名前后都可*2.要弄清出index(第几个元素,从0开始)与_size(链表中元素个数)的意义*2.*代码逻辑:*1.写代码之前,一定要弄清出目的,以及实现他需要的东西,条件*2.操作前......
  • 2023-2024-1 20231403 《计算机基础与程序设计》 第二周学习总结
    作业信息这个作业属于哪个课程<班级的链接>(如2022-2023-1-计算机基础与程序设计)这个作业要求在哪里2023-2024-1计算机基础与程序设计第二周作业)这个作业的目标学习两本课本的第一章内容作业正文https://www.cnblogs.com/lsrmy/p/17747323.html教材学习内......
  • CF506D Mr. Kitayuta's Colorful Graph
    好久没更新这个单题系列了,主要是最近没啥CF比赛空闲时间又少,今天忙里偷闲写了两个题这个题就比较典了,两点是否连通一般都是想到并查集维护,现在的问题是要对每种颜色的边把贡献算清楚很容易想到枚举所有颜色的边,每次求出所有连通分量后遍历一遍询问统计答案,这样正确性显然但复杂......