golang笔记09--go语言测试与性能调优

1 介绍

本文继上文 golang笔记08–go语言错误处理和资源管理, 进一步了解 go语言的错误处理和资源管理,以及相应注意事项。
具体包括 : 测试介绍 、代码覆盖率和性能测试、使用pprof进行性能调优、p测试http服务器(上)、测试http服务器(下)、生成文档和示例代码 等内容。

2 测试与性能调优

2.1 测试介绍

传统测试 vs 表格测试:

传统测试表格测试
测试数据和测试逻辑混在一起分离的测试数据和测试逻辑
出错信息不明确明确的出错信息
一旦一个数据出错测试全部结束可以部分失败

两者测试代码结构如下图所示(左边传统、右边表格):
在这里插入图片描述

vim triangle.go
package main

import (
	"fmt"
	"math"
)

func triangle() {
	var a, b int = 3, 4
	c := calTriangle(a, b)
	fmt.Println(c)
}

func calTriangle(a, b int) int {
	var c int
	c = int(math.Sqrt(float64(a*a + b*b)))
	return c
}

vim triangle_test.go
package main

import "testing"

func TestTriangle(t *testing.T) {
	tests := []struct{ a, b, c int }{
		{3, 4, 5},
		{5, 12, 13},
		{8, 15, 17},
		{12, 35, 37},
		{3000, 4000, 5000},
		//{3000, 4000, 5001},
	}
	for _, tt := range tests {
		if actual := calTriangle(tt.a, tt.b); actual != tt.c {
			t.Errorf("calTriangle(%d, %d); got %d; expected %d", tt.a, tt.b, tt.c, calTriangle(tt.a, tt.b))
		}
	}
}
输出:
=== RUN   TestTriangle
--- PASS: TestTriangle (0.00s)
PASS
出错输出:
=== RUN   TestTriangle
    triangle_test.go:16: calTriangle(3000, 4000); got 5001; expected 5000
--- FAIL: TestTriangle (0.00s)
FAIL

2.2 代码覆盖率和性能测试

IDEA 中,对于testing类型的程序,可以直接通过查看 Run TestTriangle in lear… with Coverage来查看测试代码的覆盖率(也可以根据需要选择CPU 或者 Memory Profiler),如下图所示:
在这里插入图片描述
在这里插入图片描述

vim triangle.go
package main

import (
	"fmt"
	"math"
)

func triangle() {
	var a, b int = 3, 4
	c := calTriangle(a, b)
	fmt.Println(c)
}

func calTriangle(a, b int) int {
	var c int
	c = int(math.Sqrt(float64(a*a + b*b)))
	return c
}

vim triangle_test.go
package main

import "testing"

func TestTriangle(t *testing.T) {
	tests := []struct{ a, b, c int }{
		{3, 4, 5},
		{5, 12, 13},
		{8, 15, 17},
		{12, 35, 37},
		{3000, 4000, 5000},
		//{3000, 4000, 5001},
	}
	for _, tt := range tests {
		if actual := calTriangle(tt.a, tt.b); actual != tt.c {
			t.Errorf("calTriangle(%d, %d); got %d; expected %d", tt.a, tt.b, tt.c, calTriangle(tt.a, tt.b))
		}
	}
}

func BenchmarkTriangle(b *testing.B) {
	a1, b1, c1 := 30000, 40000, 50000
	for i := 0; i < b.N; i++ {
		actual := calTriangle(a1, b1)
		if actual != c1 {
			b.Errorf("calTriangle(%d, %d); got %d; expected %d", a1, b1, c1, calTriangle(a1, b1))
		}
	}
}

TestTriangle 输出:
=== RUN   TestTriangle
--- PASS: TestTriangle (0.00s)
PASS

BenchmarkTriangle 输出:
goos: linux
goarch: amd64
pkg: learngo/chapter9/9.1
BenchmarkTriangle
BenchmarkTriangle-4   	1000000000	         0.285 ns/op 【执行了1000000000此操作,平均每次操作0.285 ns】
PASS

除了上述 ide直接执行测试用例外,也可以通过命令行执行测试用例:
1) 在测试用例当前目录执行go test就可以执行测试用例
chapter9/9.1$ go test
2)通过 coverprofile=c.out 可以输出测试覆盖率到 c.out 文件
chapter9/9.1$ go test -coverprofile=c.out
PASS
coverage: 50.0% of statements
ok      learngo/chapter9/9.1    0.002s
可以进一步通过 chapter9/9.1$ go tool cover -html c.out 查看覆盖率信息
3)命令行执行benchmark
go test -bench .

2.3 使用pprof进行性能调优

本案例使用benchmark 的 cpuprofile,结合寻找最长不重复子串来逐步优化程序性能,案例中的 web 图标需要依赖 graphviz 。

具体思路: -cpuprofile获取性能数据 --》go tool pprof查看性能数据 --》根据web图分析慢在哪里 --》优化代码

vim 9.2.go
package main

import "fmt"

