Volver al blog

10 errores comunes en Go (Golang) que debes evitar

Conoce los 10 errores más comunes al programar en Go (Golang) y cómo evitarlos: bytes en strings, append en slices, valores cero en maps, envolver errores, receptores por valor, defer en bucles, channels sin búfer y más.

10 errores comunes en Go (Golang) que debes evitar

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

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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
func 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.gogo
package 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.gogo
func 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.gogo
func 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.gogo
data, _ := 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.gogo
package 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.gogo
package 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.

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita