Effortless Resource Management: A Simple Context Manager Implementation in Go

Photo by cottonbro studio: https://www.pexels.com/photo/brown-wooden-brush-on-brown-wooden-table-4108711/

Introduction

Many languages have the concept of a context manager. This is a way of efficiently and automatically cleaning up resources. To be fair, this is just a first attempt, please let me know if you have suggestion on how to improve on the code.

The code and the package are on https://github.com/snoekiede/gocontextmanager

C#

C# has the using keyword:

using(var someObject=Some object implementing the IDisposable interface) {
   ....
}

At the end of the block, the Dispose() method of the IDisposable interface is called on someObject to do the clean up. This is a language feature.

Python

In Python you can use the with statement with much the same effect. If you have implemented the __enter__ and __exit__ on your object, you can clean up after the with block has ended.

Rust

Rust has perhaps the most elegant method of cleaning up resources thanks to Rust’s borrowing system. In Rust you can implement your custom Drop trait on structs so they can automatically clean up resources when they go out of scope.

Back to Go

It would be nice if we had something similar in Go. For starters you have the defer keyword in Go, which can be used for automatic clean up. However, inspired by the other languages, so I tried to build a very simple context manager, and here it is:

package contextmanager

type ContextManager[T any] struct {
	Context T
}

func (cm *ContextManager[T]) Dispose(dispose func(a T)) {
	dispose(cm.Context)
}

func WithContext[T any, O any](context T, action func(a T) (O, error), dispose func(element T)) (O, error) {
	resource := &ContextManager[T]{Context: context}
	defer resource.Dispose(dispose)
	return action(resource.Context)
}

The ContextManager is a generic type, with only one field: the Context.

The WithContext() method requires some explanation:

  1. The method is generic, T is used for the type of the context, where O is used for the output value.
  2. The first parameter is the value of the context of type T
  3. The second parameter is the function which performs the action on the object. The functions returns a tuple of an object of type O and an error which can be nil.
  4. Finally the third parameter is the dispose-function itself. In this function you can perform a clean-up
  5. As a whole, this functions return a tuple of an object of type O or an error.

How can we use this?

Let’s look at an example. Imagine we have a very simple object called a MemoryFile which simulates a file in memory. It looks like this:

type MemoryFile struct {
	name string
	open bool
}

func (mf *MemoryFile) Open() {
	mf.open = true
	fmt.Printf("Opening %s\n", mf.name)
}

func (mf *MemoryFile) Close() {
	mf.open = false
	fmt.Printf("Closing %s\n", mf.name)
}

func (mf *MemoryFile) Dispose() {
	fmt.Printf("Disposing %s\n", mf.name)
	mf.Close()
}

func (mf *MemoryFile) Read() (string, error) {
	fmt.Printf("Reading %s\n", mf.name)
	return mf.name, nil
}

As you can see, we can Open() and Close() a MemoryFile, Dispose() of it, and read some of its contents.

We can use it with our very simple ContextManager like this:

func main() {
	mf := &MemoryFile{name: "test.txt"}
	text := "Nothing read"
	text, err := WithContext(mf, func(a *MemoryFile) (string, error) {
		a.Open()
		result, err := a.Read()
		return result, err
	}, func(element *MemoryFile) {
		element.Dispose()
	})
	if err != nil {
		fmt.Printf("Error %s\n", err)
	}
	fmt.Printf("Read %s\n", text)
}

Line by line:

  1. We create a memory file.
  2. We call the WithContext() method with the memory file and two literal closures, one for the actual action, one for the disposal.
  3. We test for errors, and in the case of no errors, we print the results.

The advantages of this method are that the user is forced to think about cleaning up the resources, since all parameters are required.

Conclusion

As I mentioned in the introduction this is just a first attempt. One enhancement would be to make it really threadsafe, which would mean wrapping the Context in some sort of Mutex. I will discuss that in a next blog post. In the meanwhile experiment with this, and let me know what you think, maybe there are easier and/or shorter ways to achieve this goal.