)
文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载本章是《build-web-application-with-golang》实战手册的第十一章聚焦 Web 应用开发中最容易被忽视却决定线上质量的三件事错误处理error handling、GDB 除错debugging与测试testing。通过本章的学习读者将掌握 Go 语言 error 接口的设计哲学与自定义错误类型的方法学会用 GDB 动态查看变量、单步执行与修改运行环境并能基于内置的testing框架与go test命令编写单元测试和压力测试让后续的升级、重构与回归测试变得可控、可预期。本章导读为什么要在设计阶段就规划错误与测试很多开发者的编程时间实际上都花在检查 bug 和修复 bug 上——无论是编写新代码还是重构系统绝大部分时间都消耗在故障排除和测试上。外界常把程序员当作从无到有的系统设计师但事实上日常工作始终徘徊在排错、除错、测试之间。如果你有良好的习惯和技术方案直面这些问题就有机会把排错时间降到最低把时间花在更有价值的事情上。遗憾的是许多程序员不愿在错误处理、除错和测试能力上下工夫导致应用上线之后查错、定位问题反而耗费更多时间。因此在设计应用之前就做好错误处理规划、编写测试案例将来修改代码、升级系统都会变得简单。本章分为三个技术主线对应三节内容错误处理介绍 Go 语言中如何处理错误、如何设计自己的包与函数的错误处理使用 GDB 除错介绍如何使用 GDB 调试程序动态查看各种变量信息、监听运行情况并单步调试Go 怎么写测试案例深入探讨 Go 语言中的单元测试与压力测试说明go test的规则规范保证升级修改后运行相应测试代码即可完成最小化回归测试。错误处理Go 的 error 接口设计以显式 error 取代隐晦的返回值Go 语言的主要设计准则是简洁、明白简洁指语法与 C 类似明白指任何语句都很明显不含有任何隐含的东西。这一思想也贯彻在错误处理方案的设计中。C 语言通过返回 -1 或 NULL 来表示错误但用户不查看 API 文档根本搞不清返回值的含义——比如返回 0 究竟是成功还是失败。Go 定义了名为error的类型来明确表达错误使用时通过把返回的 error 变量与nil比较来判断操作是否成功。例如os.Open函数在打开文件失败时返回一个不为 nil 的 error 变量func Open(name string) (file *File, err error)下面的例子通过调用os.Open打开一个文件如果出现错误就调用log.Fatal输出错误信息f, err : os.Open(filename.ext) if err ! nil { log.Fatal(err) }类似os.Open标准库中所有可能出错的 API 都会返回一个 error 变量以方便调用方统一处理。error 类型一个方法定义一切的接口error 是一个内置的接口类型其定义可在/builtin/包中查找到type error interface { Error() string }内部很多包用到的 error 实现来自errors包下的私有结构体errorString// errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s }通过errors.New可以把一个字符串转化为errorString从而得到一个满足 error 接口的对象其内部实现如下// New returns an error that formats as the given text. func New(text string) error { return errorString{text} }以下例子示范了errors.New的典型用法——为负数平方根返回一个描述性错误func Sqrt(f float64) (float64, error) { if f 0 { return 0, errors.New(math: square root of negative number) } // implementation }调用Sqrt(-1)时会得到 non-nil 的 error 对象将其与 nil 比较结果为 true于是fmt.Println被调用并输出错误fmt 包在处理 error 时会调用Error()方法f, err : Sqrt(-1) if err ! nil { fmt.Println(err) }自定义 Error让错误携带更多上下文error 是一个接口因此实现自己的包时可以通过定义实现该接口的结构体来定制错误。来自 JSON 包encoding/json的SyntaxError就是一个经典范例type SyntaxError struct { msg string // 錯誤描述 Offset int64 // 錯誤發生的位置 } func (e *SyntaxError) Error() string { return e.msg }Offset字段在调用Error()时不会被打印但可以通过类型断言取得具体错误类型进而打印更丰富的错误信息if err : dec.Decode(val); err ! nil { if serr, ok : err.(*json.SyntaxError); ok { line, col : findLine(f, serr.Offset) return fmt.Errorf(%s:%d:%d: %v, f.Name(), line, col, err) } return err }需要注意函数返回自定义错误时返回值推荐设为 error 类型而不是自定义错误类型尤其不应预声明自定义错误类型的变量。否则可能导致上层调用者err ! nil的判断永远为 truefunc Decode() *SyntaxError { // 錯誤將可能導致上層呼叫者 err!nil 的判斷永遠為 true。 var err *SyntaxError // 預宣告錯誤變數 if 出錯條件 { err SyntaxError{} } return err // 錯誤err 永遠等於非 nil導致上層呼叫者 err!nil 的判斷始終為 true }当需要更复杂的错误处理时可以参考net包采用的方法——定义扩展接口在 error 基础上补充能力判定package net type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? }调用方通过类型断言判断err是否为net.Error从而细化处理策略。例如网络发生临时性错误时sleep 1 秒后重试if nerr, ok : err.(net.Error); ok nerr.Temporary() { time.Sleep(1e9) continue } if err ! nil { log.Fatal(err) }错误处理模式从散落的 if err 到统一的自定义路由器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 详解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) } }注册函数时改为func init() { http.Handle(/view, appHandler(viewRecord)) }对应的业务逻辑变得简洁清晰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) }更进一步可以把错误信息定义得更友好方便调试时定位问题。自定义错误类型携带 HTTP 状态码与面向用户的提示信息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, Cant display record, 500} } return nil }这个版本的代码量与第一版差不多但错误展示更明显、提示更友好、扩展性更好——这正是 Go Web 应用中统一错误出口 分级错误信息的推荐实践。使用 GDB 除错动态观测 Go 程序为什么需要 GDBGo 语言不像 PHP、Python 等动态语言——修改后无需编译即可直接运行并动态打印数据。Go 虽然可以用Println打印数据调试但每次都要重新编译非常麻烦。Python 有 pdb/ipdb、JavaScript 也有类似工具能够动态显示变量信息、单步调试Go 同样内置支持GDBFSF 自由软件基金会发布的类 UNIX 系统下的强大调试工具可用于调试 Go 程序。对于纯 Go 代码也可以考虑使用delve工具获得更好的调试体验。GDB 能做什么与编译前提使用 GDB 可以完成四类核心操作启动程序按开发者的自定义要求执行让被调试程序在开发者设定的断点处停住断点可以是条件表达式程序停住时检查此时程序中所发生的事动态改变当前程序的执行环境。两个重要的前提条件目前支持调试 Go 程序的 GDB 版本必须大于 7.1编译 Go 程序时需要注意两点一是传递-ldflags -s忽略 debug 打印信息二是传递-gcflags -N -l忽略 Go 内部做的优化如聚合变量和函数等优化这些优化会让 GDB 调试非常困难。常用 GDB 命令速查命令简写作用listl显示源码默认十行可带行号参数如list 15使第 15 行位于显示范围的中间breakb设置断点后跟行号如b 10在第 10 行设置断点deleted删除断点后跟断点序号序号可通过info breakpoints获取backtracebt打印执行的代码过程函数调用栈info locals—显示当前执行程序中的变量值info breakpoints—显示当前设置的断点列表info goroutines—显示当前执行的 goroutine 列表带 * 的表示当前执行的printp打印变量或其他信息配合$len()、$cap()可返回 string、slice、map 的长度与容量whatis—显示当前变量的类型如whatis msgnextn单步调试跳到下一步continuec跳出当前断点继续执行后跟参数 N 可跳过 N 次断点set variable—改变执行过程中的变量值格式set variable varvalue完整调试实战以下面这段并发示例程序为例演示 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) } }编译生成可执行文件注意关闭编译优化以便调试go build -gcflags -N -l gdbfile.go启动调试并运行程序gdb gdbfile (gdb) run Starting program: /home/xiemengjun/gdbfile Starting main count: 0 count: 1 ... [LWP 2771 exited] [Inferior 1 (process 2771) exited normally]设置断点并重新运行——b 23表示在第 23 行设置断点程序在fmt.Println(count:, count)处停住(gdb) b 23 Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. (gdb) run ... Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 23 fmt.Println(count:, count)用list查看断点附近的源码上下文用info locals/print/whatis检查变量值与类型(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) info locals count 0 bus 0xf840001a50 (gdb) p count $1 0 (gdb) whatis bus type chan int输入c继续执行每执行一轮 for 循环都会再次停在断点处而用set variable可以动态改写运行中的变量值观察修改后的结果(gdb) set variable count9 (gdb) info locals count 9 (gdb) c Continuing. count: 9最后用info goroutines观察整个并发结构每个 goroutine 的状态running / syscall / waiting / runnable并通过goroutine 1 bt查看指定 goroutine 的调用栈可以清楚看到main.main在 channel 接收chanrecv上的阻塞链路(gdb) info goroutines * 1 running runtime.gosched * 2 syscall runtime.entersyscall 3 waiting runtime.gosched 4 runnable runtime.gosched (gdb) goroutine 1 bt #0 0x000000000040e33b in runtime.gosched () at ... #1 ... in runtime.chanrecv ... #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22通过查看 goroutines可以清楚了解 goroutine 内部如何执行、每个函数的调用顺序。除上述命令外更多调试技巧可参考官方 GDB 调试手册。Go 怎么写测试案例testing 框架与 go test单元测试发现逻辑错误开发程序中很重要的一点是测试如何保证代码质量、每个函数可执行且结果正确、性能良好单元测试的重点在于发现编程或实现的逻辑错误使问题及早暴露、便于定位解决性能测试的重点在于发现编程设计上的问题让线上程序在高并发下保持稳定。Go 自带轻量级测试框架testing和go test命令来实现单元测试与性能测试也支持借助gotests插件自动生成测试代码可通过go get -u -v github.com/cweill/gotests/...安装。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 }编写单元测试必须遵循以下原则文件名必须以_test.go结尾这样执行go test时才会运行相应代码必须 importtesting包所有测试案例函数必须以Test开头测试案例会按照源码中写的顺序依次执行测试函数TestXxx()的参数是testing.T可用该类型记录错误或测试状态测试格式func TestXxx(t *testing.T)Xxx部分可以是任意字母数字组合但首字母不能是小写字母 [a-z]例如Testintdiv是错误函数名函数中通过调用testing.T的Error、Errorf、FailNow、Fatal、FatalIf方法说明测试不通过调用Log方法记录测试信息。测试案例代码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.00 seconds) gotest_test.go:16: 就是不透過 FAIL exit status 1 FAIL gotest 0.013s默认情况下go test不显示通过的测试信息需要带-v参数查看详细过程 RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds) gotest_test.go:11: 第一個測試通過了 RUN Test_Division_2 --- FAIL: Test_Division_2 (0.00 seconds) gotest_test.go:16: 就是不透過 FAIL exit status 1 FAIL gotest 0.012s把Test_Division_2改成对除零场景的断言期望返回非 nil 错误再次执行go test -v即全部通过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) //記錄一些你期望記錄的資訊 } } RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds) gotest_test.go:11: 第一個測試通過了 RUN Test_Division_2 --- PASS: Test_Division_2 (0.00 seconds) gotest_test.go:20: one test passed. 除數不能為 0 PASS ok gotest 0.013s压力测试验证性能与稳定性压力测试benchmark用于检测函数方法的性能写法与单元测试类似但需注意以下几点压力测试案例必须遵循格式func BenchmarkXXX(b *testing.B) { ... }其中 XXX 可为任意字母数字组合首字母不能是小写字母go test默认不执行压力测试函数需要带-test.bench参数语法为-test.benchtest_name_regex例如go test -test.bench.*表示测试全部压力测试函数在压力测试案例的循环体内必须使用testing.B.N使测试可以正常运行文件名同样必须以_test.go结尾。创建一个压力测试文件webbench_test.gopackage 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 webbench_test.go -test.bench.*Benchmark_Division-4 500000000 7.76 ns/op 456 B/op 14 allocs/op Benchmark_TimeConsumingFunction-4 500000000 7.80 ns/op 224 B/op 4 allocs/op PASS ok gotest 9.364s结果解读没有执行任何TestXXX单元测试只执行了压力测试函数。第一行显示Benchmark_Division执行了 500000000 次每次平均耗时 7.76 纳秒第二行Benchmark_TimeConsumingFunction执行 500000000 次每次平均 7.80 纳秒最后一行是总执行时间。值得注意的是Benchmark_TimeConsumingFunction中通过b.StopTimer()/b.StartTimer()把初始化工作排除在计时之外使性能数据只反映被测函数本身——这是编写高质量基准测试的重要技巧。本章小结让质量保障成为习惯本章通过三个小节分别介绍了 Go 语言中如何处理错误、如何设计错误处理方案如何使用 GDB 单步调试、查看/修改变量、打印执行过程以及如何利用 Go 自带的轻量级框架testing编写单元测试和压力测试、用go test方便地执行并完成回归测试。这一章对编写业务逻辑本身或许没有直接帮助但对保持代码的高质量至关重要一个好的 Web 应用必定有良好的错误处理机制提示友好、可扩展、有完善的单元测试和压力测试以保证上线后代码保持良好的性能并按预期运行。培养良好的除错与测试习惯就从当前项目的 Go Web 开发开始。下一章将进入部署与维护主题。相关阅读本章为《build-web-application-with-golang》目录中的第 11 章完整章节脉络可参考 SUMMARY.md错误处理源码范例可对照仓库中 en/code/src/mymath/sqrt.go 与各章节 code 目录 下的 Go 实现。赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐《Build Web Application with Golang》第 11 章精读Go Web 应用的错误处理、GDB 调试与单元测试实战《Build Web Application with Golang》第 11 章精读Go Web 应用的错误处理、GDB 调试与单元测试实战 本篇基于开源文档教程Build Web Application with Golang 第 11 章精讲Go 错误处理、GDB 调试与单元测试实战Build Web Application with Golang 第 11 章精讲Go 错误处理、GDB 调试与单元测试实战 本篇文章围绕开源 Go 电子书文档教程Go Web 开发实战错误处理、GDB 调试与测试build-web-application-with-golang 第 11 章Go Web 开发实战错误处理、GDB 调试与测试build web application with golang 第 11 章 本文以开源电子书《Bu文档教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考