Volver al inicio

Trampas de Go: slices channels nil

El artículo analiza el comportamiento inesperado de slices, channels, strings y nil en Go. Se proporcionan ejemplos de código con explicación de la implementación interna. Material para desarrolladores intermedios/senior.

Secretos de Go: por qué slices y channels fallan
Advertisement 728x90

Errores comunes con slices, canales y nil en Go: Análisis profundo de detalles de implementación

Los slices en Go parecen sencillos, pero un comportamiento predecible requiere entender sus mecanismos internos. Dado que los slices hacen referencia a un array subyacente, pueden ocurrir mutaciones inesperadas y fugas de memoria.

Veamos el caso básico de reutilización de arrays:

a := []int{1, 2, 3, 4}
b := a[1:3] // b = [2, 3]
b[0] = 99
fmt.Println(a)

Salida: [1 99 3 4]. Modificar b afecta al array original a.

Google AdInline article slot

Ahora agreguemos append a a:

a := []int{1, 2, 3, 4}
b := a[1:3]
a = append(a, 5)
b[0] = 99 
fmt.Println(a)

Salida: [1 2 3 4 5]. La operación append reservó un nuevo array, por lo que los cambios en b ya no afectan el original.

Subtilezas de append y capacidad

func main() {
    a := []int{1, 2, 3, 4}
    _ = append(a[:3], 5)
    fmt.Println(a)
}

Salida: [1 2 3 5]. Había suficiente capacidad, así que no hubo reasignación.

Google AdInline article slot

Con límite explícito de capacidad:

func main() {
    a := []int{1, 2, 3, 4}
    _ = append(a[:3:3], 5)
    fmt.Println(a)
}

Salida: [1 2 3 4]. Limitar la capacidad activó la reasignación.

Extender un slice más allá de su longitud:

Google AdInline article slot
func main() {
    s := []int{1, 2, 3, 4, 5}[1:3]
    fmt.Println(s)
    extendedSlice := s[:4]
    fmt.Println(extendedSlice)
}

Salida:

[2 3]

[2 3 4 5]

Fugas de memoria y paso de slices

Un pequeño slice de un gran array mantiene todo el buffer:

bigArray := make([]int, 1e6)
smallSlice := bigArray[:10]

Pasar por valor muta el array subyacente pero no cambia la capacidad:

func modifySlice(s []int) {
    s[0] = 99
    s = append(s, 100)
}

func main() {
    s := []int{1, 2, 3}
    modifySlice(s)
    fmt.Println(s)
}

Salida: [99 2 3].

Bucle con referencias a un elemento mutable:

func main() {
    s := []int{}
    refs := []*int{}

    for i := 0; i < 5; i++ {
        s = append(s, i)
        refs = append(refs, &s[0])
    }

    *refs[4] = 4
    *refs[0] = 99999
    fmt.Println(s)
}

Salida: [4 1 2 3 4].

Inconsistencia de nil para slices y maps

var s []int
fmt.Println(len(s)) // 0
s = append(s, 1)

var m map[string]string
fmt.Println(len(m)) // 0
m["key"] = "value" // pánico

Un slice nulo funciona bien, pero un mapa nulo provoca pánico al asignar.

Cadenas como bytes

Las cadenas almacenan bytes:

func main() {
    str := "å"
    fmt.Println(str[1]) // 165
}
func main() {
    str := "Three"
    fmt.Println(len(str)) // 6
}

Sombreado de identificadores predefinidos

func main() {
    true := false
    uint := "bob"
    string := 0
    fmt.Printf("%v, %v, %v", true, uint, string)
}

Salida: false, bob, 0.

Canales: lectura y cierre

Leer requiere verificar el estado del canal:

ch := getCountChannel[int]()

if v, ok := <-ch; ok {
    fmt.Println(v)
} else {
    fmt.Println("canal cerrado")
}

Asignar un canal a nil desactiva la rama select:

var in <-chan int = ch
if paused {
    in = nil
}

select {
    case v := <-in:
        fmt.Println("recibido", v)
    case <-ctx.Done():
        return
}

Después de close(), se leen valores cero desde el canal:

func main() {
    ch := make(chan int, 1)
    ch <- 0
    close(ch)

    fmt.Println(<-ch) // 0 true
    fmt.Println(<-ch) // 0 false
    fmt.Println(<-ch) // 0 false
}

Con verificación:

v, ok := <-ch
fmt.Println(v, ok)

nil tipado en interfaces

type MyErr struct{}

func (MyErr) Error() string { return "boom" }

func f() error {
    var e *MyErr = nil
    return e
}

func main() {
    err := f()
    fmt.Println(err == nil) // false
}

Una interfaz almacena tanto tipo como valor. Solución:

func f() error {
    var e *MyErr = nil
    if e == nil {
        return nil
    }
    return e
}

Problemas con for range y punteros

vals := []int{1, 2, 3}
ptrs := []*int{}

for _, v := range vals {
    ptrs = append(ptrs, &v)
}

fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2]) // 3 3 3

La variable v se reutiliza en cada iteración.

Conclusiones clave:

  • Los slices comparten un array subyacente: los cambios son visibles en todas partes.
  • append puede reasignar memoria cuando se supera la capacidad.
  • Los slices nulos permiten append; los mapas nulos no.
  • Las cadenas almacenan bytes; len() cuenta bytes.
  • El nil tipado en interfaces no es igual a nil.
  • for range crea una única variable de bucle.

— Editorial Team

Advertisement 728x90

Leer después