写错误也要优雅,必须优雅!go语言nil的漂亮用法

引言

尽管go有一个简单的错误模型,但乍一看,事情并不像它们应该的那样简单。在这篇文章中,我想提供一个很好的策略来处理错误并克服您在过程中可能遇到的问题。

首先,我们将分析go中的error。

然后我们将看到错误创建和错误处理之间的流程,并分析可能的缺陷。

最后探索一种解决方案,允许我们在不影响应用程序设计的情况下克服这些缺陷。

写错误也要优雅,必须优雅!go语言nil的漂亮用法

error

不语言中的错误类型是什么呢?下面是定义我们看一下。

<code> 
 

type

error

interface

{

Error

()

string

}/<code>

我们看到error是一个接口,它实现了一个返回字符串的简单方法error。

这个定义告诉我们错误就是一个简单的字符串,所以我们创建下面的结构。

<code>

type

MyCustomError

string

func

(err MyCustomError)

Error

()

string

{

return

string

(err) }/<code>

那要是这样的话,我想到一个简单的错误定义。

注意:这只是举个例子。我们可以创建一个错误使用go标准包fmt和errors:

<code>

import

(

"errors"

"fmt"

) simpleError := errors.New(

"a simple error"

) simpleError2 := fmt.Errorf(

"an error from a %s string"

,

"formatted"

)/<code>

这个错误处理的写法是不是很优雅?很简单。在本文的最后,我们将深入的探讨这个问题。


错误流处理

上面一小几节,我们已经知道什么是错误。下一步是可视化生命周期中的错误流程。

为了简单期间不要重复写累赘的代码。我们把错误处理抽象出来。

<code>

func

someFunc

()

(Result, error)

{ result, err := repository.Find(id)

if

err !=

nil

{ log.Errof(err)

return

Result{}, err }

return

result,

nil

}/<code>

上面这段代码的错误处理有什么不妥之处吗?原来我们通过首先记录错误,然后又返回错误,处理了两次。

试想如果团队开发,你的队友调用了这个错误处理函数,然后又手动的打印错误日志。这是不是糟糕极了?


假如我们的应用有3层,repository - interactor - web server,看下面的代码:

<code>

func

getFromRepository

(id

int

)

(Result, error)

{ result := Result{ID: id} err := orm.entity(&result)

if

err !=

nil

{

return

Result{}, err }

return

result,

nil

}/<code>

先是处理逻辑,然后从数据库拿数据。如果获取数据失败,返回故障信息。如果获取数据正常,直接返回数据。这是通常的做法,也是一种很成熟和稳定的方法。

上面的代码虽然逻辑上很合理。但是也有一个问题。go语言的错误处理没有堆栈跟踪,所以如果抛出异常,我们无法追踪到底是哪一行发生的错误。

pkg/errors库弥补了这个不足。

接着改进上面的代码。我们明确的指定错误抛出位置的信息。

<code>

import

"github.com/pkg/errors"

func

getFromRepository

(id

int

)

(Result, error)

{ result := Result{ID: id} err := orm.entity(&result)

if

err !=

nil

{

return

Result{}, errors.Wrapf(err,

"error getting the result with id %d"

, id); }

return

result,

nil

}/<code>

经过这样处理后,发生错误时返回的信息如下。

<code>/<code>

这个函数的作用,就是封装来自ORM的错误,在不影响原始信息的情况下,添加了堆栈跟踪的功能。

在interactor层的用法:

<code>

func

getInteractor

(idString

string

)

(Result, error)

{ id, err := strconv.Atoi(idString)

if

err !=

nil

{

return

Result{}, errors.Wrapf(err,

"interactor converting id to int"

) }

return

repository.getFromRepository(id) }/<code>

顶层web server的用法:

<code>r := mux.NewRouter()
r.HandleFunc(

"/result/{id}"

, ResultHandler)

func

ResultHandler

(w http.ResponseWriter, r *http.Request)

{ vars := mux.Vars(r) result, err := interactor.getInteractor(vars[

"id"

])

if

err !=

nil

{ handleError(w, err) } fmt.Fprintf(w, result) }

func

handleError

(w http.ResponseWriter, err error)

{ w.WriteHeader(http.StatusIntervalServerError) log.Errorf(err) fmt.Fprintf(w, err.Error()) }/<code>

大家看在顶层处理错误,完美吗?不完美。为什么呢?因为都是一些500的HTTP CODE,没什么用,给日志文件添加的都是无用的数据。

优雅的用法

上一段您也看到了,在web server层处理错误,不完美啊,都混沌了。

我们知道,如果我们在错误中引入新的内容,我们将以某种方式在创建错误的地方和最终处理错误的时候引入依赖项。

所以让我们来探索一个定义3个目标的解决方案:

  1. 提供良好的错误堆栈跟踪
  2. web层面的错误日志
  3. 必要时为用户提供上下文错误信息。(例如:所提供的电子邮件格式不正确)

首先创建一个错误类型。

<code>

package

errors

const

