節選自 Go 語言編程模式:錯誤處理
if err != nil
基礎的處理方式 Go 語言的一大特點就是 if err != nil
,很多新接觸 golang 的人都會非常不習慣,一個常見的函數可能是這樣的:
func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
}
通過 Closure 處理 error
我們可以通過 Closure 的方式來處理 error:
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&p.Longitude)
read(&p.Latitude)
read(&p.Distance)
read(&p.ElevationGain)
read(&p.ElevationLoss)
if err != nil {
return &p, err
}
return &p, nil
}
上面代碼中,我們定義了匿名函數 read
封裝了 error 的處理,相比於第一種方式,整個代碼簡潔了很多,但依然有一個 err 變量和內部函數。
將 error 定義在 Receiver 中
bufio.Scanner 源碼示例
從 Go 語言的 bufio.Scanner()
中我們可以看到另一種不同的錯誤處理方法:
func main() {
// An artificial input source.
const input = "Now is the winter of our discontent,\nMade glorious summer by this sun of York.\n"
scanner := bufio.NewScanner(strings.NewReader(input))
// Set the split function for the scanning operation.
scanner.Split(bufio.ScanWords)
// Count the words.
count := 0
for scanner.Scan() {
count++
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
fmt.Printf("%d\n", count)
}
// Output: 15
在上面代碼中,當 scanner 操作底層 io 的時候,for-loop 中沒有任何的 if err != nil
,而是在循環結束之后對 scanner.Err()
進行錯誤處理。
從 bufio.Scanner
的源碼中,我們可以看到它其實是采用了將 error 定義在 Receiver 中的方式:
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
從 bufio.Scan()
的源碼中可以看出,每次通過 Scanner 調用 Scan()
方法時,在方法內部會對 Scanner 中的 err
進行校驗:
func (s *Scanner) Scan() bool {
if s.done {
return false
}
s.scanCalled = true
// 循環處理
for {
// 僅當沒有 error 的時候才處理
if s.end > s.start || s.err != nil {
// process
}
}
// process
}
demo 示例
這里我們按照 bufio.Scanner
的方式對之前的 demo 進行改造:
// 定義 receiver
type Point struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.Longitude)
r.read(&p.Latitude)
r.read(&p.Distance)
r.read(&p.ElevationGain)
r.read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
個人認為上面的改造對於這個 demo 來說是不合適的:它讓代碼的整體可讀性變差了。
這種方式在 bufio.Scanner
中是合適的,因為我們主要是在循環中調用對應方法,定義在 Receiver 可以讓整個代碼變得簡潔優雅;只需要在循環開始處注釋一下,整個代碼的可讀性也不會受到多大影響。
recevier 中定義 error + 流式編程
流式編程我第一次看到是在 Java 中,Go 語言的 GORM 也是這種風格的 API,例如我們需要查找表 User
中的第一條記錄:
db.Model(&User{}).First(&result)
通過把 error 定義在 Receiver 中,我們也可以將 demo 改造成這種流式編程的風格:
// 定義 receiver
type Point struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) *Reader {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
return r
}
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r = r.read(&p.Longitude).
read(&p.Latitude).
read(&p.Distance).
read(&p.ElevationGain).
read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
下面是另一個流式編程的例子,也是將 error 定義在 Receiver 中,不過它沒有對 read()
方法進行改造,而是在其基礎上包裝了對外的流式編程接口:
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
// 長度不夠,少一個Weight
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c}
var r = bytes.NewReader(b)
type Person struct {
Name [10]byte
Age uint8
Weight uint8
err error
}
func (p *Person) read(data interface{}) {
if p.err == nil {
p.err = binary.Read(r, binary.BigEndian, data)
}
}
func (p *Person) ReadName() *Person {
p.read(&p.Name)
return p
}
func (p *Person) ReadAge() *Person {
p.read(&p.Age)
return p
}
func (p *Person) ReadWeight() *Person {
p.read(&p.Weight)
return p
}
func (p *Person) Print() *Person {
if p.err == nil {
fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)
}
return p
}
func main() {
p := Person{}
p.ReadName().ReadAge().ReadWeight().Print()
fmt.Println(p.err) // EOF 錯誤
}
到這里流式編程應該已經解釋的足夠清楚了,需要注意的是,這種編程方法的使用場景是有局限的:
- 它只適用於對於同一個業務對象的不斷操作,在此基礎上簡化錯誤處理。
如果涉及多個業務對象,那么可能需要再仔細設計過整體的錯誤處理方式。