Retour à l'accueil

Pièges Go : slices channels nil

L'article analyse le comportement inattendu des slices, channels, strings et nil en Go. Des exemples de code sont fournis avec explication de l'implémentation interne. Matériel pour développeurs middle/senior.

Secrets Go : pourquoi les slices et channels échouent
Advertisement 728x90

Pièges des tranches, canaux et nil en Go : analyse approfondie des détails d'implémentation

Les tranches (slices) en Go semblent simples, mais un comportement prévisible exige une compréhension de leurs mécanismes internes. Puisqu'une tranche fait référence à un tableau sous-jacent, des modifications inattendues et des fuites de mémoire peuvent survenir.

Examinons le cas de base de la réutilisation du tableau :

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

Sortie : [1 99 3 4]. Modifier b affecte le tableau original a.

Google AdInline article slot

Ajoutons maintenant append à a :

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

Sortie : [1 2 3 4 5]. L'opération append a alloué un nouveau tableau, donc les modifications apportées à b n'affectent plus l'original.

Subtilités de append et de la capacité

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

Sortie : [1 2 3 5]. Il y avait assez de capacité, donc aucune réallocation n’a eu lieu.

Google AdInline article slot

Avec une limitation explicite de capacité :

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

Sortie : [1 2 3 4]. La limitation de capacité a déclenché une réallocation.

Étendre une tranche au-delà de sa longueur :

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

Sortie :

[2 3]

[2 3 4 5]

Fuites de mémoire et passage de tranches

Une petite tranche provenant d’un grand tableau conserve tout le tampon :

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

Le passage par valeur modifie le tableau sous-jacent sans changer la capacité :

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

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

Sortie : [99 2 3].

Boucle avec références à un élément 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)
}

Sortie : [4 1 2 3 4].

Incohérence de nil pour les tranches et les 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" // panique

Une tranche nil fonctionne bien, mais une map nil provoque une panique lors de l’assignation.

Chaînes comme octets

Les chaînes stockent des octets :

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

Masquage des identifiants prédéfinis

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

Sortie : false, bob, 0.

Canaux : lecture et fermeture

La lecture nécessite de vérifier l’état du canal :

ch := getCountChannel[int]()

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

Affecter nil à un canal désactive la branche select :

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

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

Après close(), les valeurs zéro sont lues depuis le 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
}

Avec vérification :

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

nil typé dans les 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
}

Une interface contient à la fois le type et la valeur. Solution :

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

Problèmes avec for range et les pointeurs

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 est réutilisée à chaque itération.

Points clés :

  • Les tranches partagent un tableau sous-jacent : les modifications sont visibles partout.
  • append peut réallouer de la mémoire lorsque la capacité est dépassée.
  • Les tranches nil permettent append; les maps nil non.
  • Les chaînes stockent des octets ; len() compte les octets.
  • Un nil typé dans une interface n’est pas égal à nil.
  • for range crée une seule variable de boucle.

— Editorial Team

Advertisement 728x90

Lire ensuite