错误处理属于编程中非主要但不可或缺的部分,优雅的错误处理不仅能让业务逻辑更清晰,还提高身心健康。
错误处理
Go支持多返回值,因此Go对错误的处理模式是将错误作为返回值返回。刚接触Go时,你一定会对return nil, err
这样的代码记忆尤新,同时也会对大量的if err != nil
感到力不从心。
将错误在返回值中返回的好处是你必须显示处理所有的错误,或者使用_
显示忽略。缺点是需要写大量的if err != nil
,这一步无法省略,但是可以减少类似的代码,当然,前提是业务逻辑是类似的。
我们以一个序列化Rect
结构体的例子来看如何优雅的处理错误。
type Rect struct {
X int32
Y int32
W int32
H int32
}
func Marshal_v1(r Rect) ([]byte, error) {
var buf bytes.Buffer
if err := binary.Write(&buf, binary.BigEndian, r.X); err != nil {
return nil, err
}
if err := binary.Write(&buf, binary.BigEndian, r.Y); err != nil {
return nil, err
}
if err := binary.Write(&buf, binary.BigEndian, r.W); err != nil {
return nil, err
}
if err := binary.Write(&buf, binary.BigEndian, r.H); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
- 闭包
将业务逻辑提取到闭包中,然后在一个地方统一处理错误。
func Marshal_v2(r Rect) (bs []byte, err error) {
var buf bytes.Buffer
marshal := func(i int32) {
if err == nil {
err = binary.Write(&buf, binary.BigEndian, i)
}
}
marshal(r.X)
marshal(r.Y)
marshal(r.W)
marshal(r.H)
bs = buf.Bytes()
return
}
- 结构体
type marsher struct {
buf bytes.Buffer
err error
}
func (m *marsher) write(i int32) {
if m.err == nil {
m.err = binary.Write(&m.buf, binary.BigEndian, i)
}
}
func Marshal_v3(r Rect) ([]byte, error) {
m := marsher{}
m.write(r.X)
m.write(r.Y)
m.write(r.W)
m.write(r.H)
if m.err != nil {
return nil, m.err
}
return m.buf.Bytes(), nil
}
通过闭包和结构体封装两种方式都能减少if err != nil
代码。仔细看就能发现,这两种方式都是将相似的业务逻辑抽象成函数,区别在于是闭包还是结构体的方法,其本质还是在复用。
然而这样的优化也不是无脑的,如果真的要写例子中的程序,下面的写法难道不更香吗?
func Marshal_v4(r Rect)(bs []byte) {
temp := make([]byte, 4)
binary.BigEndian.PutUint32(temp, uint32(r.X))
bs = append(bs, temp...)
binary.BigEndian.PutUint32(temp, uint32(r.Y))
bs = append(bs, temp...)
binary.BigEndian.PutUint32(temp, uint32(r.W))
bs = append(bs, temp...)
binary.BigEndian.PutUint32(temp, uint32(r.H))
bs = append(bs, temp...)
return bs
}
// 或者
func Marshal_v5(r Rect) (bs []byte, err error) {
var buf bytes.Buffer
err = binary.Write(&buf, binary.BigEndian, r)
if err == nil {
bs = buf.Bytes()
}
return
}
错误包装
似乎生产中很少会返回原生的错误类型,比如errors.New
。此外,我们习惯上会对错误进行包装然后返回,据说是为了方便查问题。包装错误也有3种方式。
fmt.Errorf
fmt.Errorf
是fmt
包提供的一个比较特殊的格式化函数,它的参数只能是error
类型。
func main() {
err := errors.New("inner")
err = fmt.Errorf("outter:%v", err)
fmt.Println(err)
}
- 结构体
type MyErr struct {
cause string
err error
}
func(e MyErr) Error() string {
return fmt.Sprintf("%s: %v", e.cause, e.err)
}
func main() {
err := errors.New("inner")
err = MyErr{"unknown", err}
fmt.Println(err)
}
- github.com/pkg/errors
func main() {
err := errors.New("inner")
err = errors.Wrap(err, "outer")
fmt.Println(err)
}
标准库的errors
包有4个方法:
errors.As
errors.Is
errors.New
errors.Unwrap
github.com/pkg/errors
包也有类似的函数,不知道你对他们是否感到好奇。他们的作用是什么,又是如何实现的。限于篇幅,我们下期见。