El shadowing (o sombreado) ocurre cuando declaras una variable con el mismo nombre en un scope (alcance) más interno que una variable declarada en un alcance superior. En Go, esto no es un error de sintaxis, sino una característica del modelo de ámbitos léxicos: el compilador siempre busca la declaración más cercana en el árbol de ejecución. Si declaras una variable err dentro de un bloque if, esa nueva err “tapa” a la err que definiste antes, volviendo la original inaccesible dentro de ese bloque.
Esto sucede porque Go utiliza un sistema de ámbitos anidados. Cuando el compilador encuentra un identificador, busca en el bloque actual; si no lo encuentra, sube al bloque padre, y así sucesivamente. El problema surge casi siempre por el uso del operador de declaración corta :=. Este operador no solo asigna valores, sino que declara nuevas variables si el identificador no existe en el scope actual. Si intentas inicializar una variable que ya existe en el scope superior usando :=, Go no actualizará la original, sino que creará una nueva variable local con el mismo nombre, dejando la original intacta y “sombreada”.
Debes usar esto solo cuando realmente necesites una variable con un ciclo de vida estrictamente limitado a un bloque pequeño, como un if o un for. Sin embargo, en el 99% de los casos de producción, el shadowing es un error accidental. Si lo haces por error, lo que rompes es la lógica de control de errores: la función retornará un valor nil (porque la variable original nunca fue modificada), haciendo que tu programa ignore fallos críticos y continúe su ejecución en un estado inconsistente, lo que derivará tarde o temprano en un panic: runtime error: invalid memory address or nil pointer dereference.
package main
import (
"errors"
"fmt"
)
// fetchData simula una operación que falla.
func fetchData() (string, error) {
return "", errors.New("error de conexión a la base de datos")
}
func processWorkflow() error {
// 1. Declaramos 'err' en el scope de la función.
// Su valor inicial es nil.
var err error
configPath := "/etc/app/config.yaml"
// Simulamos una condición que entra en el bloque.
if configPath != "" {
// 2. ¡ERROR DE SHADOWING!
// Al usar ':=', Go ve que 'data' es nueva y 'err' también es nueva
// para este bloque 'if'. No está asignando a la 'err' de arriba,
// está creando una 'err' local que solo vive dentro de este 'if'.
data, err := fetchData()
if err != nil {
// Este error se captura aquí, pero solo para este bloque.
fmt.Printf("[Log interno] Error detectado en el bloque: %v\n", err)
// Si no retornamos aquí, el error "muere" al salir del 'if'.
_ = data
}
}
// 3. Al llegar aquí, la 'err' de arriba (la que declaramos en la línea 15)
// sigue siendo nil. La 'err' que contenía el error fue destruida
// al salir del bloque 'if'.
return err
}
func main() {
// Ejecutamos el proceso que contiene el bug de shadowing.
if err := processWorkflow(); err != nil {
fmt.Printf("Resultado final: Error detectado -> %v\n", err)
} else {
// Esto se imprimirá a pesar de que fetchData falló.
fmt.Println("Resultado final: Éxito (¡Falso positivo por shadowing!)")
}
}
Desglose del ejemplo
En el código anterior, el problema principal reside en la línea 25: data, err := fetchData().
Aunque parece que estamos capturando el error en la variable err que declaramos al principio de la función, el uso de := le indica al compilador que queremos declarar nuevas variables para data y err dentro del ámbito del if. Esto crea un nuevo espacio en la pila para esa err local.
Cuando el programa llega a la línea 33 (return err), el identificador err se refiere a la variable declarada en la línea 15, no a la que se creó dentro del if. Como la variable original nunca recibió el valor de fetchData(), su valor sigue siendo nil. El programa termina la función con éxito, ocultando un fallo crítico de la base de datos.
El error frecuente es confundir el operador de asignación = con el de declaración corta :=.
// Código erróneo:
var err error
if true {
// Esto crea una nueva variable 'err' local
res, err := task()
}
return err // Siempre devuelve nil
// Código correcto:
var err error
if true {
// Esto asigna el valor a la variable 'err' original
res, err := task() // Esto sigue siendo shadowing si 'res' es nueva
// La forma correcta si ya existe err:
res, errorVar := task()
err = errorVar
}
Para evitar esto, si necesitas actualizar una variable ya declarada en un scope superior, asegúrate de usar = para la variable existente o declara las variables nuevas de forma explícita antes del bloque. Una herramienta excelente para detectar esto es el linter shadow de la suite golang.org/x/tools, que analiza tu código y te avisa si una variable ha sido sombreada accidentalmente.
N° 28