func lengthOfNonRepeatSubStrOld(s string) int {
	lastOccurred := make(map[rune]int)
	start := 0
	maxLength := 0
	for i, ch := range []rune(s) {
		if lastI, ok := lastOccurred[ch]; ok && lastI >= start {
			start = lastI + 1
		}
		if i-start+1 > maxLength {
			maxLength = i - start + 1
		}
		lastOccurred[ch] = i
	}
	return maxLength
}

var lastOccurred = make([]int, 0xffff) //假定中文字的最大值为65535=0xffff
func lengthOfNonRepeatSubStr(s string) int {
	// lastOccurred := make([]int, 0xffff) //假定中文字的最大值为65535=0xffff
	for i := range lastOccurred {
		lastOccurred[i] = -1
	}
	start := 0
	maxLength := 0
	for i, ch := range []rune(s) {
		if lastI := lastOccurred[ch]; lastI != -1 && lastI >= start {
			start = lastI + 1
		}
		if i-start+1 > maxLength {
			maxLength = i - start + 1
		}
		lastOccurred[ch] = i
	}
	return maxLength
}

func main() {
	fmt.Println("this chapter 9.3")
	str := "黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花"
	fmt.Printf("%s lengthOfNonRepeatSubStr = %d ", str, lengthOfNonRepeatSubStr(str))
}

vim nonrepeating_test.go
package main

import "testing"

func BenchmarkSubstr(b *testing.B) {
	s := "黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花"
	for i := 0; i < 13; i++ {
		s = s + s
	}
	b.Logf("len(s) = %d", len(s))
	ans := 8
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		actual := lengthOfNonRepeatSubStr(s)
		if actual != ans {
			b.Errorf("got %d for input %s; "+"expected %d", actual, s, ans)
		}
	}
}

输出(没有优化):
$ go test -bench . -cpuprofile cpu.out
goos: linux
goarch: amd64
pkg: learngo/chapter9/9.2
BenchmarkSubstr-4            178           6580817 ns/op
--- BENCH: BenchmarkSubstr-4
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
PASS
ok      learngo/chapter9/9.2    2.016s
输出(优化rune字符串功能):
goos: linux
goarch: amd64
pkg: learngo/chapter9/9.2
BenchmarkSubstr-4            426           2756128 ns/op
--- BENCH: BenchmarkSubstr-4
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
PASS
ok      learngo/chapter9/9.2    1.612s
输出(将make 放在最外层):
goos: linux
goarch: amd64
pkg: learngo/chapter9/9.2
BenchmarkSubstr-4            486           2332484 ns/op
--- BENCH: BenchmarkSubstr-4
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
    nonrepeating_test.go:10: len(s) = 491520
PASS
ok      learngo/chapter9/9.2    1.511s

也可以通过命令行来测试cpu性能:
go test -bench . -cpuprofile cpu.out
可以通过tool进一步查看cpu.out 信息,  go tool pprof cpu.out ->交互终端输出 web就会显示出测试时间关系图,结果如下(需要安装graphviz)

在这里插入图片描述
从图中可以看到 2 个map 和 一个rune 发的时间较多,rune 暂时不适合优化,但是 map 可以适当优化为对应的数组
在这里插入图片描述
优化后的耗时图如下,课件优化后主要时间发在 stringtoslicerune 上了,当然还有少量时间发在 makeslice 上面(0.05s):
在这里插入图片描述
进一步将make 移到函数外面,可以发现压测时间进一步略微减少了,此时结果中已经没有 makeslice 了:
在这里插入图片描述

2.4 测试http服务器(上)

本案例基于 golang笔记08–go语言错误处理和资源管理 中的 filelisting 来测试 http 服务,具体示例如下:

chapter8/8.3$ tree -L 2
.
├── filelisting
│   └── handler.go
├── web.go
└── web_test.go

vim web_test.go
package main

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func errPanic(_ http.ResponseWriter,
	_ *http.Request) error {
	panic(123)
}

var tests = []struct {
	h       appHandler
	code    int
	message string
}{
	{errPanic, 500, "Internal Server Error"},
}

func TestErrWarpper(t *testing.T) {
	for _, tt := range tests {
		f := errWarpper(tt.h)
		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
		f(response, request)
		b, _ := ioutil.ReadAll(response.Body)
		body := strings.Trim(string(b), "\n") // web默认返回有个换行符,此处需要去掉才能正确匹配
		if response.Code != tt.code || body != tt.message {
			t.Errorf("expect (%d, %s); got (%d, %s)", tt.code, tt.message, response.Code, body)
		}
	}
}
输出:
=== RUN   TestErrWarpper
--- PASS: TestErrWarpper (0.00s)
PASS

2.5 测试http服务器(下)

http 测试通常包括两种方式:
1) 通过使用假的Request/Response
2)通过起服务器

本案例对上述 2.4 中的测试 case 进一步丰富, 使之能够测试更多异常情况,具体内容如下:

vim web2_test.go
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
)

