Menu
Coddy logo textTech

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.

challenge icon

Desafio

Fácil

Vamos 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 BankAccount com um sync.Mutex incorporado, um campo balance (int) e uma fatia transactions que 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 vazia
    • Deposit(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 retorna true. Caso contrário, retorna false sem modificar nada
    • Balance() int - Retorna o saldo atual
    • History() []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 defer para 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, withdraw ou balance) e, para depósito/saque, leia o valor.

    Imprima os resultados de cada operação:

    • deposit: Imprima Deposited [amount], Balance: [new balance]
    • withdraw: Imprima Withdrew [amount], Balance: [new balance] se for bem-sucedido ou Withdrawal failed: insufficient funds caso contrário
    • balance: Imprima Current 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, withdraw ou balance) e, para depósito/saque, o valor na linha seguinte

Por exemplo, dadas estas entradas:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Sua saída deverá ser:

Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30

O 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
}
quiz iconTeste seus conhecimentos

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

Pratique por conta própria: Compilador de Go online