Home > database >  Is there a common idiom for conditionally deferring resource cleanup on error?
Is there a common idiom for conditionally deferring resource cleanup on error?

Time:10-10

I have a function that's going to open a bunch of resources and bundle them together into a return struct. Something like this:

type Bundle struct {
  a,b,c ExpensiveResource
}

func NewBundle() (Bundle, error) {
  var bundle Bundle
  bundle.a, err = GetExpensiveResource()
  if err != nil { return Bundle{}, err }
  bundle.b, err = GetAnotherExpensiveResource()
  if err != nil { return Bundle{}, err }
  bundle.c, err = GetAThirdExpensiveResource()
  if err != nil { return Bundle{}, err }
  return bundle, nil
}

If GetAThirdExpensiveResource fails, then bundle.a and bundle.b leak. Is there a recommended idiom for handling this? I came up with a closeOnError function like this:

func NewBundle() (Bundle, error) {
  var bundle Bundle
  var err error
  destroyOnError:= func (r ExpensiveResource) func() {
    return func () { if err != nil { r.Destroy() } }
  }
  bundle.a, err = GetExpensiveResource()
  if err != nil { return Bundle{}, err }
  defer destroyOnError(bundle.a)()
  // and so on

But for reasons I can't quite articulate this seems clunky. Is there a better way?

CodePudding user response:

type Bundle struct {
    a, b, c ExpensiveResource
}

func (b *Bundle) destroy() {
    if b.a != nil {
        // destroy a
    }
    if b.b != nil {
        // destroy b
    }
    if b.c != nil {
        // destroy c
    }
}

func NewBundle() (b Bundle, err error) {
    defer func() {
        if err != nil {
            b.destroy()
        }
    }()

    if b.a, err = GetExpensiveResource(); err != nil {
        return Bundle{}, err
    }
    if b.b, err = GetAnotherExpensiveResource(); err != nil {
        return Bundle{}, err
    }
    if b.c, err = GetAThirdExpensiveResource(); err != nil {
        return Bundle{}, err
    }
    return b, nil
}

CodePudding user response:

The ultimate solution to this is somewhat subjective. You would only need cleanup for resources that might leak, like things that open connections or start goroutines.

You can use pointers to denote initialization:


bundle.rsc1, err=construct1()
if err!=nil {
   bundle.cleanup()
   return err
}
bundle.rsc2, err=construct2()
if err!=nil {
   bundle.cleanup()
   return err
}

where:

func (b *Bundle) cleanup () {
   if b.rsc1!=nil {
     b.rsc1.Close()
   }
   if b.rsc2!=nil {
     b.rsc2.Close()
   }
   ...
}

You can use flags:

var rsc1Initialied, rsc2Initialized ... bool

cleanup:=func() {
  if rsc1Initialized {
    bundle.rsc1.Close()  
  }
  if rsc2Initialized {
     bundle.rsc2.Close()
  }
}

bundle.rsc1, err= construct1()
if err!=nil {
   cleanup()
   return err
}
rsc1Initialized=true
...

You can queue-up cleanup methods:

cleaners:=make([]func(), 0)

cleanup:=func() {
  for _,x:=range cleaners {
     x()
  }
}

bundle.rsc1, err=construct1()
if err!=nil {
   cleanup()
   return err
}
cleaners=append(cleaners,func() {bundle.rsc1.Close()})
...

CodePudding user response:

This is fairly simple, if necessary (depending on type of ExpensiveResource) add zero-value checks in the destroy() method.

type Bundle struct {
    a, b, c ExpensiveResource
}

func (bundle Bundle) destroy() {
    bundle.a.Destroy()
    bundle.b.Destroy()
    bundle.c.Destroy()
}

func NewBundle() (Bundle, error) {
    var err error
    var bundle Bundle
    if bundle.a, err = GetExpensiveResource(); err == nil {
        if bundle.b, err = GetExpensiveResource(); err == nil {
            if bundle.c, err = GetExpensiveResource(); err == nil {
                return bundle, nil
            }
        }
    }
    bundle.destroy()
    return Bundle{}, err
}
  •  Tags:  
  • go
  • Related