Diseño de estructuras Thread-Safe
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy. Lección 65 de 107.
Ahora que entiendes los mutexes y los WaitGroups, combinémoslos para diseñar estructuras que sean seguras de usar simultáneamente desde varias gorutinas. Una estructura segura para la concurrencia encapsula la sincronización dentro de sus métodos, por lo que quienes la llaman no tienen que preocuparse por el bloqueo.
El patrón es sencillo: incorpora un mutex en tu estructura y bloquéalo en cada método que acceda al estado compartido:
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
}Observa que incluso el método de solo lectura Value() bloquea el mutex. Sin esto, una gorutina podría leer mientras otra escribe, lo que provocaría una condición de carrera de datos. Si las lecturas son mucho más frecuentes que las escrituras, usa sync.RWMutex en su lugar y llama a RLock() para las lecturas.
Un principio de diseño clave: mantén el mutex privado. Al usar un nombre de campo en minúsculas (mu), impides que el código externo acceda a él directamente. Toda la sincronización se realiza mediante tus métodos, lo que te proporciona un control total sobre la seguridad de los hilos.
Para las estructuras con varios campos, protege todos los campos relacionados con el mismo mutex para garantizar un estado coherente:
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 como history se actualizan atómicamente: ninguna goroutine puede observar un estado incoherente en el que uno haya cambiado, pero el otro no.
Desafío
FácilConstruyamos un sistema de cuenta bancaria seguro para subprocesos que demuestre la encapsulación adecuada de la sincronización dentro de los métodos de una estructura. Tu cuenta gestionará de forma segura depósitos, retiros y comprobaciones de saldo simultáneos sin exponer ningún detalle de bloqueo a quienes la utilicen.
Organizarás tu código en dos archivos:
account.go: Define tu cuenta bancaria segura para subprocesos.Crea una estructura
BankAccountcon unsync.Mutexincrustado, un campobalance(int) y un slicetransactionsque registre todas las operaciones exitosas como cadenas.Implementa estos métodos:
NewBankAccount(initial int) *BankAccount- Crea una cuenta nueva con el saldo inicial indicado y un slice de transacciones vacíoDeposit(amount int)- Suma el importe al saldo y registra la transacción como+[amount]Withdraw(amount int) bool- Si hay fondos suficientes, resta el importe, registra-[amount]y devuelvetrue. De lo contrario, devuelvefalsesin modificar nadaBalance() int- Devuelve el saldo actualHistory() []string- Devuelve una copia del slice de transacciones
Todo método que acceda a los campos de la estructura debe bloquear el mutex para garantizar la seguridad de los subprocesos. Usa
deferpara desbloquearlo. Mantén el mutex y todos los campos sin exportar (en minúsculas) para que el código externo tenga que usar tus métodos.main.go: Procesa las operaciones bancarias y muestra tu cuenta segura para subprocesos.Lee el saldo inicial y después el número de operaciones. Para cada operación, lee el tipo (
deposit,withdrawobalance) y, para depositar o retirar, lee el importe.Imprime los resultados de cada operación:
deposit: ImprimeDeposited [amount], Balance: [new balance]withdraw: ImprimeWithdrew [amount], Balance: [new balance]si tiene éxito, oWithdrawal failed: insufficient fundssi nobalance: ImprimeCurrent balance: [balance]
Después de todas las operaciones, imprime el historial de transacciones, con cada entrada en una línea nueva y con el prefijo
History:únicamente en la primera entrada.
Se proporcionarán las siguientes entradas:
- Línea 1: Saldo inicial (entero)
- Línea 2: Número de operaciones (entero)
- Líneas siguientes: Para cada operación, el tipo (
deposit,withdrawobalance) y, para depositar o retirar, el importe en la línea siguiente
Por ejemplo, dada la siguiente entrada:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceTu salida debería ser:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30El principio clave aquí es que toda la sincronización está oculta dentro de tus métodos de BankAccount. Quienes utilicen la cuenta simplemente usan Deposit(), Withdraw() y Balance() sin tener que pensar jamás en los bloqueos. Tu estructura gestiona internamente la seguridad de los subprocesos.
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leer el saldo inicial
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Leer el número de operaciones
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Crear la cuenta bancaria
account := NewBankAccount(initial)
// Procesar cada operación
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: Llama a Deposit e imprime el resultado
// Formato: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Llama a Withdraw e imprime el resultado apropiado
// Si es exitoso: "Withdrew [amount], Balance: [new balance]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // Elimina esta línea cuando lo implementes
case "balance":
// TODO: Llama a Balance e imprime el resultado
// Format: "Current balance: [balance]"
}
}
// TODO: Imprime el historial de transacciones
// La primera entrada debe ir prefijada con "History: "
// Las entradas posteriores deben estar en líneas nuevas sin prefijo
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de POO en Go
Archivos externosEspacio de trabajo y módulos de GoPaquetes e importacionesNombres exportados vs. no exportadosIntroducción a la POO en GoStructs como clasesDefinición de métodos en structsReceptores de puntero vs. de valorInicialización de structsFunciones constructorasResumen - Calculadora simple4Interfaces
Introducción a las interfacesImplementación implícitaLa interfaz como contratoInterfaz vacía (any)Type AssertionType SwitchComposición de interfacesInterfaces Stringer y ErrorResumen: Calculadora de figuras7Encapsulamiento
Campos exportados vs. no exportadosEncapsulamiento a nivel de paqueteMétodos Getter y SetterOcultamiento de información en GoResumen - Registros de estudiantes10Genéricos (Go 1.18+)
Introducción a los GenéricosParámetros de tipoRestricciones de tipoStructs genéricosSolución alternativa para métodos genéricosResumen - Colección genérica13Patrones de diseño - Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón Abstract FactoryPatrón ObserverPatrón Strategy2Inmersión profunda en Tipos y Structs
Tipos básicos y compuestosDefiniciones de tipos personalizadosEtiquetas de StructsStructs anónimosStructs anidadosValores cero y por defectoRepaso - Libreta de contactos5Composición sobre herencia
Por qué Go no tiene herenciaConceptos básicos de incrustación de structsPromoción de métodosIncrustación de múltiples structsIncrustación vs AgregaciónShadowing de métodos incrustadosResumen - Jerarquía de empleados8Manejo de Errores y OOP
La interfaz errorTipos de errores personalizadosEnvoltura de errores (fmt.Errorf)Errores centinelaerrors.Is() y errors.As()Panic, Defer y RecoverResumen - Analizador de archivos3Punteros y Memoria
Fundamentos de punteros en GoPunteros a StructsPaso por valor vs referenciaLa función new()Garbage Collection en GoResumen - Constructor de Linked List6Polimorfismo en Go
Polimorfismo mediante interfacesDuck Typing en GoReglas de satisfacción de interfacesColecciones polimórficasInyección de dependenciasResumen - Procesador de pagos9Concurrencia y POO
Fundamentos de GoroutinesChannels y comunicaciónChannels con buffer vs. sin bufferSentencia Selectsync.Mutex y sync.RWMutexsync.WaitGroupDiseño de estructuras Thread-SafeResumen - Worker PoolPractica por tu cuenta: Compilador de Go online