( NoType = ErrorType(

iota

) BadRequest NotFound )

type

ErrorType

uint

type

customError

struct

{ errorType ErrorType originalError error contextInfo

map

[

string

]

string

}

func

(error customError)

Error

()

string

{

return

error.originalError.Error() }

func

(

type

ErrorType)

New

(msg

string

)

error

{

return

customError{errorType:

type

, originalError: errors.New(msg)} }

func

(

type

ErrorType)

Newf

(msg

string

, args ...

interface

{})

error

{ err := fmt.Errof(msg, args...)

return

customError{errorType:

type

, originalError: err} }

func

(

type

ErrorType)

Wrap

(err error, msg

string

)

error

{

return

type

.Wrapf(err, msg) }

func

(

type

ErrorType)

Wrapf

(err error, msg

string

, args ...

interface

{})

error

{ newErr := errors.Wrapf(err, msg, args..)

return

customError{errorType: errorType, originalError: newErr} }/<code>

正如上面代码所示,只有ErrorType 和错误类型是公开可访问的。我们可以创建任意新的错误,或修饰已存在的错误。

但是有两件事情没有做到:

  • 如何在不导出customError的情况下检查错误类型?
  • 我们如何向错误中添加/获取上下文,甚至是向外部依赖项中已存在的错误中添加上下文?

改进上面的代码:

<code>

func

New

(msg

string

)

error

{

return

customError{errorType: NoType, originalError: errors.New(msg)} }

func

Newf

(msg

string

, args ...

interface

{})

error

{

return

customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))} }

func

Wrap

(err error, msg

string

)

error

{

return

Wrapf(err, msg) }

func

Cause

(err error)

error

{

return

errors.Cause(err) }

func

Wrapf

(err error, msg

string

, args ...

interface

{})

error

{ wrappedError := errors.Wrapf(err, msg, args...)

if

customErr, ok := err.(customError); ok {

return

customError{ errorType: customErr.errorType, originalError: wrappedError, contextInfo: customErr.contextInfo, } }

return

customError{errorType: NoType, originalError: wrappedError} }/<code>

现在让我们建立我们的方法处理上下文和任何一般错误的类型:

<code>

func

AddErrorContext

(err error, field, message

string

)

error

{ context := errorContext{Field: field, Message: message}

if

customErr, ok := err.(customError); ok {

return

customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context} }

return

customError{errorType: NoType, originalError: err, contextInfo: context} }

func

GetErrorContext

(err error)

map

[

string

]

string

{ emptyContext := errorContext{}

if

customErr, ok := err.(customError); ok || customErr.contextInfo != emptyContext {

return

map

[

string

]

string

{

"field"

: customErr.context.Field,

"message"

: customErr.context.Message} }

return

nil

}

func

GetType

(err error)

ErrorType

{

if

customErr, ok := err.(customError); ok {

return

customErr.errorType }

return

NoType }/<code>

现在回到我们的例子,我们要应用这个新的错误包:

<code>

import

"github.com/our_user/our_project/errors"

func

getFromRepository

(id

int

)

(Result, error)

{ result := Result{ID: id} err := orm.entity(&result)

if

err !=

nil

{ msg := fmt.Sprintf(

"error getting the result with id %d"

, id)

switch

err {

case

orm.NoResult: err = errors.Wrapf(err, msg);

default

: err = errors.NotFound(err, msg); }

return

Result{}, err }

return

result,

nil

}/<code>

interactor层的写法:

<code>

func

getInteractor(idString string) (Result, error) {

err := strconv.Atoi(idString)

if

err != nil {

err

=

errors.BadRequest.Wrapf(err, "interactor converting id to int")

err

=

errors.AddContext(err, "id", "wrong id format, should be an integer)

return

Result{}, err

}

return

repository.getFromRepository(id)

}

/<code>

最后web server层的写法:

<code>r := mux.NewRouter()
r.HandleFunc(

"/result/{id}"

, ResultHandler)

func

ResultHandler

(w http.ResponseWriter, r *http.Request)

{ vars := mux.Vars(r) result, err := interactor.getInteractor(vars[

"id"

])

if

err !=

nil

{ handleError(w, err) } fmt.Fprintf(w, result) }

func

handleError

(w http.ResponseWriter, err error)

{

var

status

int

errorType := errors.GetType(err)

switch

errorType {

case

BadRequest: status = http.StatusBadRequest

case

NotFound: status = http.StatusNotFound

default

: status = http.StatusInternalServerError } w.WriteHeader(status)

if

errorType == errors.NoType { log.Errorf(err) } fmt.Fprintf(w,

"error %s"

, err.Error()) errorContext := errors.GetContext(err)

if

errorContext !=

nil

{ fmt.Printf(w,

"context %v"

, errorContext) } }/<code>

写在最后

大家看到了,使用导出的类型和一些导出的值,我们可以更轻松地处理错误。

这个解决方案在创建错误时,也显式地显示了错误的类型,这很赞!


分享到:


相關文章: