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:
- The method is generic,
T
is used for the type of the context, whereO
is used for the output value. - The first parameter is the value of the context of type
T
- 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. - Finally the third parameter is the dispose-function itself. In this function you can perform a clean-up
- 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:
- We create a memory file.
- We call the
WithContext()
method with the memory file and two literal closures, one for the actual action, one for the disposal. - 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.