Best practice to handle error from multiple abstra

2019-01-15 01:46发布

问题:

I wondering what is the best way to handle error form multiple level abstraction in go. Every time if I must add a new level abstraction to program, I am forced to transfer error code from level less to level high. Thereby is duplicate communitaces in log file or I must remmember to delete communicate form level low and transfer him to level higher. Below simply example. I skipped creating each object to more shortly and celar code, but I think You understand my problem

type ObjectOne struct{
    someValue int
}

func (o* ObjectOne)CheckValue()error{
    if o.someValue == 0 {
        SomeLogger.Printf("Value is 0 error program") // communicate form first level abstraction to logger
        return errors.New("Internal value in object is 0")
    }
    return nil
}

type ObjectTwoHigherLevel struct{
    objectOne ObjectOne
}

func (oT*  ObjectTwoHigherLevel)CheckObjectOneIsReady() error{
    if err := oT.objectOne.CheckValue() ; err != nil{
        SomeLogger.Printf("Value in objectOne is not correct for objectTwo %s" , err) //  second communicate
        return  err
    }
    return nil
}

type ObjectThreeHiggerLevel struct{
    oT ObjectTwoHigherLevel
}

func (oTh* ObjectThreeHiggerLevel)CheckObjectTwoIsReady()error{
    if err := oTh.oT.CheckObjectOneIsReady() ; err != nil{
        SomeLogger.Printf("Value in objectTwo is not correct for objectThree %s" , err)
    return err
    }
    return nil
}

In result in log file I get duplicate posts

Value is 0 error program 
Value in objectOne is not correct for objectTwo Internal value in object is 0 
Value in objectTwo is not correct for objectThree Internal value in object is 0

In turn if I only transfer some err to higher level without additional log I lost information what happend in each level.

How this solve ? How privent duplicate communicates ? Or My way is the good and the only ?

Problem is more frustrating if I create a few object which search something in database on a few abstraction level then I get also few lines form this same task in logFile.

回答1:

You should either handle an error, or not handle it but delegate it to a higher level (to the caller). Handling the error and returning it is bad practice as if the caller also does the same, the error might get handled several times.

Handling an error means inspecting it and making a decision based on that, which may be you simply log it, but that also counts as "handling" it.

If you choose to not handle but delegate it to a higher level, that may be perfectly fine, but don't just return the error value you got, as it may be meaningless to the caller without context.

Annotating errors

A really nice and recommended way of delegation is Annotating errors. This means you create and return a new error value, but the old one is also wrapped in the returned value. The wrapper provides the context for the wrapped error.

There is a public library for annotating errors: github.com/pkg/errors; and its godoc: errors

It basically has 2 functions: 1 for wrapping an existing error:

func Wrap(cause error, message string) error

And one for extracting a wrapped error:

func Cause(err error) error

Using these, this is how your error handling may look like:

func (o *ObjectOne) CheckValue() error {
    if o.someValue == 0 {
        return errors.New("Object1 illegal state: value is 0")
    }
    return nil
}

And the second level:

func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
    if err := oT.objectOne.CheckValue(); err != nil {
        return errors.Wrap(err, "Object2 illegal state: Object1 is invalid")
    }
    return nil
}

And the third level: call only the 2nd level check:

func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
    if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
        return errors.Wrap(err, "Object3 illegal state: Object2 is invalid")
    }
    return nil
}

Note that since the CheckXX() methods do not handle the errors, they don't log anything. They are delegating annotated errors.

If someone using ObjectThreeHiggerLevel decides to handle the error:

o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
    fmt.Println(err)
}

The following nice output will be presented:

Object3 illegal state: Object2 is invalid: Object2 illegal state: Object1 is invalid: Object1 illegal state: value is 0

There is no pollution of multiple logs, and all the details and context are preserved because we used errors.Wrap() which produces an error value which formats to a string which preserves the wrapped errors, recursively: the error stack.

You can read more about this technique in blog post:

Dave Cheney: Don’t just check errors, handle them gracefully

"Extending" errors

If you like things simpler and / or you don't want to hassle with external libraries and you're fine with not being able to extract the original error (the exact error value, not the error string which you can), then you may simply extend the error with the context and return this new, extended error.

Extending an error is easiest done by using fmt.Errorf() which allows you to create a "nice" formatted error message, and it returns you a value of type error so you can directly return that.

Using fmt.Errorf(), this is how your error handling may look like:

func (o *ObjectOne) CheckValue() error {
    if o.someValue == 0 {
        return fmt.Errorf("Object1 illegal state: value is %d", o.someValue)
    }
    return nil
}

And the second level:

func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
    if err := oT.objectOne.CheckValue(); err != nil {
        return fmt.Errorf("Object2 illegal state: %v", err)
    }
    return nil
}

And the third level: call only the 2nd level check:

func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
    if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
        return fmt.Errorf("Object3 illegal state: %v", err)
    }
    return nil
}

And the following error message would be presented at ObjectThreeHiggerLevel should it decide to "handle" it:

o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
    fmt.Println(err)
}

The following nice output will be presented:

Object3 illegal state: Object2 illegal state: Object1 illegal state: value is 0

Be sure to also read blog post: Error handling and Go



回答2:

There are various libraries that embed stack traces in Go errors. Simply create your error with one of those, and it will bubble up with the full stack context you can later inspect or log.

One such library:

https://github.com/go-errors/errors

And there are a few others I forgot.