Idempotent Close in Go

nalgeon

Anton Zhiyanov

Posted on January 11, 2023

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
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The NewGate() constructor opens the gate, acquires some system resources, and returns an instance of the Gate.

g := NewGate()
Enter fullscreen mode Exit fullscreen mode

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
// ...
Enter fullscreen mode Exit fullscreen mode

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
// ...
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

playground

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

playground

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!

💖 💪 🙅 🚩
nalgeon
Anton Zhiyanov

Posted on January 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related