Design de Structs Thread-Safe
Parte da seção Programação Orientada a Objetos do Journey de GO da Coddy — lição 65 de 107.
Agora que você entende mutexes e WaitGroups, vamos combiná-los para projetar structs que sejam seguras para uso em múltiplas goroutines simultaneamente. Uma struct thread-safe encapsula a sincronização dentro de seus métodos, para que os chamadores não precisem se preocupar com o bloqueio.
O padrão é simples: incorpore um mutex em sua struct e bloqueie-o em cada método que acessa o estado compartilhado:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Observe que até mesmo o método Value() de apenas leitura bloqueia o mutex. Sem isso, uma goroutine pode ler enquanto outra escreve, causando uma condição de corrida (data race). Se as leituras forem muito mais comuns que as escritas, use sync.RWMutex em vez disso e chame RLock() para leituras.
Um princípio de design fundamental: mantenha o mutex privado. Ao usar um nome de campo em letras minúsculas (mu), você impede que o código externo o acesse diretamente. Toda a sincronização acontece por meio de seus métodos, oferecendo a você controle total sobre a segurança de threads (thread safety).
Para structs com múltiplos campos, proteja todos os campos relacionados com o mesmo mutex para garantir um estado consistente:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Tanto balance quanto history são atualizados atomicamente—nenhuma goroutine pode observar um estado inconsistente onde um mudou, mas o outro não.
Desafio
FácilVamos construir um sistema de conta bancária thread-safe que demonstra o encapsulamento adequado da sincronização dentro dos métodos da struct. Sua conta lidará com depósitos, saques e verificações de saldo concorrentes de forma segura, sem expor nenhum detalhe de travamento (locking) aos chamadores.
Você organizará seu código em dois arquivos:
account.go: Defina sua conta bancária thread-safe.Crie uma struct
BankAccountcom umsync.Mutexembutido, um campobalance(int) e um slicetransactionsque registra todas as operações bem-sucedidas como strings.Implemente estes métodos:
NewBankAccount(initial int) *BankAccount- Cria uma nova conta com o saldo inicial fornecido e um slice de transações vazioDeposit(amount int)- Adiciona o valor ao saldo e registra a transação como+[amount]Withdraw(amount int) bool- Se houver fundos suficientes, subtrai o valor, registra-[amount]e retornatrue. Caso contrário, retornafalsesem modificar nadaBalance() int- Retorna o saldo atualHistory() []string- Retorna uma cópia do slice de transações
Todo método que acessa os campos da struct deve travar o mutex para garantir a segurança de thread. Use
deferpara destravar. Mantenha o mutex e todos os campos não exportados (letra minúscula) para que o código externo precise usar seus métodos.main.go: Processe as operações bancárias e demonstre sua conta thread-safe.Leia o saldo inicial e, em seguida, o número de operações. Para cada operação, leia o tipo (
deposit,withdrawoubalance) e, para deposit/withdraw, leia o valor.Imprima os resultados para cada operação:
deposit: ImprimaDeposited [amount], Balance: [new balance]withdraw: ImprimaWithdrew [amount], Balance: [new balance]se for bem-sucedido, ouWithdrawal failed: insufficient fundscaso contráriobalance: ImprimaCurrent balance: [balance]
Após todas as operações, imprima o histórico de transações com cada entrada em uma nova linha, prefixada por
History:apenas para a primeira entrada.
As seguintes entradas serão fornecidas:
- Linha 1: Saldo inicial (inteiro)
- Linha 2: Número de operações (inteiro)
- Linhas seguintes: Para cada operação, o tipo (
deposit,withdrawoubalance) e, para deposit/withdraw, o valor na linha seguinte
Por exemplo, dado:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceSua saída deve ser:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30O princípio fundamental aqui é que toda a sincronização está oculta dentro dos métodos da sua BankAccount. Os chamadores simplesmente usam Deposit(), Withdraw() e Balance() sem nunca pensar em travas — sua struct lida com a segurança de thread internamente.
Folha de consulta
Uma struct thread-safe encapsula a sincronização dentro de seus métodos ao incorporar um mutex e bloqueá-lo em cada método que acessa o estado compartilhado:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Princípios fundamentais:
- Bloqueie o mutex em todos os métodos que acessam o estado compartilhado, incluindo métodos de apenas leitura, para evitar condições de corrida (data races)
- Use
deferpara garantir que o mutex seja desbloqueado mesmo se a função retornar antecipadamente - Mantenha o mutex privado (nome de campo em letra minúscula) para que o código externo não possa acessá-lo diretamente
- Para cargas de trabalho com muitas leituras, use
sync.RWMutexe chameRLock()para leituras
Para structs com múltiplos campos, proteja todos os campos relacionados com o mesmo mutex para garantir um estado consistente:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Isso garante que tanto o balance quanto o history sejam atualizados atomicamente — nenhuma goroutine poderá observar um estado inconsistente.
Experimente você mesmo
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Ler o saldo inicial
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Ler o número de operações
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Criar a conta bancária
account := NewBankAccount(initial)
// Processar cada operação
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Chamar Deposit e imprimir o resultado
// Formato: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Chamar Withdraw e imprimir o resultado apropriado
// Se bem-sucedido: "Withdrew [amount], Balance: [new balance]"
// Se falhar: "Withdrawal failed: insufficient funds"
_ = amount // Remova esta linha quando implementar
case "balance":
// TODO: Chamar Balance e imprimir o resultado
// Formato: "Current balance: [balance]"
}
}
// TODO: Imprimir o histórico de transações
// A primeira entrada deve ter o prefixo "History: "
// As entradas subsequentes devem estar em novas linhas sem prefixo
}
Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.
Todas as lições de Programação Orientada a Objetos
1Fundamentos de OOP em Go
Arquivos ExternosGo Workspace e MódulosPacotes e ImportaçõesNomes Exportados vs Não ExportadosIntrodução a OOP em GoStructs como ClassesDefinindo Métodos em StructsRecebedores de Ponteiro vs ValorInicialização de StructsFunções ConstrutorasRecapitulação - Calculadora Simples4Interfaces
Introdução a InterfacesImplementação ImplícitaInterface como ContratoInterface Vazia (any)Type AssertionType SwitchComposição de InterfacesInterfaces Stringer & ErrorRecapitulação - Calculadora de Formas7Encapsulamento
Campos Exportados vs Não ExportadosEncapsulamento em Nível de PacoteMétodos Getter e SetterOcultação de Informação em GoRecapitulação - Registros de Estudantes10Generics (Go 1.18+)
Introdução a GenericsParâmetros de TipoRestrições de TipoStructs GenéricasSolução Alternativa para Métodos GenéricosRecapitulação - Coleção Genérica2Mergulho Profundo em Tipos e Structs
Tipos Básicos e CompostosDefinições de Tipos PersonalizadosStruct TagsStructs AnônimasStructs AninhadasZero Values e PadrõesRecapitulação - Agenda de Contatos5Composição em vez de Herança
Por que Go não possui HerançaFundamentos de Incorporação de StructsPromoção de MétodosIncorporando Múltiplas StructsIncorporação vs AgregaçãoShadowing de Métodos IncorporadosRecapitulação - Hierarquia de Funcionários8Tratamento de Erros e OOP
A Interface errorTipos de Erro PersonalizadosError Wrapping (fmt.Errorf)Erros Sentinelaerrors.Is() e errors.As()Panic, Defer e RecoverRecapitulação - File Parser3Ponteiros e Memória
Fundamentos de Ponteiros em GoPonteiros para StructsPassagem por Valor vs ReferênciaA Função new()Garbage Collection em GoRecapitulação - Construtor de Lista Encadeada6Polimorfismo em Go
Polimorfismo via InterfacesDuck Typing em GoRegras de Satisfação de InterfaceColeções PolimórficasInjeção de DependênciaRecapitulação - Processador de Pagamentos9Concorrência e OOP
Fundamentos de GoroutinesCanais e ComunicaçãoCanais com Buffer vs. Sem BufferInstrução Selectsync.Mutex e sync.RWMutexsync.WaitGroupDesign de Structs Thread-SafeRecapitulação - Worker Pool