func errPanic2(_ http.ResponseWriter,
	_ *http.Request) error {
	panic(123)
}

type testingUserError string

func (e testingUserError) Error() string {
	return e.Message()
}

func (e testingUserError) Message() string {
	return string(e)
}

func errUserError(_ http.ResponseWriter,
	_ *http.Request) error {
	return testingUserError("user error")
}

func errNotFound(_ http.ResponseWriter,
	_ *http.Request) error {
	return os.ErrNotExist
}

func errNoPermission(_ http.ResponseWriter,
	_ *http.Request) error {
	return os.ErrPermission
}

func errUnknown(_ http.ResponseWriter,
	_ *http.Request) error {
	return errors.New("unknown error")
}

func noError(writer http.ResponseWriter,
	_ *http.Request) error {
	fmt.Fprintln(writer, "no error")
	return nil
}

var tests = []struct {
	h       appHandler
	code    int
	message string
}{
	{errPanic2, 500, "Internal Server Error"},
	{errUserError, 400, "user error"},
	{errNotFound, 404, "Not Found"},
	{errNoPermission, 403, "Forbidden"},
	{errUnknown, 500, "Internal Server Error"},
	{noError, 200, "no error"},
}

func TestErrWarpper2(t *testing.T) {
	for _, tt := range tests {
		f := errWarpper(tt.h)
		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
		f(response, request)
		b, _ := ioutil.ReadAll(response.Body)
		body := strings.Trim(string(b), "\n") // web默认返回有个换行符,此处需要去掉才能正确匹配
		if response.Code != tt.code || body != tt.message {
			t.Errorf("expect (%d, %s); got (%d, %s)", tt.code, tt.message, response.Code, body)
		}
	}
}

func TestErrWarpperInserver(t *testing.T) {
	for _, tt := range tests {
		f := errWarpper(tt.h)
		server := httptest.NewServer(http.HandlerFunc(f))
		resp, _ := http.Get(server.URL)
		b, _ := ioutil.ReadAll(resp.Body)
		body := strings.Trim(string(b), "\n") // web默认返回有个换行符,此处需要去掉才能正确匹配
		if resp.StatusCode != tt.code || body != tt.message {
			t.Errorf("expect (%d, %s); got (%d, %s)", tt.code, tt.message, resp.StatusCode, body)
		}
	}
}
输出(TestErrWarpper2)=== RUN   TestErrWarpper2
gopm WARN error occurred handling request: user error
gopm WARN error occurred handling request: file does not exist
gopm WARN error occurred handling request: permission denied
gopm WARN error occurred handling request: unknown error
--- PASS: TestErrWarpper2 (0.00s)
PASS
输出(TestErrWarpperInserver)=== RUN   TestErrWarpperInserver
gopm WARN error occurred handling request: user error
gopm WARN error occurred handling request: file does not exist
gopm WARN error occurred handling request: permission denied
gopm WARN error occurred handling request: unknown error
--- PASS: TestErrWarpperInserver (0.00s)
PASS

2.6 生成文档和示例代码

go语言中可以通过go doc 查看代码的包和对应的函数,也可以查看系统文档功能。

1) 通过 go doc 查看queue 的文档信息
chapter6/queue$ go doc
package queue // import "learngo/chapter6/queue"

type Queue []interface{}

2)通过 go doc 查看具体数据结构信息
chapter6/queue$ go doc Queue
go doc Queue
package queue // import "."

type Queue []interface{}

func (q *Queue) IsEmpty() bool
func (q *Queue) Pop() interface{}
func (q *Queue) Push(v interface{})

3) 查看go库函数说明文档 
$ go doc fmt.println

4)输出godoc 的 http 服务器
$ godoc -http :6060
若在项目目录下起,则会包括项目的文档信息

5) 若需要添加文档,则直接将注释卸载函数上一或行即可,例如
// this is push interface
func (q *Queue) Push(v interface{}) {
	*q = append(*q, v)
}

6) go 中也可以新建Example示例代码,并能检查相关的结果,如果生成对应的godoc,则会生成对应的 Example的 Code 和 Output,具体代码如下
func ExampleQueue_Pop() {
	q := Queue{1}
	q.Push(2)
	q.Push(3)
	fmt.Println(q.Pop())
	fmt.Println(q.Pop())
	fmt.Println(q.IsEmpty())

	fmt.Println(q.Pop())
	fmt.Println(q.IsEmpty())

	// Output:
	// 1
	// 2
	// false
	// 3
	// true
}

通过本地 godoc 起一个文档查询服务器:
在这里插入图片描述

3 注意事项

  1. 查看 cpuprofile 信息时需要安装 graphviz
    apt install graphviz
    

4 说明

  1. 软件环境
    go版本:go1.15.8
    操作系统:Ubuntu 20.04 Desktop
    Idea:2020.01.04
  2. 参考文档
    由浅入深掌握Go语言 --慕课网
    go 语言编程 --许式伟
    graphviz download
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昕光xg

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值