A Bite of GoLang (4)

叁叁肆2018-09-19 11:15

本文来自网易云社区


作者:盛国存


6.3、服务器统一出错处理

现在呢我们就通过一个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.goserve 函数报出的,具体是哪一步报出的我就不细说了,有兴趣的可以自己按照例子自己查阅相关的源码,说到这那错误统一处理又是如何处理呢? 我们先把第一个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


这时候的错误就明显友好了很多,讲到这就是一个简单的统一错误处理的思路。


7. 测试和性能调优

7.0、测试

1、传统测试 VS 表格驱动测试

测试的作用对于一个软件行业从业者而言都是毋庸置疑的,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语言的语法使得我们更容易使用表格驱动测试的测试模式

2、实例演示

说了这么多,我们通常又是如何写测试用例呢?首先下面是一段加法的代码

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的问题,这就是一个完整的测试用例的编写的过程。


7.1、代码覆盖率和性能测试

1、代码覆盖率

用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

运行上面的命令,就会展现一个下面的静态页面

这就是一个详细的覆盖率报告

2、性能测试

对于程序员而言,代码的性能是每个人都会去关注的,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)

A Bite of GoLang (2)

A Bite of GoLang (3)

A Bite of GoLang (4)

A Bite of GoLang (5)

A Bite of GoLang (6)

A Bite of GoLang (7)

A Bite of GoLang (8)


网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区