节选自 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:
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 变量和内部函数。
从 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 }
这里我们按照 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 可以让整个代码变得简洁优雅;只需要在循环开始处注释一下,整个代码的可读性也不会受到多大影响。
流式编程我第一次看到是在 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 错误 }
到这里流式编程应该已经解释的足够清楚了,需要注意的是,这种编程方法的使用场景是有局限的:
如果涉及多个业务对象,那么可能需要再仔细设计过整体的错误处理方式。