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
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
来指定:
//实例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
}
输入命令 go test -v
,默认会打印所有情况(包括出错和未出错的):
输入命令 go test -run More
,指定正则表达式匹配上测试函数名然后执行:
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
命令后如下:
但是测试组在出现错误时不容易辨别提示信息,比如:
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)
}
})
}
}
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=待测试函数名
执行测试:
其中的参数解释:
BenchmarkSplit-16
:18792752表示调用 Split 函数的次数,62.55 ns/op
表示每次调用 Split 函数耗时 62.55ns。
测试时加上 -benchmem
参数用来获得内存分配的统计数据:
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