本文来自网易云社区
作者:盛国存
现在呢我们就通过一个Http服务来展开如何统一处理服务器出错这件事,结合一个实际读取目录内文件的例子来简单介绍一下
func main() {
http.HandleFunc("/list/",
func(writer http.ResponseWriter, request *http.Request) {
path := request.URL.Path[len("/list/"):]
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
all, err := ioutil.ReadAll(file)
if err != nil {
panic(err)
}
writer.Write(all)
})
err := http.ListenAndServe(":2872", nil)
if err != nil {
panic(err)
}
}
因为在GOPATH下有一个 demo.txt
文件,浏览器输入一下地址 http://localhost:2872/list/demo.txt
,浏览器正确输出结果
万一我访问一个不存在的文件呢?会得到什么样的结果,比如我现在访问 http://localhost:2872/list/demo.txts
GOPATH目录下没有demo.txts文件,自然你会想到会panic一个错误
2018/05/04 17:08:54 http: panic serving [::1]:51946: open demo.txts: no such file or directory
goroutine 5 [running]:
net/http.(*conn).serve.func1(0xc4200968c0)
/usr/local/Cellar/go/1.10.2/libexec/src/net/http/server.go:1726 +0xd0
panic(0x124fde0, 0xc420086fc0)
/usr/local/Cellar/go/1.10.2/libexec/src/runtime/panic.go:502 +0x229
main.main.func1(0x12d1420, 0xc42010e000, 0xc42010a000)
/Users/verton/GoLangProject/src/shengguocun.com/web/web.go:52 +0x144
net/http.HandlerFunc.ServeHTTP(0x12aff28, 0x12d1420, 0xc42010e000, 0xc42010a000)
/usr/local/Cellar/go/1.10.2/libexec/src/net/http/server.go:1947 +0x44
net/http.(*ServeMux).ServeHTTP(0x140b3e0, 0x12d1420, 0xc42010e000, 0xc42010a000)
/usr/local/Cellar/go/1.10.2/libexec/src/net/http/server.go:2337 +0x130
net/http.serverHandler.ServeHTTP(0xc420089110, 0x12d1420, 0xc42010e000, 0xc42010a000)
从上面的部分的报错信息来看,
相关的错误信息都是 /usr/local/Cellar/go/1.10.2/libexec/src/net/http/server.go
的 serve
函数报出的,具体是哪一步报出的我就不细说了,有兴趣的可以自己按照例子自己查阅相关的源码,说到这那错误统一处理又是如何处理呢? 我们先把第一个panic替换成
path := request.URL.Path[len("/list/"):]
file, err := os.Open(path)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
我们再来访问上述地址
相比之前,提示稍微友好一点了,但是这对用户来讲还是不合适的,直接将程序内部错误信息输出给用户有些欠妥。我们可以包装成一个外部的Error,首先我们先定义一个函数appHandler, 返回一个error
type appHandler func(writer http.ResponseWriter, request *http.Request) error
然后定义一个 errWrapper 函数, 返回一个handler 里面需要的函数
type appHandler func(writer http.ResponseWriter, request *http.Request) error
func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handler(writer, request)
if err != nil {
switch {
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
}
}
}
然后将writer和request传进handler,通过switch判断err的类型,做一个统一的返回处理;这时我们需要将原来的业务逻辑的代码稍微做一下调整,
http.HandleFunc("/list/",
errWrapper(func(writer http.ResponseWriter, request *http.Request) error {
path := request.URL.Path[len("/list/"):]
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}
writer.Write(all)
return nil
}))
err := http.ListenAndServe(":2872", nil)
if err != nil {
panic(err)
}
http.HandleFunc
的第二个参数我们需要改为 errWrapper
同时将原来的函数作为参数传进去,当然这个函数为了代码的可读性应该单独抽离出来,相应的返回直接返回error就可以了,这时候我们再去访问之前的一个不存在的URL
这时候的错误就明显友好了很多,讲到这就是一个简单的统一错误处理的思路。
测试的作用对于一个软件行业从业者而言都是毋庸置疑的,Go语言在测试这块它有自己独特的见解,下面我们先介绍一下这两种模式下的测试
测试数据和测试逻辑混在一起
出错信息不明确
下面我们简单的举个例子:
public function testCase(){
assertEquals(2, add(1, 1));
assertEquals(1, add(1, 3));
assertEquals(0, add(1, -1));
}
很明显上面的几个特征它都占了,那下面我们来看一段Go语言的测试case
tests := []struct{
a, b, c int32
}{
{2, 1, 1},
{1, 1, 3},
{0, 1, -1},
}
for _, test := range tests {
if act := add(test.a, test.b); act != test.c {
// 相应的错误处理...
}
}
上述就是一个典型的表格驱动测试
分离测试数据和测试逻辑
明确的出错信息
Go语言的语法使得我们更容易使用表格驱动测试的测试模式
说了这么多,我们通常又是如何写测试用例呢?首先下面是一段加法的代码
package calculator
func Add(a, b int32) int32 {
return a + b
}
现在就写上面的函数的测试用例
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct{
a, b, c int32
}{
{1, 1, 2},
{-1, 1, 0},
}
for _, test := range tests {
if act := Add(test.a, test.b); act != test.c {
t.Errorf("%d + %d != %d 实际为 %d", test.a, test.b, test.c, act)
}
}
}
用IDE的同学直接点击 Run Test
就可以了,当然也同样支持命令行运行,进入到指定的文件目录下面
sheng$ go test ./
ok shengguocun.com/functional/calculator 0.006s
运行相关的执行命令就可以了,要是有错误的case依然不影响相关的测试的执行,比如:
package calculator
import (
"testing"
"math"
)
func TestAdd(t *testing.T) {
tests := []struct{
a, b, c int32
}{
{1, 1, 2},
{-1, 1, 0},
{math.MaxInt32, 1, math.MaxInt32},
}
for _, test := range tests {
if act := Add(test.a, test.b); act != test.c {
t.Errorf("%d + %d != %d 实际为 %d", test.a, test.b, test.c, act)
}
}
}
测试用例的执行结果为
sheng$ go test ./
--- FAIL: TestAdd (0.00s)
add_test.go:21: 2147483647 + 1 != 2147483647 实际为 -2147483648
FAIL
FAIL shengguocun.com/functional/calculator 0.006s
我们需要将不符合预期的case做出检查,看是否是代码逻辑有问题,还是case的问题,这就是一个完整的测试用例的编写的过程。
用IDE的同学我们会发现点击 Run Test
按钮的时候还有一个 with coverage 的选项
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
coverage: 100.0% of statements
Process finished with exit code 0
这就是一个测试用例的代码覆盖率的结果。
IDE这块有详细的覆盖率报告,可以看到左侧的绿色就是代码的覆盖的范围,右侧有详细的每个文件的覆盖率。当然除了IDE之外命令行也是同样支持的
sheng$ go test -coverprofile=a.out
PASS
coverage: 100.0% of statements
ok shengguocun.com/functional/calculator 0.006s
直接查看这个 a.out 文件,似乎看得不是很明白,当然我们有一个工具叫 go tool cover
sheng$ go tool cover -html=a.out
运行上面的命令,就会展现一个下面的静态页面
这就是一个详细的覆盖率报告
对于程序员而言,代码的性能是每个人都会去关注的,Go语言在性能测试这块依然有它的独特见解 Benchmark
func BenchmarkAdd(b *testing.B) {
aa := int32(math.MaxInt32 / 16)
bb := int32(math.MaxInt32 / 16)
cc := int32(math.MaxInt32 / 8) - 1
for i := 0; i < b.N; i ++ {
act := Add(aa, bb)
if act != cc {
b.Errorf("%d + %d != %d 实际为 %d",
aa, bb, cc, act)
}
}
}
上面就是一段性能测试代码,我们不需要关注这段代码具体要跑多少次,Go语言自身会帮你决定,IDE点击 Run Test
完,输出相关的结果
goos: darwin
goarch: amd64
pkg: shengguocun.com/functional/calculator
2000000000 0.35 ns/op
PASS
Process finished with exit code 0
总共跑了多少次以及每次的平均耗时,都会给出结果。当然同样支持命令行的交互方式
sheng$ go test -bench .
goos: darwin
goarch: amd64
pkg: shengguocun.com/functional/calculator
BenchmarkAdd-4 2000000000 0.34 ns/op
PASS
ok shengguocun.com/functional/calculator 0.721s
相关阅读:A Bite of GoLang (1)
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区。