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 错误 } 

到这里流式编程应该已经解释的足够清楚了,需要注意的是,这种编程方法的使用场景是有局限的

  • 它只适用于对于同一个业务对象的不断操作,在此基础上简化错误处理。

如果涉及多个业务对象,那么可能需要再仔细设计过整体的错误处理方式。