开发 Web 应用过程当中,错误天然不免,开发者培养良好的处理错误、调试和测试习惯,能有效的提升开发效率,保证产品质量。web
Go 语言定义了一个叫作 error 的类型来显式表达错误,在使用时,经过把返回的 error 变量与 nil 进行比较来断定操做是否成功。数据库
例如 os.Open 函数在打开文件失败时将返回一个不为 nil 的 error 变量:安全
func Open(name string) (file *File, err error)
复制代码
使用示例:bash
package main
import (
"fmt"
"log"
"os"
)
func main() {
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
fmt.Println(f)
}
复制代码
执行以上代码,由于 filename.ext 文件不存在,控制台输出:app
2019/07/30 14:52:51 open filename.ext: no such file or directory
复制代码
相似于 os.Open 函数,标准包中全部可能出错的 API 都会返回一个 error 变量,以方便错误处理。框架
error 类型是一个接口类型,这是它的定义:函数
type error interface {
Error() string
}
复制代码
如下是 Go 语言 errors 包中的 New 函数的实现:工具
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
复制代码
如何使用 errors.New 的示例:oop
package main
import (
"errors"
"fmt"
"math"
)
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
} else {
return math.Sqrt(f), nil
}
}
func main() {
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt.Println(f)
}
复制代码
执行以上代码,由于 -1 小于 0,因此控制台输出:性能
math: square root of negative number
0
复制代码
若是把 -1 换成 4,控制台输出:
2
复制代码
error 是一个 interface,因此在实现本身的包时,经过定义实现此接口的结构,就能够实现本身的错误定义。
示例:
package main
import (
"fmt"
"math"
)
type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}
func (e *SyntaxError) Error() string { return e.msg }
// 求平方根
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, &SyntaxError{"负数没有平方根", 24}
} else {
return math.Sqrt(f), nil
}
}
func main() {
var fm float64 = -1
f, err := Sqrt(fm)
if err != nil {
if err, ok := err.(*SyntaxError); ok {
fmt.Printf("错误: 第 %v 行有误,%v。\n", err.Offset, err.msg)
}
return
}
fmt.Println(f)
}
复制代码
执行以上代码,由于 -1 小于 0,因此控制台输出:
错误: 第 24 行有误,负数没有平方根。
复制代码
Go 在错误处理上采用了与 C 相似的检查返回值的方式,而不是其它多数主流语言采用的异常方式,这形成了代码编写上的一个很大的缺点:错误处理代码的冗余,能够经过复用检测函数来减小相似处理错误的代码。
例如:
func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
复制代码
上面的例子中获取数据和模板展现调用时都有检测错误,当有错误发生时,调用了统一的处理函数 http.Error,返回给客户端 500 错误码,并显示相应的错误数据。可是当愈来愈多的 HandleFunc 加入以后,这样的错误处理逻辑代码就会愈来愈多,其实能够经过自定义路由器来缩减代码。
能够自定义 HTTP 处理 appHandler 类型,包括返回一个 error 值来减小重复:
type appHandler func(http.ResponseWriter, *http.Request) error
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
复制代码
ServeHTTP 方法调用 appHandler 函数,而且显示返回的错误(若是有的话)。注意这个方法的接收者——fn,是一个函数。(Go 能够这样作!)方法调用表达式 fn(w, r) 中定义的接收者。
如今当向 http 包注册了 viewRecord,就可使用 Handle 函数(代替 HandleFunc)appHandler 做为一个 http.Handler(而不是一个 http.HandlerFunc)。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
复制代码
当请求 /view 的时候,逻辑处理变成以下代码,和第一种实现方式相比较已经简单了不少。
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
复制代码
上面的例子错误处理的时候全部的错误返回给用户的都是 500 错误码,而后打印出来相应的错误代码,其实能够把这个错误信息定义的更加友好,调试的时候也方便定位问题,能够自定义返回的错误类型:
type appError struct {
Error error
Message string
Code int
}
复制代码
自定义路由器改为以下方式:
type appHandler func(http.ResponseWriter, *http.Request) *appError
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
复制代码
修改完自定义错误以后,逻辑处理改为以下方式:
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
复制代码
如上所示,在访问 view 的时候能够根据不一样的状况获取不一样的错误码和错误信息,虽然这个和第一个版本的代码量差很少,可是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。
Go 内部内置支持 GDB,可使用 GDB 进行调试。
GDB 是 FSF (自由软件基金会)发布的一个强大的类 UNIX 系统下的程序调试工具。使用 GDB 能够作以下事情:
编译Go程序的时候须要注意如下几点
GDB 的一些经常使用命令以下所示:
list
简写命令 l,用来显示源代码,默认显示十行代码,后面能够带上参数显示的具体行,例如:list 15,显示十行代码,其中第 15 行在显示的十行里面的中间,以下所示。
10 time.Sleep(2 * time.Second)
11 c <- i
12 }
13 close(c)
14 }
15
16 func main() {
17 msg := "Starting main"
18 fmt.Println(msg)
19 bus := make(chan int)
复制代码
break
简写命令 b,用来设置断点,后面跟上参数设置断点的行数,例如 b 10 在第十行设置断点。
delete
简写命令 d,用来删除断点,后面跟上断点设置的序号,这个序号能够经过 info breakpoints 获取相应的设置的断点序号,以下是显示的设置断点序号:
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time
复制代码
backtrace
简写命令 bt,用来打印执行的代码过程,以下所示:
#0 main.main () at /home/xiemengjun/gdb.go:23
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3 0x0000000000000000 in ?? ()
复制代码
info
info 命令用来显示信息,后面有几种参数,咱们经常使用的有以下几种:
info locals
显示当前执行的程序中的变量值
info breakpoints
显示当前设置的断点列表
info goroutines
显示当前执行的goroutine列表,以下代码所示,带*的表示当前执行的
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
复制代码
print
简写命令 p,用来打印变量或者其余信息,后面跟上须要打印的变量名,固然还有一些颇有用的函数 $len()
和 $cap()
,用来返回当前 string、slices 或者 maps 的长度和容量。
whatis
用来显示当前变量的类型,后面跟上变量名,例如 whatis msg,显示以下:
type = struct string
复制代码
next
简写命令 n,用来单步调试,跳到下一步,当有断点以后,能够输入n跳转到下一步继续执行。
coutinue
简称命令 c,用来跳出当前断点处,后面能够跟参数N,跳过多少次断点。
next
该命令用来改变运行过程当中的变量值,格式如:set variable =
最快捷的方法是使用brew来安装,命令以下:
brew install gdb
复制代码
安装完后,若是 MAC 系统调试程序会遇到以下错误:
(gdb) run
Starting program: /usr/local/bin/xxx
Unable to find Mach task port for process-id 28885: (os/kern) failure (0x5).
(please check gdb is codesigned - see taskgated(8))
复制代码
这是由于 Darwin 内核在你没有特殊权限的状况下,不容许调试其余进程。调试某个进程,意味着对这个进程有彻底的控制权限。因此出于安全考虑默认是禁止的。因此容许 gdb 控制其它进程最好的方法就是用系统信任的证书对它进行签名。
具体请查看 GDB Wiki:sourceware.org/gdb/wiki/Bu…
经过下面这个代码来演示如何经过 GDB 来调试 Go 程序,下面是将要演示的代码:
package main
import (
"fmt"
"time"
)
func counting(c chan<- int) {
for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
c <- i
}
close(c)
}
func main() {
msg := "Starting main"
fmt.Println(msg)
bus := make(chan int)
msg = "starting a gofunc"
go counting(bus)
for count := range bus {
fmt.Println("count:", count)
}
}
复制代码
编译文件,生成可执行文件gdbfile:
go build -gcflags "-N -l" gdbfile.go
复制代码
而后经过 gdb 命令启动调试:
gdb gdbfile
复制代码
启动以后首先看看这个程序是否是能够运行起来,只要输入 run 命令回车后程序就开始运行,程序正常的话能够看到程序输出以下,和在命令行直接执行程序输出是同样的:
(gdb) run
Starting program: /Users/play/goweb/src/error/gdbfile
[New Thread 0x1903 of process 4325]
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[Inferior 1 (process 4325) exited normally]
复制代码
如今程序已经跑起来了,接下来开始给代码设置断点:
(gdb) b 23
Breakpoint 1 at 0x108e0f5: file /Users/play/goweb/src/error/gdbfile.go, line 23.
(gdb) run
Starting program: /Users/play/goweb/src/error/gdbfile
[New Thread 0x2503 of process 4519]
[New Thread 0x2303 of process 4519]
Starting main
[New Thread 0x1803 of process 4519]
[New Thread 0x1903 of process 4519]
[New Thread 0x2203 of process 4519]
[New Thread 0x240f of process 4519]
[Switching to Thread 0x240f of process 4519]
Thread 6 hit Breakpoint 1, main.main () at /Users/play/goweb/src/error/gdbfile.go:23
23 fmt.Println("count:", count)
复制代码
上面例子 b 23 表示在第23行设置了断点,以后输入 run 开始运行程序。如今程序在前面设置断点的地方停住了,若是须要查看断点相应上下文的源码,输入 list 就能够看到源码显示从当前中止行的前五行开始:
(gdb) list
18 fmt.Println(msg)
19 bus := make(chan int)
20 msg = "starting a gofunc"
21 go counting(bus)
22 for count := range bus {
23 fmt.Println("count:", count)
24 }
25 }
复制代码
如今 GDB 在运行当前的程序的环境中已经保留了一些有用的调试信息,只需打印出相应的变量,查看相应变量的类型及值:
(gdb) info locals
count = 0
bus = 0xc420078060
msg = 0x10c107a "starting a gofunc"
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xc420078060
(gdb) whatis bus
type = chan int
复制代码
接下来该让程序继续往下执行,继续下面的命令:
(gdb) c
Continuing.
count: 0
Thread 6 hit Breakpoint 1, main.main () at /Users/play/goweb/src/error/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
Thread 6 hit Breakpoint 1, main.main () at /Users/play/goweb/src/error/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb)
复制代码
每次输入c以后都会执行一次代码,又跳到下一次for循环,继续打印出来相应的信息。
设想目前须要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果:
(gdb) info locals
count = 2
bus = 0xc420078060
msg = 0x10c107a "starting a gofunc"
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xc420078060
msg = 0x10c107a "starting a gofunc"
(gdb) c
Continuing.
count: 9
[Switching to Thread 0x2303 of process 4519]
Thread 2 hit Breakpoint 1, main.main () at /Users/play/goweb/src/error/gdbfile.go:23
23 fmt.Println("count:", count)
复制代码
最后查看前面整个程序运行的过程当中到底建立了多少个 goroutine,每一个 goroutine 都在作什么:
(gdb) info goroutines
* 1 running runtime.gopark
2 waiting runtime.gopark
3 waiting runtime.gopark
4 waiting runtime.gopark
5 waiting runtime.gopark
* 6 syscall runtime.systemstack_switch
17 waiting runtime.gopark
33 waiting runtime.gopark
(gdb) goroutine 1 bt
#0 runtime.mach_semaphore_wait () at /usr/local/go/src/runtime/sys_darwin_amd64.s:540
#1 0x0000000001024342 in runtime.semasleep1 (ns=-1, ~r1=0)
at /usr/local/go/src/runtime/os_darwin.go:438
#2 0x000000000104a5f3 in runtime.semasleep.func1 ()
at /usr/local/go/src/runtime/os_darwin.go:457
#3 0x0000000001024474 in runtime.semasleep (ns=-1, ~r1=3)
at /usr/local/go/src/runtime/os_darwin.go:456
#4 0x000000000100c869 in runtime.notesleep (n=0xc420034548)
at /usr/local/go/src/runtime/lock_sema.go:167
#5 0x000000000102bd55 in runtime.stopm () at /usr/local/go/src/runtime/proc.go:1952
#6 0x000000000102cf1c in runtime.findrunnable (gp=0xc420000180, inheritTime=false)
at /usr/local/go/src/runtime/proc.go:2415
#7 0x000000000102da2b in runtime.schedule ()
at /usr/local/go/src/runtime/proc.go:2541
#8 0x000000000102dd56 in runtime.park_m (gp=0xc420000180)
at /usr/local/go/src/runtime/proc.go:2604
#9 0x000000000104bd3b in runtime.mcall ()
at /usr/local/go/src/runtime/asm_amd64.s:351
#10 0x0000000000000000 in ?? ()
复制代码
开发程序其中很重要的一点是测试,Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试。
因为 go test 命令只能在一个相应的目录下执行全部文件,因此接下来新建一个项目目录 gotest,这样全部的代码和测试代码都在这个目录下。
接下来在该目录下面建立两个文件:gotest.go 和 gotest_test.go
package gotest
import (
"errors"
)
// 除法
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
复制代码
gotest_test.go 单元测试文件,记住下面的这些原则:
下面是测试用例的代码:
package gotest
import (
"testing"
)
func Test_Division_1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
t.Error("除法函数测试没经过") // 若是不是如预期的那么就报错
} else {
t.Log("第一个测试经过了") //记录一些你指望记录的信息
}
}
func Test_Division_2(t *testing.T) {
t.Error("就是不经过")
}
复制代码
在项目目录下面执行 go test,就会显示以下信息:
--- FAIL: Test_Division_2 (0.00s)
gotest_test.go:16: 就是不经过
FAIL
exit status 1
FAIL _/Users/play/goweb/src/error/gotest 0.005s
复制代码
从这个结果显示测试没有经过,由于在第二个测试函数中写死了测试不经过的代码 t.Error
,那么第一个函数执行的状况怎么样呢?默认状况下执行 go test
是不会显示测试经过的信息的,须要带上参数 go test -v
,这样就会显示以下信息:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00s)
gotest_test.go:11: 第一个测试经过了
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00s)
gotest_test.go:16: 就是不经过
FAIL
exit status 1
FAIL _/Users/play/goweb/src/error/gotest 0.005s
复制代码
上面的输出详细的展现了这个测试的过程,能够看到测试函数1 Test_Division_1
测试经过,而测试函数2 Test_Division_2
测试失败了,最后得出结论测试不经过。接下来把测试函数 2 修改为以下代码:
func Test_Division_2(t *testing.T) {
if _, e := Division(6, 0); e == nil { //try a unit test on function
t.Error("Division did not work as expected.") // 若是不是如预期的那么就报错
} else {
t.Log("one test passed.", e) //记录一些你指望记录的信息
}
}
复制代码
而后执行 go test -v
,就显示以下信息,测试经过了:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00s)
gotest_test.go:11: 第一个测试经过了
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00s)
gotest_test.go:19: one test passed. 除数不能为0
PASS
ok _/Users/play/goweb/src/error/gotest 0.005s
复制代码
压力测试用来检测函数(方法)的性能,须要注意如下几点:
压力测试用例必须遵循以下格式,其中 XXX 能够是任意字母数字的组合,可是首字母不能是小写字母
func BenchmarkXXX(b *testing.B) { ... }
复制代码
go test 不会默认执行压力测试的函数,若是要执行压力测试须要带上参数 -test.bench,语法:-test.bench="test_name_regex",例如 go test -test.bench=".*" 表示测试所有的压力测试函数
在压力测试用例中,请记得在循环体内使用 testing.B.N
,以使测试能够正常的运行
文件名也必须以 _test.go 结尾
新建一个压力测试文件 webbench_test.go,代码以下所示:
package gotest
import (
"testing"
)
func Benchmark_Division(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Division(4, 5)
}
}
func Benchmark_TimeConsumingFunction(b *testing.B) {
b.StopTimer() //调用该函数中止压力测试的时间计数
//作一些初始化的工做,例如读取文件数据,数据库链接之类的,
//这样这些时间不影响咱们测试函数自己的性能
b.StartTimer() //从新开始时间
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
复制代码
执行命令 go test -run="webbench_test.go" -test.bench=".*"
,能够看到以下结果:
goos: darwin
goarch: amd64
Benchmark_Division-4 2000000000 0.29 ns/op
Benchmark_TimeConsumingFunction-4 2000000000 0.59 ns/op
PASS
ok _/Users/play/goweb/src/error/gotest 1.856s
复制代码
上面的结果显示没有执行任何 TestXXX 的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了Benchmark_Division
执行了 2000000000 次,每次的执行平均时间是 0.29 纳秒,第二条显示了 Benchmark_TimeConsumingFunction
执行了 2000000000,每次的平均执行时间是 0.59 纳秒。最后一条显示总共的执行时间 1.856s。