Go: Referencia de Eficiencia Idiomática
Tabla de Contenidos
- Manejo de Errores
- Slices y Mapas
- Gorutinas y Canales
- Structs e Interfaces
- Funciones y Closures
- Anti-patrones específicos de Go
1. Manejo de Errores {#errores}
// ❌ Ignorar errores
result, _ := os.Open(path)
// ✅ — siempre manejar; usar _ solo cuando el error es irrelevante de manera comprobable
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ Variable de error redundante
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — cada :=/: está bien; esto es Go idiomático. No intentes "arreglarlo".
// Lo que SÍ puedes simplificar: colapsar en una línea cuando el cuerpo del if es un solo return
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ Tipo de error personalizado sin valor añadido
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — usar errors.New o fmt.Errorf a menos que los llamadores necesiten inspeccionar el tipo
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
Envuelve los errores con %w (no %v) para que los llamadores puedan usar errors.Is / errors.As.
2. Slices y Mapas {#slices}
// ❌ Crecer un slice sin preasignación cuando se conoce el tamaño
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ Verificación manual de existencia antes de escribir en el mapa
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — agregar a un slice nil es válido en Go
m[key] = append(m[key], value)
// ❌ Copiar un mapa por asignación (copia la referencia)
copia := original
// ✅
copia := make(map[K]V, len(original))
for k, v := range original { copia[k] = v }
3. Gorutinas y Canales {#concurrencia}
// ❌ Gorutina de lanzar y olvidar sin ciclo de vida
go doWork()
// ✅ — usar errgroup o WaitGroup para rastrear la finalización
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ Canal sin buffer que causa bloqueo innecesario de gorutina
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — para un solo resultado, un canal con buffer evita fugas de gorutina si el receptor sale temprano
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ select con default de espera activa
for {
select {
case v := <-ch:
process(v)
default:
// girar
}
}
// ✅ — select bloqueante a menos que realmente necesites no bloqueo
for v := range ch {
process(v)
}
Usa golang.org/x/sync/errgroup para fan-out con recolección de errores.
4. Structs e Interfaces {#structs}
// ❌ Interfaz grande
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 10 métodos más
}
// ✅ — interfaces pequeñas y componibles
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ Devolver struct concreto desde el constructor (vincula a los llamadores a la implementación)
func NewStore() *RedisStore { ... }
// ✅ — devolver interfaz cuando tienes o anticipas múltiples implementaciones
func NewStore() Storage { return &RedisStore{...} }
// ❌ Receptor de puntero para tipos de valor pequeños
func (p *Point) X() float64 { return p.x }
// ✅ — receptor de valor para tipos inmutables pequeños
func (p Point) X() float64 { return p.x }
Regla: receptor de puntero cuando el método muta el estado O el struct es grande (>3 campos de tamaño no trivial). Receptor de valor en caso contrario.
5. Funciones y Closures {#funciones}
// ❌ Valores de retorno nombrados usados solo para evitar una declaración de variable
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — los retornos nombrados valen la pena solo para mutación diferida o documentación
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ Closure capturando variable de bucle (error clásico de Go, corregido en Go 1.22+)
// Pre-1.22: cada gorutina captura la misma i
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — pasar como parámetro)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+: la variable de bucle tiene ámbito por iteración, así que la original es segura
6. Anti-patrones específicos de Go {#antipatrones}
| Anti-patrón | Preferido |
|---|---|
if err != nil { return err } repetido 5+ veces | aceptable — es Go idiomático |
panic para errores esperados | return err |
init() con efectos secundarios | inicialización explícita en main o constructores |
interface{} / any sin genéricos | usar genéricos (Go 1.18+) o interfaces tipadas |
| Campo Mutex no adyacente a los datos que protege | poner mu directamente encima del campo que protege |
| Canal de canales | generalmente señal de sobreingeniería; rediseñar |
time.Sleep en pruebas | usar hooks de testing o canales para sincronización |
| Tipos exportados con campos no exportados (cuando los campos son el objetivo principal) | structs estilo record con todos los campos exportados |
log.Fatal fuera de main | devolver errores hacia arriba en la pila |
Limitaciones
- Estas son guías específicas del lenguaje y no cubren decisiones arquitectónicas generales.
- La sobrecompresión puede reducir la legibilidad; aplicar criterio.