Progettazione di struct thread-safe
Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 65 di 107.
Ora che hai capito i mutex e i WaitGroup, combiniamoli per progettare struct sicure da usare contemporaneamente da più goroutine. Una struct thread-safe racchiude la sincronizzazione nei suoi metodi, così chi la usa non deve preoccuparsi dei lock.
Lo schema è semplice: inserisci un mutex nella tua struct e acquisisci il lock in ogni metodo che accede allo stato condiviso:
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
}Nota che anche il metodo di sola lettura Value() blocca il mutex. Senza questo, una goroutine potrebbe leggere mentre un’altra scrive, causando una race condition. Se le letture sono molto più frequenti delle scritture, usa invece sync.RWMutex e chiama RLock() per le letture.
Un principio fondamentale di progettazione: mantieni privato il mutex. Usando un nome di campo in minuscolo (mu), impedisci al codice esterno di accedervi direttamente. Tutta la sincronizzazione avviene tramite i tuoi metodi, dandoti il pieno controllo sulla sicurezza dei thread.
Per le struct con più campi, proteggi tutti i campi correlati con lo stesso mutex per garantire uno stato coerente:
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))
}Sia balance che history vengono aggiornati atomicamente: nessuna goroutine può osservare uno stato incoerente in cui uno è cambiato ma l’altro no.
Sfida
FacileCostruiamo un sistema di conto bancario sicuro per l’uso concorrente, che dimostri il corretto incapsulamento della sincronizzazione nei metodi della struct. Il conto gestirà in sicurezza depositi, prelievi e controlli del saldo concorrenti, senza esporre ai chiamanti i dettagli dei lock.
Organizzerai il codice in due file:
account.go: definisci il tuo conto bancario sicuro per l’uso concorrente.Crea una struct
BankAccountcon unsync.Mutexincorporato, un campobalance(int) e una slicetransactionsche registra come stringhe tutte le operazioni riuscite.Implementa questi metodi:
NewBankAccount(initial int) *BankAccount- Crea un nuovo conto con il saldo iniziale specificato e una slice delle transazioni vuotaDeposit(amount int)- Aggiunge l’importo al saldo e registra la transazione come+[amount]Withdraw(amount int) bool- Se i fondi sono sufficienti, sottrae l’importo, registra-[amount]e restituiscetrue. Altrimenti restituiscefalsesenza modificare nullaBalance() int- Restituisce il saldo correnteHistory() []string- Restituisce una copia della slice delle transazioni
Ogni metodo che accede ai campi della struct deve bloccare il mutex per garantire la sicurezza per l’uso concorrente. Usa
deferper sbloccarlo. Mantieni il mutex e tutti i campi non esportati (in minuscolo), in modo che il codice esterno debba usare i tuoi metodi.main.go: elabora le operazioni bancarie e mostra come usare il tuo conto sicuro per l’uso concorrente.Leggi il saldo iniziale e poi il numero di operazioni. Per ogni operazione, leggi il tipo (
deposit,withdrawobalance) e, per depositi/prelievi, leggi l’importo.Stampa il risultato di ogni operazione:
deposit: stampaDeposited [amount], Balance: [new balance]withdraw: se l’operazione riesce, stampaWithdrew [amount], Balance: [new balance]; altrimenti stampaWithdrawal failed: insufficient fundsbalance: stampaCurrent balance: [balance]
Dopo tutte le operazioni, stampa la cronologia delle transazioni, con ogni voce su una nuova riga e il prefisso
History:solo per la prima voce.
Verranno forniti i seguenti input:
- Riga 1: saldo iniziale (intero)
- Riga 2: numero di operazioni (intero)
- Righe successive: per ogni operazione, il tipo (
deposit,withdrawobalance) e, per depositi/prelievi, l’importo sulla riga successiva
Per esempio, dato:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceIl tuo output dovrebbe essere:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Il principio fondamentale è che tutta la sincronizzazione è nascosta all’interno dei metodi di BankAccount. I chiamanti usano semplicemente Deposit(), Withdraw() e Balance(), senza mai doversi preoccupare dei lock. La tua struct gestisce internamente la sicurezza per l’uso concorrente.
Provalo tu
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leggi il saldo iniziale
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Leggi il numero di operazioni
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Crea il conto bancario
account := NewBankAccount(initial)
// Elabora ogni operazione
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: Chiama Deposit e stampa il risultato
// Formato: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Chiama Withdraw e stampa il risultato appropriato
// Se riesce: "Withdrew [amount], Balance: [new balance]"
// Se fallisce: "Withdrawal failed: insufficient funds"
_ = amount // Rimuovi questa riga quando implementi la soluzione
case "balance":
// TODO: Chiama Balance e stampa il risultato
// Formato: "Current balance: [balance]"
}
}
// TODO: Stampa la cronologia delle transazioni
// La prima voce deve essere preceduta da "History: "
// Le voci successive devono essere su nuove righe senza prefisso
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti di OOP in Go
File esterniWorkspace e moduli GoPacchetti e importazioniNomi esportati e non esportatiIntroduzione all’OOP in GoGli struct come classiDefinire metodi sugli structReceiver con puntatore vs receiver con valoreInizializzazione degli structFunzioni costruttriciRiepilogo - Calcolatrice semplice4Interfacce
Introduzione alle interfacceImplementazione implicitaL’interfaccia come contrattoInterfaccia vuota (any)Asserzione di tipoSwitch sul tipoComposizione di interfacceInterfacce Stringer ed ErrorRipasso - Calcolatore di forme7Incapsulamento
Campi esportati e non esportatiIncapsulamento a livello di packageMetodi Getter e SetterNascondimento delle informazioni in GoRiepilogo - Registri degli studenti10Tipi generici (Go 1.18+)
Introduzione ai tipi genericiParametri di tipoVincoli di tipoStruct genericheSoluzione alternativa per i metodi genericiRiepilogo - Collezione generica13Modelli di progettazione Parte 1
Introduzione ai modelli di progettazioneModello SingletonModello FactoryModello Abstract FactoryModello ObserverModello Strategy2Approfondimento su tipi e struct
Tipi di base e compostiDefinizioni di tipi personalizzatiTag delle structStruct anonimeStruct annidateValori zero e predefinitiRipasso - Rubrica5Composizione invece dell'ereditarietà
Perché Go non ha l'ereditarietàFondamenti dell'embedding delle structPromozione dei metodiEmbedding di più structEmbedding e aggregazione a confrontoMascheramento dei metodi embeddedRiepilogo: gerarchia dei dipendenti8Gestione degli errori e OOP
L'interfaccia errorTipi di errore personalizzatiWrapping degli errori (fmt.Errorf)Errori sentinellaerrors.Is() ed errors.As()Panic, Defer e RecoverRiepilogo - Parser di file11Libreria standard e OOP
io.Reader e io.Writersort.InterfaceInterfaccia fmt.Stringerencoding/json con le structInterfaccia http.HandlerRipasso - modelli REST API14Modelli di progettazione – Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeMiddleware come Decorator3Puntatori e memoria
Concetti base dei puntatori in GoPuntatori a structPassaggio per valore vs per riferimentoLa funzione new()Garbage collection in GoRiepilogo - Costruttore di liste concatenate6Polimorfismo in Go
Polimorfismo tramite interfacceDuck typing in GoRegole di soddisfacimento delle interfacceCollezioni polimorficheIniezione delle dipendenzeRiepilogo - Elaboratore di pagamenti9Concorrenza e OOP
Basi delle goroutineCanali e comunicazioneCanali bufferizzati vs non bufferizzatiIstruzione selectsync.Mutex e sync.RWMutexsync.WaitGroupProgettazione di struct thread-safeRiepilogo: worker poolEsercitati da solo: Compilatore Go online