Loading

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

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

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

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

posted @ 2022-04-20 00:49  KawaiHe  阅读(1738)  评论(0编辑  收藏  举报