Hoje eu aprendi sobre o padrão de confinamento em Go: em vez de proteger o estado compartilhado com um sync.Mutex, você atribui a posse do estado a uma única goroutine e se comunica por meio de canais.
O problema de usar mutexes em todo lugar
Usar mutexes para estado compartilhado funciona, mas traz desvantagens:
- A contenção de locks reduz o paralelismo.
- Esquecer o
Unlock()leva a deadlocks. - Bugs sutis de condição de corrida aparecem sob concorrência.
- Raciocinar sobre a posse do estado fica mais difícil conforme os locks se espalham.
O que é confinamento?
Confinamento significa que um pedaço de estado só é acessível a partir de uma única goroutine. Nenhuma outra goroutine pode tocá-lo diretamente, então não é preciso sincronização. Isso segue a filosofia do Go: “Não se comunique compartilhando memória; compartilhe memória se comunicando.”
Exemplo: Mutex vs Confinamento
Antes - estado compartilhado protegido por um mutex:
counter := 0
var mu sync.Mutex
for i := 0; i < 3; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
Depois - estado confinado a uma goroutine dona:
increments := make(chan int)
read := make(chan chan int)
go func() {
counter := 0 // confined state
for {
select {
case v := <-increments:
counter += v
case response := <-read:
response <- counter
}
}
}()
increments <- 1
increments <- 1
increments <- 1
response := make(chan int)
read <- response
fmt.Println("Final counter:", <-response)
Por que funciona
countervive dentro da goroutine — nenhum outro código consegue acessá-lo.- Toda a interação acontece por meio de canais.
- Sem mutex, sem condições de corrida, sem estado mutável compartilhado.
Resumo
- A maioria dos bugs de concorrência vem do estado compartilhado, não do Go em si.
- O padrão de confinamento elimina os locks ao dar a cada pedaço de estado um único dono.
- O estado compartilhado mais seguro é aquele que não é compartilhado de jeito nenhum.
