douyu9159 2016-05-20 12:25
浏览 54
已采纳

从多个抽象级别处理错误的最佳实践

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.

  • 写回答

2条回答 默认 最新

  • douyuepi6485 2016-05-20 13:05
    关注

    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

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 基于FOC驱动器,如何实现卡丁车下坡无阻力的遛坡的效果
  • ¥15 IAR程序莫名变量多重定义
  • ¥15 (标签-UDP|关键词-client)
  • ¥15 关于库卡officelite无法与虚拟机通讯的问题
  • ¥100 已有python代码,要求做成可执行程序,程序设计内容不多
  • ¥15 目标检测项目无法读取视频
  • ¥15 GEO datasets中基因芯片数据仅仅提供了normalized signal如何进行差异分析
  • ¥100 求采集电商背景音乐的方法
  • ¥15 数学建模竞赛求指导帮助
  • ¥15 STM32控制MAX7219问题求解答