Идемпотентный 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()
корректно работают и в многозадачной среде.
Вот и все!
Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