Zpět na domů

Podvodné kameny Go: slicy kanály nil

Článek rozebírá neočekávané chování sliců, kanálů, stringů a nil v Go. Uvádějí se příklady kódu s vysvětlením vnitřní implementace. Materiál pro middle/senior vývojáře.

Tajemství Go: proč slicy a kanály selhávají
Advertisement 728x90

Světelné pasti v Go: slajsy, kanály a nil

Slajsy v Go vypadají jednoduše, ale pro předvídatelné chování je třeba pochopit jejich interní implementaci. Slajsy odkazují na základní pole, což může vést k neočekávaným změnám a únikům paměti.

Zvažme základní případ opakovaného použití pole:

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

Výstup: [1 99 3 4]. Změna v b ovlivňuje původní pole a.

Google AdInline article slot

Přidejme append k a:

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

Výstup: [1 2 3 4 5]. append vyhradil nové pole, takže změna v b nemá žádný dopad na původní pole.

Jemnosti funkce append a kapacity

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

Výstup: [1 2 3 5]. Kapacita byla dostatečná, pole nebylo přerozděleno.

Google AdInline article slot

S explicitním nastavením kapacity:

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

Výstup: [1 2 3 4]. Omezení kapacity způsobilo přerozdělení pole.

Rozšíření slajsu mimo délku:

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

Výstup:

[2 3]

[2 3 4 5]

Úniky paměti a předávání slajsových proměnných

Malý slajsový úsek z velkého pole udržuje celý buffer:

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

Předávání hodnoty mění podkladové pole, ale nepřesahuje kapacitu:

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

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

Výstup: [99 2 3].

Cyklus s odkazy na měnitelný prvek:

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)
}

Výstup: [4 1 2 3 4].

Nesrovnalost nil pro slajsy a mapy

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" // panic

Slajsová proměnná s hodnotou nil funguje, mapa nikoli.

Řetězce jako bajty

Řetězce ukládají bajty:

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

Příkrytí předdefinovaných identifikátorů

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

Výstup: false, bob, 0.

Kanály: čtení a uzavírání

Čtení vyžaduje ověření stavu:

ch := getCountChannel[int]()

if v, ok := <-ch; ok {
    fmt.Println(v)
} else {
    fmt.Println("kanál uzavřen")
}

Nulování kanálu deaktivuje větev select:

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

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

Po zavření kanálu se čtou nulové hodnoty:

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
}

S kontrolou:

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

Typizovaný nil v rozhraních

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
}

Rozhraní obsahuje typ i hodnotu. Řešení:

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

Problémy s for range a ukazateli

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

Proměnná v je režírována.

Důležité:

  • Slajsy odkazují na společné pole: změny jsou viditelné všude.
  • append může přerozdělit paměť při překročení kapacity.
  • nil slajsová proměnná umožňuje append, nil mapa ne.
  • Řetězce ukládají bajty, len() počítá bajty.
  • Rozhraní s typizovaným nil není rovno nil.
  • for range vytváří jednu proměnnou cyklu.

— Editorial Team

Advertisement 728x90

Číst dál