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