Abstract Devices: Implementing an “Opener” interface pattern in golang

There are many examples of providing abstract interfaces to resource management in golang. For example, the io.Reader and io.Writer interfaces can be implemented by different concrete reading and writing packages such as io.LimitReader, io.MultiReader, io.TeeReader and so forth.

The apparent simplicity of the io.Reader and io.Writer can then hide a multitude of implementations both complicated and simple, which aids both testing and future-proofing, amongst other advantages.

How might one go about implementing something similar? The simplest pattern might provide two methods: Open to “grab” or “obtain” resources and Close to release them. Another consideration might be how to tie this abstract pattern to your actual “concrete” implementation. In this article, I explain how I implemented the abstract pattern (an interface in golang) and concrete implementation (the “Driver”).

The opener module defines two interfaces and one function as follows:

package opener
type Config interface {
Open() (Driver, error)
}
type Driver interface {
Close() error
}
func Open(config Config) (Driver, error) {
return config.Open()
}

The Config interface is used to open the specific concrete Driver by calling the concreteOpen method from the more generic one. The “concrete” implementation is provided in a separate module, which could imaginatively be called concrete. Within this module I have two structs, one of which provides the configuration parameters and the other the state required to maintain the instance. There are also Open and Close methods:

package concrete
import (
"opener"
)
type Config struct {
// Put anything in here we need to use to configure resources
}
type Driver struct {
// Put anything in here to maintain state for resources
}
func (config Config) Open() (opener.Driver, error) {
// This function satisfies the opener.Config interface
  this := new(Driver)
  // Use the config variable to retrieve config info
// and grab resources required, or "return nil, err"
// if Open should fail
  return this, nil
}
func (this *Driver) Close() error {
// This function satisfies the opener.Driver interface
  // Free resources here. If error, "return err", but it may
// be ignored. Probably a good idea to consider what happens
// if this function is called more than once.
  return nil
}

Once you’ve implemented both your abstract opener module and one or more concrete implementation modules, your main function might work something like this:

package main
import (
"opener"
"concrete"
)
func main() {
driver, err := opener.Open(concrete.Config{ /* parameters */ })
if err != nil {
/* handle open error */
}
defer driver.Close()
  /* ... */
}

Of course, you need to write code to replace the ... comment in order to do something, so why not just think about another abstract interface in order to achieve this? If you want to implement a window system for example, you might have methods to open, close and move windows:

package windows
import (
"opener"
)
type Driver struct {
// Implement the general opener interface
opener.Driver
  // Our window driver implements Open & Close methods
// for windows. Also moving windows on the screen...
OpenWindow(Origin,Size) (Window, error)
CloseWindow(Window) error
MoveWindow(Origin) error
}
// Origin and Size are pretty standard...
type Origin struct { X, Y int }
type Size struct { W, H uint }
// Window may represent any struct, or may have methods of its' own.
type Window interface { }

The windows.Driver simply inherits the Close method from the opener.Driver abstract interface. I think it also has the advantage of providing very readable and concise documentation with no unnecessary implementation detail.

One gotcha here is that the opener.Open method will always return a pretty generic opener.Driver object which you’ll need to cast to your slightly more specific windows driver interface. You might find this ugly, and the operation of casting to a different interface might be expensive in CPU as it ensures the cast satisfies the interface (or panics with error message).

In order to illustrate this, here’s a re-written main function for opening a window from the driver:

package main
import (
"opener"
"windows"
"concrete"
)
func main() {
windows_driver, err := opener.Open(concrete.Windows{ })
if err != nil {
/* handle open error */
}
defer windows_driver.Close()
  // Create a new window
window, err := windows_driver.(windows.Driver).OpenWindow(/*..*/)
if err != nil {
/* handle open error */
}
defer windows_driver.(windows.Driver).CloseWindow(window)
  /* ... do stuff ... */
}

Now we can finally see one of the advantages of implementing your code this way. If I wanted a separate windowing implementation (say, with a module called concrete_v2 or some other snappy name) all I need then to do in the main function is to replace the concrete.Windows{ } argument with concrete_v2.Windows{ } and everything else should work the same, assuming my new implementation can satisfy the windows.Driver interface. You can see how this might aid testing (through mock implementation objects) and platform portability…


In summary, I found that by implementing my code with this pattern, I was able to separate my thinking about how something should be “interfaced” and how it should be “implemented”. It’s something I recommend you think about when you’re designing and implementing any resource-grabbing code.