Go, que mucha gente busca como Golang, es un lenguaje pequeño, y por eso es fácil subestimarlo. Desarrolladores con años de Go a sus espaldas siguen cayendo en el mismo puñado de trampas. La mayoría de estos bugs compilan sin problema. El programa simplemente da una respuesta incorrecta, deja recursos sin liberar o provoca un deadlock en producción.
A continuación tienes los 10 más comunes. Cada uno incluye un ejemplo en Golang que puedes ejecutar y la solución idiomática.
Índice
- 1. Tratar los strings como caracteres en lugar de bytes
- 2. Olvidar reasignar el resultado de append
- 3. Valores cero que ocultan claves ausentes en un map
- 4. Usar %v en lugar de %w al envolver errores
- 5. Receptores por valor que descartan cambios sin avisar
- 6. Variables sombreadas que devuelven errores nil
- 7. Campos no exportados de un struct que desaparecen en JSON
- 8. Usar defer dentro de bucles
- 9. Ignorar errores con el identificador en blanco
- 10. Deadlocks por channels sin búfer
1. Tratar los strings como caracteres en lugar de bytes
Un string en Go es un slice de bytes de solo lectura, no de caracteres. len(s) devuelve el número de bytes, y acceder con s[i] devuelve un byte. Con texto ASCII no hay problema. Falla en cuanto un carácter ocupa más de un byte.
example.gogopackage main import ( "fmt" "unicode/utf8" ) func main() { word := "kött" fmt.Println("len:", len(word)) fmt.Println("runes:", utf8.RuneCountInString(word)) }
La palabra sueca para «carne» tiene cuatro caracteres pero cinco bytes. Cortar por índice de byte puede partir una rune por la mitad y producir UTF-8 no válido.
Solución: Usa utf8.RuneCountInString para contar caracteres. Al iterar, for i, r := range s te da runes. Para cortar un string, conviértelo primero a []rune.
2. Olvidar reasignar el resultado de append
append no actualiza el slice que le pasas. Cuando se agota la capacidad, reserva un nuevo array subyacente y devuelve un slice que apunta a él. Si ignoras el valor devuelto, los elementos añadidos se pierden.
example.gogopackage main import "fmt" func main() { nums := []int{1, 2, 3} appendWrong(nums) fmt.Println("wrong:", nums) nums = appendRight(nums) fmt.Println("right:", nums) } func appendWrong(s []int) { _ = append(s, 4) } func appendRight(s []int) []int { return append(s, 4) }
En Go, una llamada suelta a append(s, 4) es de hecho un error de compilación, porque el valor devuelto tiene que usarse. El _ = de appendWrong solo está ahí para pasar el compilador. Se comporta igual que llamar a append y no volver a asignar nunca el resultado.
Solución: Reasigna siempre con s = append(s, x). Si una función auxiliar hace crecer un slice, haz que devuelva el nuevo slice y reasígnalo en el punto de la llamada.
3. Valores cero que ocultan claves ausentes en un map
Leer una clave que no existe en un map no devuelve un error. Devuelve el valor cero del tipo de valor del map. En un map[string]int eso es 0, que no puedes distinguir de una clave a la que se le asignó cero a propósito.
example.gogopackage main import "fmt" func main() { scores := map[string]int{"alice": 0} if v := scores["alice"]; v == 0 { fmt.Println("alice looks the same as missing users") } if v, ok := scores["bob"]; ok { fmt.Println("bob:", v) } else { fmt.Println("bob is missing") } }
Solución: Usa el modismo comma-ok, v, ok := m[key]. El booleano te indica si la clave existía.
4. Usar %v en lugar de %w al envolver errores
fmt.Errorf con %v formatea el error original dentro de un nuevo string y descarta el valor del error. Con %w, el nuevo error envuelve al original, así que errors.Is y errors.As todavía pueden encontrarlo.
example.gogopackage main import ( "errors" "fmt" "os" ) func main() { _, err := os.Open("does-not-exist.txt") wrapped := fmt.Errorf("reading config: %w", err) fmt.Println(wrapped) fmt.Println("is not-exist:", errors.Is(wrapped, os.ErrNotExist)) }
Solución: Envuelve con %w siempre que quien llama pueda necesitar comprobar el error subyacente. Usa %v solo cuando el error original sea un detalle de implementación del que no quieres que dependa quien llama.
5. Receptores por valor que descartan cambios sin avisar
Un método con receptor por valor recibe una copia del struct. Lo que cambie en esa copia se pierde cuando el método retorna.
example.gogopackage main import "fmt" type Counter struct { n int } func (c Counter) IncValue() { c.n++ } func (c *Counter) IncPointer() { c.n++ } func main() { c := Counter{} c.IncValue() fmt.Println("after value receiver:", c.n) c.IncPointer() fmt.Println("after pointer receiver:", c.n) }
Solución: Usa un receptor de puntero cuando el método modifique el receptor o el struct sea lo bastante grande como para que copiarlo sea un desperdicio. Si algunos métodos de un tipo necesitan receptor de puntero, normalmente queda más limpio dárselo a todos.
6. Variables sombreadas que devuelven errores nil
Una declaración corta de variable (:=) dentro de un if crea una variable nueva que solo vive en ese bloque. Si sombrea sin querer un err exterior, el exterior sigue siendo nil y la función informa de éxito aunque algo haya fallado.
example.gogofunc loadUser(id string) (*User, error) { var err error user, err := fetch(id) if err != nil { return nil, err } if err := validate(user); err != nil { log.Println(err) // the outer err is still nil here } return user, err }
Solución: Asigna a la variable exterior con = en lugar de :=, o retorna de inmediato dentro del bloque if. Activa go vet -shadow o el linter govet para detectar estos casos antes de la revisión.
7. Campos no exportados de un struct que desaparecen en JSON
encoding/json solo ve los campos exportados. Un campo en minúscula es invisible para el marshaller, así que queda fuera de la salida sin ningún error.
example.gogopackage main import ( "encoding/json" "fmt" ) type User struct { Name string `json:"name"` email string Age int `json:"age,omitempty"` } func main() { u := User{Name: "Ada", email: "[email protected]", Age: 36} data, _ := json.Marshal(u) fmt.Println(string(data)) }
La salida no tiene la clave email, y nada te avisa de ello.
Solución: Escribe con mayúscula inicial los campos que quieras serializar y usa struct tags para fijar el nombre de la clave JSON. Añade omitempty a los campos opcionales.
8. Usar defer dentro de bucles
defer se ejecuta cuando retorna la función que lo contiene, no al final de cada iteración del bucle. Si un bucle abre mil archivos y hace defer de file.Close() para cada uno, los mil descriptores de archivo siguen abiertos hasta que la función termina.
example.gogofunc processAll(paths []string) error { for _, p := range paths { f, err := os.Open(p) if err != nil { return err } defer f.Close() // all closes pile up until processAll returns // ... read file ... } return nil }
Solución: Mueve el trabajo de cada iteración a una función auxiliar, para que defer se ejecute al final de cada llamada.
example.gogofunc processAll(paths []string) error { for _, p := range paths { if err := processOne(p); err != nil { return err } } return nil } func processOne(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() // ... read file ... return nil }
9. Ignorar errores con el identificador en blanco
result, _ := doThing() descarta cualquier fallo que reporte la función. El programa sigue adelante con un valor cero o un estado a medias, y cuando por fin se cae, lo hace en algún punto lejos de la causa real.
example.gogodata, _ := os.ReadFile("config.json") _ = json.Unmarshal(data, &cfg)
Si el archivo no existe, data es nil, Unmarshal devuelve un error que nadie lee y cfg se queda en su valor cero. Te enteras más tarde, cuando la aplicación actúa como si no hubiera ninguna configuración.
Solución: Maneja el error o devuélvelo a quien llama. Usa _ solo cuando el error de verdad no pueda importar, y deja un comentario breve que explique por qué.
10. Deadlocks por channels sin búfer
En un channel sin búfer, un envío solo se completa cuando un receptor toma el valor en ese mismo momento. Si nadie llega a recibir, el envío se bloquea para siempre.
example.gogopackage main import "fmt" func main() { ch := make(chan int) // unbuffered ch <- 42 // deadlock: no goroutine is receiving fmt.Println(<-ch) }
El envío espera a un receptor, pero la recepción está en la línea siguiente y nunca llega a ejecutarse. El runtime ve que todas las goroutines están atascadas y termina con fatal error: all goroutines are asleep - deadlock!.
Solución: Asegúrate de que haya una goroutine lista para recibir antes de enviar, o usa un channel con búfer cuando el productor y el consumidor van a velocidades distintas. Para un único resultado, un channel con un búfer de 1 es una opción segura por defecto.
example.gogopackage main import "fmt" func main() { ch := make(chan int, 1) // buffered ch <- 42 fmt.Println(<-ch) }
Sigue avanzando
Conocer estas diez trampas de Golang te ahorrará mucha depuración. El resto lo aprendes construyendo proyectos reales y topándote con casos límite con el depurador abierto. Si quieres práctica estructurada, la ruta de Go de LevelUpGo cubre estos patrones con ejercicios ejecutables y feedback automático sobre tu código.
