初始化多种资源并管理其寿命

Suppose you have to initialize a chain of resources in order to do something, typically with one initialization depending on the next. For example, you need to launch a browser, to open a browser window, to open a tab, to navigate that tab to a web site. At the end of the operation, you want to close or tear down every resource you have initialized.

Let's look at this naive code:

func main() {
  window, err := NewWindow()
  if err != nil {
    panic(err)
  }
  defer window.Close()

  tab, err := NewTab(window)
  if err != nil {
    panic(err)
  }
  defer tab.Close()

  NavigateToSite(tab)
}

(Of course, this code is pretty simple, so one might ask why ever refactor it, so keep in mind it's example's sake, and the actual chain of initializations might be longer and more convoluted.)

Suppose then I want to factor out the initialization, noticing that the actual logic in my code doesn't need the window at all. What would be an idiomatic way to do it? So far I can think of:

func main() {
  rs, err := NewMyResource()
  if err != nil {
     panic(err)
  }
  defer rs.Close()

  NavigateToSite(rs.Tab)
}

struct MyResource {
  Window *window;
  Tab *tab;
}

func NewMyResource() (*MyResource, error) {
  rs := &MyResource{}

  window, err := CreateWindow()
  if err != nil {
    rs.Close()
    return nil, err
  }
  rs.Window = window

  tab, err := CreateTab()
  if err != nil {
    rs.Close()
    return nil, err
  }
  rs.Tab := tab

  return rs, nil
}

func (rs MyResource) Close() {
  if rs.Window != nil {
    rs.Window.Close()
  }

  if rs.Tab != nil {
    rs.Tab.Close()
  }
}

A possible alternative (not necessarily better, it depends on context) might be to return a closure:

func NewMyResource() (tab Tab, closer func(), err error) {
    var window Window
    window, err = NewWindow()
    if err != nil {
        return
    }

    tab, err = NewTab(window)
    if err != nil {
        return
    }
    closer = func() {
        tab.Close()
        window.Close()
    }
    return
}

Using it something like:

tab, cl, err := NewMyResource()
if err != nil {
    panic(err)
}
defer cl()

I would generally go with the struct-based solution, but sometimes a new type is overkill and returning a function is easier.

A fairly complex solution (elegance is left for you to judge) involves a generic helper function:

func ContextCloser(ctx context.Context, closer io.Closer) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                closer.Close()
                return
            }
        }
    }()
}

This helper function allows the leverage contexts for resource management, somewhat similar to an allocation/release pool. Let me show you how:

struct MyResource {
  ContextCancel context.CancelFunc
  // NOTE: we no longer keep `window` etc. for interim variables
  //       that our caller doesn't really want and we only kept
  //       so we could close them --
  //       ContextCloser will take care of it!
  Tab *tab;
}

func NewMyResource(ctx context.Content) (*MyResource, error) {
  var err error
  ctx, ctxCancel := context.WithCancel(ctx)

  // This ensures resources are closed if this function fails
  defer func() {
    if err != nil {
        ctxCancel()
    }
  }()

  // We need to create a window in order to create a tab,
  // but we don't need to return it
  window, err := CreateWindow()
  if err != nil {
    return nil, err
  }
  ContextCloser(ctx, window)

  tab, err := CreateTab()
  if err != nil {
    return nil, err
  }
  ContextCloser(ctx, tab)

  return &MyResource{ContextCancel: ctxCancel, Tab: tab}, nil
}

Finally, for completeness sake, let's illustrate how it's being called:

func main() {
  // (you can use this context for more than just NewMyResource)
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()

  rs, err := NewMyResource(ctx)
  if err != nil {
     panic(err)
  }

  NavigateToSite(rs.Tab)
}