Idempotent close in Go
Idempotence is when a repeated call to an operation on an object does not result in changes or errors. Idempotence is a handy development tool.
Let's see how idempotence helps to free the occupied resources safely.
Idempotent Close
Suppose we have a gate:
type Gate struct{
// internal state
// ...
}
The NewGate()
constructor opens the gate, acquires some system resources, and returns an instance of the Gate
.
g := NewGate()
In the end, we must release the occupied resources:
func (g *Gate) Close() error {
// free acquired resources
// ...
return nil
}
g := NewGate()
defer g.Close()
// do stuff
// ...
Problems arise when in some branch of the code, we want to close the gate explicitly:
g := NewGate()
defer g.Close()
err := checkSomething()
if err != nil {
g.Close()
// do something else
// ...
return
}
// do more stuff
// ...
The first Close()
will work, but the second (via defer
) will break because the resources have already been released.
The solution is to make Close()
idempotent so that repeated calls do nothing if the gate is already closed:
type Gate struct{
closed bool
// internal state
// ...
}
func (g *Gate) Close() error {
if g.closed {
return nil
}
// free acquired resources
// ...
g.closed = true
return nil
}
Now we can call Close()
repeatedly without any problems. Until we try to close the gate from different goroutines — then everything will fall apart.
Idempotency in a concurrent environment
We have made the gate closing idempotent — safe for repeated calls:
func (g *Gate) Close() error {
if g.closed {
return nil
}
// free acquired resources
// ...
g.closed = true
return nil
}
But if several goroutines use the same Gate
instance, a simultaneous call to Close()
will lead to races — a concurrent modification of the closed
field. We don't want this.
We have to protect the closed
field access with a mutex:
type Gate struct {
mu sync.Mutex
closed bool
// internal state
// ...
}
func (g *Gate) Close() error {
g.mu.Lock()
if g.closed {
g.mu.Unlock()
return nil
}
// free acquired resources
// ...
g.closed = true
g.mu.Unlock()
return nil
}
The mutex ensures that only a single goroutine executes the code between Lock()
and Unlock()
at any given time.
Now multiple calls to Close()
work correctly in a concurrent environment.
That's it!
──
P.S. Playgrounds in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.
★ Subscribe to keep up with new posts.