En los posts anteriores exploramos las goroutines, channels, sincronización y varios patrones de concurrencia. Hoy vamos a descubrir una de las herramientas más importantes de Go para escribir código concurrente seguro: el Race Detector. Si vienes de Java, esto es similar a herramientas como ThreadSanitizer, pero está integrado directamente en el runtime de Go.
¿Qué es un Data Race?
Un data race (condición de carrera) ocurre cuando dos o más goroutines acceden a la misma variable de memoria de forma concurrente, y al menos una de las accesos es una escritura, sin usar mecanismos de sincronización apropiados.
Ejemplo de Data Race
var counter int
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Printf("Counter: %d (may not be 10 due to race condition)\n", counter)
¿Por qué es un problema?
counter++ no es una operación atómica
Se compone de: leer → incrementar → escribir
Múltiples goroutines pueden leer el mismo valor y escribir el mismo resultado
El resultado final puede ser incorrecto
Visualización del Problema
Tiempo Goroutine 1 Goroutine 2 Counter
----------------------------------------------------------
T1 Read counter (0)
T2 Read counter (0)
T3 Increment (0+1=1)
T4 Increment (0+1=1)
T5 Write counter (1)
T6 Write counter (1)
Resultado: counter = 1 (debería ser 2)
¿Por Qué Son Peligrosos los Data Races?
Los data races pueden causar:
Valores incorrectos: Los resultados pueden ser incorrectos
Comportamiento impredecible: El programa puede comportarse diferente en cada ejecución
Crashes: Pueden causar panics o crashes en producción
Corrupción de memoria: Pueden corromper estructuras de datos
Bugs difíciles de reproducir: Pueden aparecer solo bajo ciertas condiciones
Ejemplo Real: Corrupción de Datos
type SafeMap struct {
data map[string]int
}
func (sm *SafeMap) Set(key string, value int) {
sm.data[key] = value
}
func (sm *SafeMap) Get(key string) int {
return sm.data[key]
}
El Race Detector de Go
Go incluye un race detector incorporado que detecta data races en tiempo de ejecución. Es parte del toolchain estándar y no requiere instalación adicional.
Cómo Usar el Race Detector
1. Ejecutar Programa con Race Detector
go run -race main.go
2. Ejecutar Tests con Race Detector
go test -race ./...
3. Compilar con Race Detector
go build -race
⚠️ Importante:
El race detector aumenta el uso de memoria (~5-10x)
Reduce la velocidad de ejecución (~2-20x)
Solo debe usarse durante desarrollo y testing
NO debe usarse en producción
Ejemplo: Detectar un Data Race
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Printf("Counter: %d\n", counter)
}
Ejecutar con race detector:
go run -race main.go
Salida del race detector:
==================
WARNING: DATA RACE
Read at 0x00c00001a0a8 by goroutine 8:
main.main.func1()
/path/to/main.go:15: +0x44
Previous write at 0x00c00001a0a8 by goroutine 7:
main.main.func1()
/path/to/main.go:15: +0x60
Goroutine 8 (running) created at:
main.main()
/path/to/main.go:13: +0x8a
Goroutine 7 (finished) created at:
main.main()
/path/to/main.go:13: +0x8a
==================
Counter: 8
Found 1 data race(s)
exit status 66
Información proporcionada:
✅ Ubicación exacta del data race (archivo y línea)
✅ Qué goroutine leyó y qué goroutine escribió
✅ Stack trace de dónde se crearon las goroutines
✅ El programa termina con código de error
Data Races Comunes
1. Contador sin Sincronización
var counter int
func increment() {
counter++
}
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
2. Map sin Sincronización
var data = make(map[string]int)
func set(key string, value int) {
data[key] = value
}
func get(key string) int {
return data[key]
}
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.data[key] = value
}
func (sm *SafeMap) Get(key string) int {
sm.mu.RLock()
defer sm.mu.RUnlock()
return sm.data[key]
}
3. Slice sin Sincronización
var items []int
func add(item int) {
items = append(items, item)
}
var (
items []int
mu sync.Mutex
)
func add(item int) {
mu.Lock()
defer mu.Unlock()
items = append(items, item)
}
4. Variable Compartida sin Protección
var sharedValue int
func updateValue() {
sharedValue = 42
}
func readValue() int {
return sharedValue
}
var (
sharedValue int
mu sync.Mutex
)
func updateValue() {
mu.Lock()
defer mu.Unlock()
sharedValue = 42
}
func readValue() int {
mu.Lock()
defer mu.Unlock()
return sharedValue
}
5. Inicialización sin sync.Once
var instance *Database
var initialized bool
func getDatabase() *Database {
if !initialized {
instance = &Database{}
initialized = true
}
return instance
}
var (
instance *Database
once sync.Once
)
func getDatabase() *Database {
once.Do(func() {
instance = &Database{}
})
return instance
}
Cómo Prevenir Data Races
1. Usar Channels (Recomendado)
La forma más idiomática en Go es usar channels en lugar de compartir memoria:
type Counter struct {
increment chan int
value chan int
}
func NewCounter() *Counter {
c := &Counter{
increment: make(chan int),
value: make(chan int),
}
go func() {
var count int
for {
select {
case <-c.increment:
count++
case c.value <- count:
}
}
}()
return c
}
func (c *Counter) Increment() {
c.increment <- 1
}
func (c *Counter) Value() int {
return <-c.value
}
Ventajas:
2. Usar Mutex para Protección
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Lock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
3. Usar Operaciones Atómicas
Para tipos primitivos simples:
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func value() int64 {
return atomic.LoadInt64(&counter)
}
4. Usar sync.Once para Inicialización
var (
instance *Database
once sync.Once
)
func getDatabase() *Database {
once.Do(func() {
instance = &Database{}
})
return instance
}
Mejores Prácticas
1. Siempre Ejecutar Tests con Race Detector
go test -race ./...
- name: Run tests with race detector
run: go test -race ./...
2. Ejecutar Race Detector en Desarrollo
go run -race main.go
go build -race
./program
3. No Compartir Memoria sin Sincronización
var shared int
go func() {
shared = 1
}()
var (
shared int
mu sync.Mutex
)
go func() {
mu.Lock()
shared = 1
mu.Unlock()
}()
4. Documentar Requisitos de Concurrencia
type Counter struct {
mu sync.Mutex
value int
}
func unsafeFunction() {
}
5. Usar go vet para Detectar Problemas Comunes
go vet ./...
6. Revisar Código para Patrones Peligrosos
Buscar en el código:
Variables globales modificadas por goroutines
Maps compartidos sin protección
Slices compartidos sin protección
Campos de struct modificados concurrentemente
Errores Comunes
❌ Error 1: Olvidar Ejecutar Race Detector
go run main.go
go test ./...
go run -race main.go
go test -race ./...
❌ Error 2: Asumir que el Código es Thread-Safe
var counter int
func increment() {
counter++
}
go run -race main.go
❌ Error 3: No Proteger Lecturas
var (
value int
mu sync.Mutex
)
func set(v int) {
mu.Lock()
defer mu.Unlock()
value = v
}
func get() int {
return value
}
func get() int {
mu.Lock()
defer mu.Unlock()
return value
}
❌ Error 4: Usar Race Detector en Producción
go build -race
go build -race
go build
❌ Error 5: Ignorar Warnings del Race Detector
Ejemplo Completo: Detectar y Arreglar Data Race
Código con Data Race
package main
import (
"fmt"
"sync"
)
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Value() int {
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Counter: %d (should be 1000)\n", counter.Value())
}
Ejecutar con race detector:
go run -race main.go
Salida:
WARNING: DATA RACE
Read at 0x00c00001a0a8 by goroutine 8:
main.(*Counter).Value()
/path/to/main.go:18: +0x44
Previous write at 0x00c00001a0a8 by goroutine 7:
main.(*Counter).Increment()
/path/to/main.go:14: +0x60
Código Corregido
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Counter: %d (should be 1000)\n", counter.Value())
}
Ejecutar con race detector:
go run -race main.go
Salida:
Counter: 1000 (should be 1000)
# No hay warnings - código es thread-safe
Comparación: Go vs Otras Herramientas
Java: ThreadSanitizer y FindBugs
Problemas:
Go: Race Detector Incorporado
go run -race main.go
go test -race ./...
go build -race
Ventajas:
✅ Incorporado en el toolchain
✅ No requiere instalación adicional
✅ Fácil de usar
✅ Detecta data races en tiempo de ejecución
✅ Información detallada sobre el race
Integración en CI/CD
GitHub Actions
name: Test with Race Detector
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.21'
- name: Run tests with race detector
run: go test -race ./...
GitLab CI
test:
script:
- go test -race ./...
Makefile
.PHONY: test
test:
go test -race ./...
.PHONY: test-race
test-race:
go test -race -v ./...
Limitaciones del Race Detector
El race detector tiene algunas limitaciones:
Solo detecta data races que ocurren durante la ejecución
Si un data race no ocurre en una ejecución específica, no se detecta
Por eso es importante ejecutar tests múltiples veces
No detecta todos los problemas de concurrencia
Overhead significativo
Puede tener falsos positivos
Consejos para Usar el Race Detector Efectivamente
1. Ejecutar Tests Múltiples Veces
for i in {1..10}; do
go test -race ./...
done
2. Ejecutar con Diferentes Cargas
GOMAXPROCS=1 go test -race ./...
GOMAXPROCS=4 go test -race ./...
GOMAXPROCS=8 go test -race ./...
3. Usar en Desarrollo Activo
go run -race main.go
while true; do
go run -race main.go
sleep 1
done
4. Integrar en Pre-commit Hooks
#!/bin/sh
echo "Running race detector..."
go test -race ./...
if [ $? -ne 0 ]; then
echo "Data race detected! Commit aborted."
exit 1
fi
Conclusiones
El race detector de Go es una herramienta poderosa:
✅ Incorporado: No requiere instalación adicional
✅ Fácil de usar: Solo agregar -race flag
✅ Detecta data races: En tiempo de ejecución con información detallada
✅ Prevención: Ayuda a escribir código thread-safe
✅ Integración: Fácil integrar en CI/CD
✅ Educativo: Ayuda a entender problemas de concurrencia
Si vienes de Java u otros lenguajes, el race detector incorporado de Go es una ventaja significativa. No necesitas herramientas externas complejas - solo ejecuta con -race y obtienes información detallada sobre data races.
Regla de oro: Si escribes código concurrente en Go, siempre ejecuta tus tests con -race. Es la mejor forma de asegurar que tu código es thread-safe.
Próximos Pasos
Con esto completamos nuestra serie sobre concurrencia en Go. Hemos cubierto:
Goroutines
Channels (buffered y unbuffered)
Select statement
Context con cancelación
Sincronización (Mutex, RWMutex, WaitGroup)
Worker Pools
Pipelines
Fan-Out / Fan-In
Detección de Data Races
Ahora tienes todas las herramientas necesarias para escribir código concurrente seguro y eficiente en Go.
¿Has usado el race detector en tus proyectos? ¿Qué data races has encontrado? Comparte tus experiencias y casos de uso en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.