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 錯誤
}

到這里流式編程應該已經解釋的足夠清楚了,需要注意的是,這種編程方法的使用場景是有局限的

  • 它只適用於對於同一個業務對象的不斷操作,在此基礎上簡化錯誤處理。

如果涉及多個業務對象,那么可能需要再仔細設計過整體的錯誤處理方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM