backend / go
Pointeurs
Explication
Ce que vous allez apprendre
- Comprendre ce que représente une adresse mémoire avec
&xet*p - Savoir quand passer une valeur par pointeur plutôt que par valeur
- Comprendre pourquoi retourner un pointeur vers une variable locale est sûr en Go
- Utiliser
new()et distinguer un pointeurnild'un pointeur valide - Relier ce concept aux receivers de méthodes vus dans la leçon sur les structs
Dans quel contexte ?
Un développeur écrit une fonction censée réinitialiser le solde d'un compte bancaire à zéro, mais après l'appel de cette fonction, le solde affiché n'a pas changé du tout. Le bug vient presque toujours de la même erreur : la fonction a reçu une copie de la struct Compte par valeur, l'a modifiée localement, puis cette copie a été jetée à la fin de la fonction sans jamais toucher l'original.
D'abord, il faut visualiser ce qu'est réellement une adresse mémoire
Chaque variable Go occupe un emplacement précis en mémoire. L'opérateur &x récupère cette adresse (un pointeur), et l'opérateur *p fait l'inverse : il déréférence le pointeur pour lire ou écrire la valeur qu'il désigne.
Contrairement au C, Go ne permet aucune arithmétique de pointeurs (impossible de faire p + 1 pour "avancer" dans la mémoire) : cette restriction volontaire élimine toute une classe de bugs de sécurité mémoire liés à un pointeur qui dériverait vers une zone invalide.
Une fois cette mécanique comprise, la vraie question devient : pourquoi l'utiliser ?
Passer une variable par valeur à une fonction en crée une copie complète ; toute modification à l'intérieur de la fonction reste locale à cette copie. Passer un pointeur, au contraire, donne à la fonction un accès direct à la donnée d'origine.
| Situation | Passer par valeur | Passer par pointeur |
|---|---|---|
| La fonction doit modifier l'argument | Impossible sans pointeur | Obligatoire |
| La struct est volumineuse | Copie coûteuse à chaque appel | Copie uniquement de l'adresse (8 octets) |
| La valeur peut être absente/optionnelle | Impossible à représenter | nil représente naturellement l'absence |
Prérequis
Cette leçon éclaire un mécanisme déjà utilisé implicitement dans la leçon sur les structs et méthodes (receiver par pointeur) : une relecture rapide de cette leçon aide à faire le lien.
Il reste une inquiétude légitime à lever : et si la variable pointée disparaît ?
En C, retourner l'adresse d'une variable locale est dangereux, car cette variable est détruite à la fin de la fonction (elle vit "sur la pile"). Go résout ce problème automatiquement grâce à l'escape analysis : le compilateur détecte qu'une variable locale doit survivre à sa fonction (parce qu'un pointeur vers elle est retourné) et la place alors sur le tas (heap) au lieu de la pile.
Bonne pratique
Fais confiance à l'escape analysis de Go : return &Compteur{Valeur: 0} est parfaitement sûr, contrairement à l'équivalent en C. Tu n'as jamais besoin de gérer manuellement où vit une variable en mémoire.
Enfin, un pointeur peut légitimement ne rien désigner
Un pointeur non initialisé vaut nil par défaut — sa zero value. C'est en réalité une façon très naturelle de représenter "cette valeur est optionnelle, elle n'existe peut-être pas", très utilisé pour des champs de struct facultatifs.
Piège fréquent
Déréférencer un pointeur nil (*pn alors que pn == nil) provoque un panic immédiat au runtime. Toujours vérifier if pn == nil avant de déréférencer un pointeur dont la validité n'est pas garantie par le contexte.
Maintenant que tu comprends comment les données circulent réellement en mémoire, il est temps d'aborder le pattern qui s'appuie sur tout ce que tu as vu jusqu'ici — retours multiples, interfaces, pointeurs : la gestion d'erreurs idiomatique de Go.
Commandes & code
Pointeurs
Go a des pointeurs mais pas d'arithmétique de pointeurs (contrairement à C) : plus sûr.
package main
import "fmt"
func incrementer(n *int) {
*n = *n + 1 // dereferencer avec * pour lire/ecrire la valeur pointee
}
func incrementerCopie(n int) {
n = n + 1 // ne modifie qu'une copie locale, sans effet exterieur
}
type Compteur struct {
Valeur int
}
func (c *Compteur) Incrementer() {
c.Valeur++
}
func nouveauCompteur() *Compteur {
// retourner un pointeur vers une variable locale est SUR en Go :
// le compilateur fait "escape analysis" et place la variable sur le heap.
return &Compteur{Valeur: 0}
}
func main() {
x := 10
p := &x // p contient l'adresse de x
fmt.Println(p, *p) // adresse, puis valeur pointee
*p = 20
fmt.Println(x) // 20 : x a change via le pointeur
incrementer(&x)
fmt.Println(x) // 21
incrementerCopie(x)
fmt.Println(x) // toujours 21 : passe par valeur, aucun effet
c := nouveauCompteur()
c.Incrementer()
c.Incrementer()
fmt.Println(c.Valeur) // 2
// new() alloue une zero value et retourne un pointeur
np := new(int)
*np = 42
fmt.Println(*np)
// Pointeur nil : verifier avant de dereferencer
var pn *int
if pn == nil {
fmt.Println("pointeur nil, rien a dereferencer")
}
// Pointeur vers pointeur (rare, mais valide)
pp := &p
fmt.Println(**pp)
// Quand utiliser un pointeur :
// 1. la fonction doit MODIFIER l'argument
// 2. la struct est grosse (eviter de copier)
// 3. la valeur peut etre nil (optionnelle)
var utilisateur *Compteur // nil par defaut : represente "absence de valeur"
fmt.Println(utilisateur == nil)
}Résumé
&xprend l'adresse,*pdéréférence — pas d'arithmétique de pointeurs.- Passer par pointeur permet de modifier l'original ; par valeur, c'est une copie isolée.
- Go gère l'escape analysis : retourner
&struct{}local est sûr (placé sur le heap si nécessaire). - Un pointeur
nilreprésente naturellement une valeur optionnelle/absente.
Exercices pratiques
Mission : une remise à zéro de compte qui ne remet rien à zéro
Objectif : Corriger une fonction qui modifie une copie au lieu de l'original, puis raisonner sur la sécurité mémoire des pointeurs retournés par Go.
Contexte
Le service comptes bancaires expose une fonction remiseAZero(c Compte) censée remettre le solde à 0 en cas de fraude confirmée. Après son appel, les logs de l'incident confirment que la fonction s'est bien exécutée sans erreur, mais le solde du compte en base reste inchangé.