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
}