La filosofía de Code Review Comments: Diseño de código idiomático

Escribir código que compile es el estándar mínimo; escribir código que el equipo de Go consideraría “idiomático” requiere entender la filosofía de diseño detrás de Code Review Comments. No es una guía de estilo superficial sobre dónde poner las llaves, sino un conjunto de principios para garantizar que el código sea legible, mantenible y que no cause fugas de recursos en producción.

Cuando revisas código, te encontrarás con tres pilares fundamentales. Primero, la gestión de interfaces: la regla de oro es aceptar interfaces, devolver estructuras. Si una función devuelve una interfaz, estás ocultando los detalles de implementación del tipo concreto, lo que impide al consumidor acceder a campos o métodos que no estén en la interfaz. En cambio, si la función devuelve una estructura concreta (como *os.File), el llamador decide qué interfaz implementar para consumirlo, manteniendo la flexibilidad en el lado del cliente.

Segundo, el manejo de errores. En Go, los errores son valores, no excepciones. No deben ser simplemente mensajes de texto, sino tipos que permitan la introspección mediante errors.As o errors.Is. Además, el diseño de la jerarquía de errores es crítico: los errores deben ser descriptivos pero no deben contener información que ya es evidente en el stack de llamadas.

Tercero, el control de la concurrencia. Una goroutine es una unidad de ejecución extremadamente ligera, pero es “fuego sin control” si no tiene un mecanismo de finalización. Si tu función lanza una goroutine, la firma de esa función debe dejar claro cómo se detiene (normalmente mediante un context.Context o un canal de señalización). De lo contrario, estás creando una fuga de goroutines que eventualmente agotará la memoria del proceso.

package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

// CustomError permite la inspección de errores mediante errors.As.
type CustomError struct {
	Op  string
	Err error
}

func (e *CustomError) Error() string {
	return fmt.Sprintf("operation %s: %v", e.Op, e.Err)
}

// UserStore define el comportamiento, no la implementación.
// El consumidor define esta interfaz para desacoplarse de la implementación.
type UserStore interface {
	SaveUser(ctx context.Context, id string) error
}

// MemoryStore es una implementación concreta. 
// Devolvemos el puntero a la estructura, no la interfaz.
type MemoryStore struct {
	data map[string]string
}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{data: make(map[string]string)}
}

// SaveUser implementa UserStore.
func (m *MemoryStore) SaveUser(ctx context.Context, id string) error {
	// Simulamos una operación lenta que respeta el contexto.
	select {
	case <-time.After(100 * time.Millisecond):
		if id == "" {
			return &CustomError{Op: "SaveUser", Err: errors.New("empty ID")}
		}
		m.data[id] = "active"
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

// Processor encapsula la lógica de negocio.
type Processor struct {
	store UserStore
}

// Process ejecuta la lógica de forma concurrente pero controlada.
func (p *Processor) Process(ctx context.Context, userID string) error {
	// El canal tiene buffer para evitar que la goroutine se bloquee
	// si el proceso principal termina antes de que escriba.
	errCh := make(chan error, 1)

	go func() {
		// La goroutine depende del contexto para no quedar huérfana.
		errCh <- p.store.SaveUser(ctx, userID)
	}()

	select {
	case err := <-errCh:
		if err != nil {
			return fmt.Errorf("processing failed: %w", err)
		}
		return nil
	case <-ctx.Done():
		// Si el contexto se cancela, salimos; la goroutine morirá 
		// eventualmente al intentar escribir en el canal buffered.
		return ctx.Err()
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	store := NewMemoryStore()
	proc := Processor{store: store}

	if err := proc.Process(ctx, "user_123"); err != nil {
		fmt.Printf("Error detectado: %v\n", err)
		return
	}

	fmt.Println("Operación completada con éxito")
}

Desglose del ejemplo

En el ejemplo, NewMemoryStore devuelve *MemoryStore (la estructura concreta) en lugar de la interfaz UserStore. Esto es fundamental: si deviniéramos la interfaz, el usuario de NewMemoryStore no podría acceder directamente a la propiedad data de MemoryStore si necesitara inspeccionarla, a pesar de que sabe exactamente qué tipo es.

La función Process demuestra el manejo correcto de la concurrencia. Nota que errCh se inicializa con un buffer de tamaño 1: make(chan error, 1). Si el select elige el caso ctx.Done(), la goroutine que está ejecutando p.store.SaveUser eventualmente intentará enviar su resultado al canal. Si el canal no tuviera buffer, esa goroutine se quedaría bloqueada permanentemente esperando un receptor que nunca llegará, causando una goroutine leak.

El error en Process se envuelve usando %w. Esto no es solo para tener mensajes más bonitos; es para preservar la cadena de errores original, permitiendo que quien llame a Process pueda usar errors.As para extraer el CustomError original y decidir qué hacer.

El error frecuente

Un error clásico que los revisores de Go penalizan es devolver un nil de un tipo personalizado que implementa la interfaz error.

// MAL: Esto causará comportamientos inesperados
func doSomething() error {
    var err *CustomError = nil
    if someCondition {
        err = &CustomError{Op: "fail", Err: errors.New("bad stuff")}
    }
    return err // Si err es nil, devuelve un error que NO es nil para el receptor
}

// Uso erróneo:
err := doSomething()
if err != nil {
    // Esto SE EJECUTARÁ, aunque la función devolvió un puntero nil, 
    // porque la interfaz error contiene un tipo no-nil (el puntero nil).
    fmt.Println("Hay un error")
}

Cuando una función devuelve una interfaz (como error), si devuelves un puntero que es nil, la interfaz resultante no es nil (porque la interfaz contiene tanto el valor como el tipo, y el tipo está presente). Para evitar esto, la regla es: siempre devuelve el tipo error explícitamente en el valor de retorno, no un puntero de un tipo personalizado.

255

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio