Идемпотентный Close в Go

Идемпотентность — это когда повторный вызов операции над объектом не приводит к изменениям или ошибкам. Очень полезная в разработке штука.

Давайте посмотрим, как применить идемпотентность, чтобы безопасно освободить занятые ресурсы.

Идемпотентный Close

Например, есть у нас ворота:

type Gate struct{
    // internal state
    // ...
}

Конструктор NewGate() открывает ворота, занимая какие-то системные ресурсы, и возвращает экземпляр Gate.

g := NewGate()

Понятно, что в итоге занятые ресурсы надо освободить:

func (g *Gate) Close() error {
    // free acquired resources
    // ...
    return nil
}

g := NewGate()
defer g.Close()
// do stuff
// ...

Проблемы начнутся, если в какой-то ветке кода мы захотим явно закрыть ворота:

g := NewGate()
defer g.Close()

err := checkSomething()
if err != nil {
    g.Close()
    // do something else
    // ...
    return
}

// do more stuff
// ...

Первый Close() сработает, а вот второй (через defer) сломается, потому что ресурсы уже освобождены.

Решение — сделать Close() идемпотентным, чтобы повторные вызовы ничего не делали, если ворота уже закрыты:

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
}

песочница

Теперь Close() можно вызывать сколько угодно без каких-либо проблем. До тех пор, пока мы не попытаемся закрыть ворота из разных горутин — тут-то все и развалится.

Идемпотентность в многозадачной среде

Мы сделали закрытие ворот идемпотентным — безопасным для повторных вызовов:

func (g *Gate) Close() error {
    if g.closed {
        return nil
    }
    // free acquired resources
    // ...
    g.closed = true
    return nil
}

Но если несколько горутин используют общий экземпляр Gate, одновременный вызов Close() приведет к гонкам — конкурентной модификации поля closed. Такого счастья нам не надо.

Придется защитить изменение closed мьютексом:

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
}

песочница

Мьютекс гарантирует, что код между Lock() и Unlock() выполняется только одной горутиной в любой момент времени.

Теперь многократные вызовы Close() корректно работают и в многозадачной среде.

Вот и все!

Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