Zurück zur Startseite

Go Fallstricke: slices channels nil

Der Artikel analysiert unerwartetes Verhalten von slices, channels, strings und nil in Go. Code-Beispiele werden mit Erklärung der internen Implementierung bereitgestellt. Material für Middle-/Senior-Entwickler.

Go Geheimnisse: warum slices und channels fehlschlagen
Advertisement 728x90

Fallstricke von Slices, Channels und nil in Go: Tiefgang in Implementierungsdetails

Go-Slices wirken einfach, doch ein vorhersehbares Verhalten erfordert das Verständnis ihrer internen Mechanismen. Da Slices auf ein zugrundeliegendes Array verweisen, können unerwartete Änderungen und Speicherlecks auftreten.

Betrachten wir den grundlegenden Fall der Array-Wiederverwendung:

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

Ausgabe: [1 99 3 4]. Die Änderung an b wirkt sich direkt auf das ursprüngliche Array a aus.

Google AdInline article slot

Nun fügen wir mit append an a hinzu:

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

Ausgabe: [1 2 3 4 5]. Die append-Operation hat ein neues Array allokiert, sodass Änderungen an b nicht mehr das Original beeinflussen.

Feinheiten von append und Kapazität

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

Ausgabe: [1 2 3 5]. Es gab genügend Kapazität, daher erfolgte keine Neuallokation.

Google AdInline article slot

Mit expliziter Beschränkung der Kapazität:

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

Ausgabe: [1 2 3 4]. Die Begrenzung der Kapazität löste eine Neuallokation aus.

Erweitern eines Slices über seine Länge hinaus:

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

Ausgabe:

[2 3]

[2 3 4 5]

Speicherlecks und Übergeben von Slices

Ein kleiner Slice aus einem großen Array hält den gesamten Puffer:

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

Übergeben per Wert ändert das zugrundeliegende Array, verändert aber die Kapazität nicht:

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

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

Ausgabe: [99 2 3].

Schleife mit Referenzen auf ein veränderliches Element:

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

Ausgabe: [4 1 2 3 4].

Inkonsistenz von nil für Slices und 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" // panic

Ein nil-Slice funktioniert problemlos, aber ein nil-Map panics bei Zuweisung.

Strings als Bytes

Strings speichern Bytes:

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

Überschreiben vordefinierter Bezeichner

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

Ausgabe: false, bob, 0.

Channels: Lesen und Schließen

Lesen erfordert die Prüfung des Kanalzustands:

ch := getCountChannel[int]()

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

Ein Kanal auf nil setzen deaktiviert den select-Zweig:

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

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

Nach close() werden Nullwerte vom Kanal gelesen:

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
}

Mit Prüfung:

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

Typisiertes nil in Schnittstellen

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
}

Eine Schnittstelle speichert sowohl Typ als auch Wert. Lösung:

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

Probleme mit for-range und Zeigern

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

Die Variable v wird in jeder Iteration wiederverwendet.

Wichtige Erkenntnisse:

  • Slices teilen sich ein zugrundeliegendes Array: Änderungen sind überall sichtbar.
  • append kann Speicher neu allozieren, wenn die Kapazität überschritten wird.
  • nil-Slices erlauben append; nil-Maps nicht.
  • Strings speichern Bytes; len() zählt Bytes.
  • Typisiertes nil in Schnittstellen ist nicht gleich nil.
  • for range erstellt eine einzige Schleifenvariable.

— Editorial Team

Advertisement 728x90

Weiterlesen