Golang 中处理 error 的几种方式
节选自 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 错误
}
到这里流式编程应该已经解释的足够清楚了,需要注意的是,这种编程方法的使用场景是有局限的:
- 它只适用于对于同一个业务对象的不断操作,在此基础上简化错误处理。
如果涉及多个业务对象,那么可能需要再仔细设计过整体的错误处理方式。