開發 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。