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 possam ser usados com segurança por várias goroutines simultaneamente. Uma struct segura para uso concorrente encapsula a sincronização em seus métodos, portanto, quem os chama não precisa se preocupar com bloqueios.
O padrão é simples: incorpore um mutex na sua struct e bloqueie-o em todos os métodos que acessam 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 somente leitura Value() bloqueia o mutex. Sem isso, uma goroutine poderia ler enquanto outra escreve, causando uma condição de corrida de dados. Se as leituras forem muito mais comuns do que as escritas, use sync.RWMutex em vez disso e chame RLock() para as leituras.
Um princípio fundamental de design: mantenha o mutex privado. Ao usar um nome de campo em minúsculas (mu), você impede que códigos externos o acessem diretamente. Toda a sincronização acontece por meio dos seus métodos, dando a você controle total sobre a segurança de threads.
Para estruturas com vários 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 em que um mudou, mas o outro não.
Desafio
FácilVamos criar um sistema de conta bancária seguro para concorrência que demonstre o encapsulamento adequado da sincronização dentro dos métodos de uma struct. Sua conta lidará com depósitos, saques e consultas de saldo concorrentes com segurança, sem expor detalhes de bloqueio aos chamadores.
Você organizará seu código em dois arquivos:
account.go: Defina sua conta bancária segura para concorrência.Crie uma struct
BankAccountcom umsync.Mutexincorporado, um campobalance(int) e uma fatiatransactionsque registre 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 uma fatia de transações vaziaDeposit(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 da fatia de transações
Todo método que acessar os campos da struct deve bloquear o mutex para garantir a segurança em relação à concorrência. Use
deferpara desbloqueá-lo. Mantenha o mutex e todos os campos não exportados (em letras minúsculas), para que o código externo precise usar seus métodos.main.go: Processe as operações bancárias e demonstre sua conta segura para concorrência.Leia o saldo inicial e, em seguida, o número de operações. Para cada operação, leia o tipo (
deposit,withdrawoubalance) e, para depósito/saque, leia o valor.Imprima os resultados de 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, precedida por
History:apenas na 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 depósito/saque, o valor na linha seguinte
Por exemplo, dadas estas entradas:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceSua saída deverá 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 fica oculta dentro dos métodos de BankAccount. Os chamadores simplesmente usam Deposit(), Withdraw() e Balance() sem nunca precisar pensar em bloqueios. Sua struct lida internamente com a segurança em relação à concorrência.
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: Chame Deposit e imprima o resultado
// Formato: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Chame Withdraw e imprima o resultado apropriado
// Se bem-sucedido: "Withdrew [amount], Balance: [new balance]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // Remova esta linha quando implementar
case "balance":
// TODO: Chame Balance e imprima o resultado
// Format: "Current balance: [balance]"
}
}
// TODO: Imprima o histórico de transações
// A primeira entrada deve ser prefixada com "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 ExternosWorkspace e Módulos do GoPacotes e ImportsNomes Exportados vs. Não ExportadosIntrodução à OOP em GoStructs como ClassesDefinindo Métodos em StructsReceptores de Ponteiro vs. de ValorInicialização de StructsFunções ConstrutorasRecapitulação — Calculadora Simples4Interfaces
Introdução às InterfacesImplementação ImplícitaInterface como ContratoInterface Vazia (any)Asserção de TipoSwitch de TipoComposição de InterfacesInterfaces Stringer e ErrorRecapitulação - Calculadora de Formas7Encapsulamento
Campos Exportados vs. Não ExportadosEncapsulamento no Nível de PacoteMétodos Getters e SettersOcultação de Informações em GoRecapitulação - Registros de Alunos10Genéricos (Go 1.18+)
Introdução aos genéricosParâmetros de tipoRestrições de tipoStructs genéricasAlternativa para métodos genéricosRecapitulação - Coleção genérica13Padrões de Projeto — Parte 1
Introdução aos Padrões de ProjetoPadrão SingletonPadrão FactoryPadrão Abstract FactoryPadrão ObserverPadrão Strategy2Tipos e Structs em Profundidade
Tipos Básicos e CompostosDefinições de Tipos PersonalizadosTags de StructStructs AnônimasStructs AninhadasValores Zero e PadrõesRecapitulação - Livro de Contatos5Composição em vez de herança
Por que Go não tem herançaNoções básicas de incorporação de structsPromoção de métodosIncorporação de várias structsIncorporação vs agregaçãoOcultação de métodos incorporadosRecapitulação - Hierarquia de funcionários8Tratamento de Erros e OOP
A interface errorTipos de Erro PersonalizadosEncapsulamento de Erros (fmt.Errorf)Erros Sentinelaerrors.Is() e errors.As()Panic, Defer e RecoverRecapitulação - Analisador de Arquivos3Ponteiros e Memória
Fundamentos de Ponteiros em GoPonteiros para StructsPassagem por Valor vs. ReferênciaA Função new()Coleta de Lixo em GoRecapitulação - Construtor de Lista Ligada6Polimorfismo em Go
Polimorfismo via InterfacesDuck Typing em GoRegras de Satisfação de InterfacesColeções PolimórficasInjeção de DependênciasRecapitulação - Processador de Pagamentos9Concorrência e OOP
Fundamentos de GoroutinesCanais e comunicaçãoCanais com e sem bufferInstrução selectsync.Mutex e sync.RWMutexsync.WaitGroupDesign de structs thread-safeRevisão - Worker PoolPratique por conta própria: Compilador de Go online