
Pointeurs : l'essentiel (opérateurs `&` et `*`, quand les utiliser simplement)
Comprenez les bases des pointeurs en Go : obtenez l'adresse avec `&`, accédez à la valeur avec `*`, et découvrez quand les utiliser simplement pour modifier des variables ou gérer des structs.
Sous le capot : Qu'est-ce qu'un pointeur ?
Jusqu'à présent, lorsque nous manipulions des variables (entiers, chaînes, structs...), nous travaillions directement avec leurs valeurs. Cependant, il existe une autre façon d'interagir avec les variables : en utilisant leur adresse mémoire. Chaque variable que vous déclarez est stockée quelque part dans la mémoire de votre ordinateur, et cet emplacement a une adresse unique.
Un pointeur est simplement une variable spéciale dont la valeur est l'adresse mémoire d'une autre variable. Au lieu de contenir un `int`, un `string` ou une `Personne`, un pointeur contient l'"endroit où trouver" un `int`, un `string` ou une `Personne`.
Pourquoi utiliser des pointeurs ? En Go, ils servent principalement à deux choses simples :
- Permettre à une fonction de modifier directement la variable originale qui lui a été passée en argument (simuler un "passage par référence").
- Améliorer l'efficacité en évitant la copie de grosses structures de données lorsqu'elles sont passées à des fonctions (bien que ce soit souvent une optimisation secondaire).
- Permettre la valeur `nil` pour indiquer l'absence d'une valeur (particulièrement utile avec les structs).
Obtenir l'adresse : L'opérateur `&`
Pour obtenir l'adresse mémoire d'une variable existante, on utilise l'opérateur "adresse de" : `&`. Placé devant une variable, `&variable` retourne un pointeur contenant l'adresse de cette variable.
Le type d'un pointeur est indiqué par un astérisque `*` suivi du type de la valeur pointée. Par exemple, un pointeur vers une variable de type `int` aura le type `*int`. Un pointeur vers une variable de type `Personne` aura le type `*Personne`.
package main
import "fmt"
func main() {
x := 42
fmt.Printf("Valeur de x: %d\n", x)
fmt.Printf("Type de x: %T\n", x) // Type: int
// p est un pointeur vers x. Il contient l'adresse de x.
p := &x
fmt.Printf("Valeur de p (adresse de x): %p\n", p) // %p formate une adresse mémoire (pointeur)
fmt.Printf("Type de p: %T\n", p) // Type: *int (pointeur vers int)
s := "hello"
ps := &s
fmt.Printf("Valeur de ps (adresse de s): %p\n", ps)
fmt.Printf("Type de ps: %T\n", ps) // Type: *string (pointeur vers string)
}La sortie des adresses (`%p`) sera une série de chiffres hexadécimaux qui varient à chaque exécution, car l'emplacement mémoire peut changer.Accéder à la valeur pointée : L'opérateur `*` (Déréférencement)
Si vous avez un pointeur (qui contient une adresse), comment accéder à la valeur qui se trouve *à cette adresse* ? C'est le rôle de l'opérateur de déréférencement (ou indirection) : `*`. Placé devant une variable de type pointeur, `*pointeur` retourne la valeur stockée à l'adresse contenue dans le pointeur.
Ne confondez pas l'utilisation de `*` dans une déclaration de type (`var p *int`) qui signifie "p est un pointeur vers int", et l'utilisation de `*` devant une variable pointeur (`*p`) qui signifie "donne-moi la valeur à l'adresse contenue dans p".
package main
import "fmt"
func main() {
y := 100
ptrY := &y // ptrY contient l'adresse de y
fmt.Printf("Adresse dans ptrY: %p\n", ptrY)
// Déréférencer ptrY pour obtenir la valeur de y
valeurY := *ptrY
fmt.Printf("Valeur pointée par ptrY: %d\n", valeurY) // Affiche: 100
fmt.Printf("Type de valeurY: %T\n", valeurY) // Affiche: int
// On peut aussi modifier la valeur originale via le pointeur déréférencé
fmt.Printf("Valeur de y avant modification via ptrY: %d\n", y) // Affiche: 100
*ptrY = 200 // Modifie la valeur à l'adresse contenue dans ptrY
fmt.Printf("Valeur de y après modification via ptrY: %d\n", y) // Affiche: 200 !
}Dans la dernière partie, `*ptrY = 200` signifie : "Va à l'adresse stockée dans `ptrY` (qui est l'adresse de `y`), et mets la valeur 200 à cet endroit". Cela modifie bien la variable originale `y`.Cas d'usage simple 1 : Modifier un argument de fonction
Par défaut, Go passe les arguments aux fonctions par valeur. Cela signifie que la fonction reçoit une copie de l'argument. Si la fonction modifie sa copie, la variable originale en dehors de la fonction n'est pas affectée.
Exemple (sans pointeur) :
package main
import "fmt"
func incrementerCopie(val int) {
val++ // Modifie SEULEMENT la copie locale 'val'
fmt.Printf("Dans incrementerCopie, val = %d\n", val)
}
func main() {
compteur := 5
fmt.Printf("Avant appel, compteur = %d\n", compteur)
incrementerCopie(compteur)
fmt.Printf("Après appel, compteur = %d\n", compteur) // Reste 5 !
}Sortie :Avant appel, compteur = 5
Dans incrementerCopie, val = 6
Après appel, compteur = 5Pour permettre à une fonction de modifier la variable originale, vous devez passer un pointeur vers cette variable. La fonction prendra alors un argument de type pointeur (`*int` au lieu de `int`), et pourra modifier la valeur originale en déréférençant ce pointeur.
Exemple (avec pointeur) :
package main
import "fmt"
// Prend un pointeur vers un int
func incrementerOriginal(ptrVal *int) {
// Déréférence le pointeur et incrémente la valeur pointée
*ptrVal++
fmt.Printf("Dans incrementerOriginal, *ptrVal = %d\n", *ptrVal)
}
func main() {
compteur := 5
fmt.Printf("Avant appel, compteur = %d\n", compteur)
// Passe l'ADRESSE de compteur à la fonction
incrementerOriginal(&compteur)
fmt.Printf("Après appel, compteur = %d\n", compteur) // Modifié à 6 !
}Sortie :Avant appel, compteur = 5
Dans incrementerOriginal, *ptrVal = 6
Après appel, compteur = 6C'est un usage très courant des pointeurs, notamment pour modifier des structs passées à des fonctions ou méthodes.Cas d'usage simple 2 : Pointeurs et Structs
Comme vu précédemment, il est très courant de travailler avec des pointeurs vers des structs (`*NomStruct`). L'une des raisons est de permettre aux fonctions (ou méthodes) de modifier la struct originale. Une autre raison (plus avancée) est d'éviter de copier potentiellement de grosses structs lors des appels de fonction.
Rappelons la syntaxe pratique de Go : même si vous avez un pointeur vers une struct (`pPtr *Personne`), vous pouvez accéder à ses champs directement avec l'opérateur point (`pPtr.Nom`), sans avoir besoin de déréférencer explicitement (`(*pPtr).Nom`).
package main
import "fmt"
type Point struct {
X, Y int
}
// Fonction qui modifie un Point via un pointeur
func deplacer(pt *Point, dx, dy int) {
pt.X += dx // Accès direct via pointeur
pt.Y += dy // Accès direct via pointeur
}
func main() {
p := Point{X: 10, Y: 20}
fmt.Printf("Point initial: %+v\n", p)
// Passe l'adresse de p à la fonction deplacer
deplacer(&p, 5, -3)
fmt.Printf("Point déplacé: %+v\n", p) // p a été modifié
}Sortie :Point initial: {X:10 Y:20}
Point déplacé: {X:15 Y:17}De plus, un pointeur peut avoir la valeur `nil`. Cela peut être utile pour représenter l'absence d'une valeur ou un état optionnel. Par exemple, une fonction pourrait retourner un `*Personne`. Si la personne est trouvée, elle retourne un pointeur vers la struct `Personne` ; sinon, elle retourne `nil`.
var ptrPoint *Point // ptrPoint est initialisé à nil
if ptrPoint == nil {
fmt.Println("Le pointeur de point est nil.")
}
// Tenter de faire ptrPoint.X ici provoquerait une panique.
// Initialisation
ptrPoint = &Point{1, 1}
if ptrPoint != nil {
fmt.Printf("Le pointeur pointe maintenant vers: %+v\n", *ptrPoint)
}L'essentiel à retenir sur les pointeurs (pour débuter)
Les pointeurs peuvent sembler intimidants au début, mais leur usage de base en Go est assez direct :
- Un pointeur stocke l'adresse mémoire d'une autre variable.
- L'opérateur `&` donne l'adresse d'une variable (`&maVar` -> pointeur).
- L'opérateur `*` déréférence un pointeur pour accéder à la valeur pointée (`*monPtr` -> valeur).
- Le type d'un pointeur est `*TypeDeLaValeur` (ex: `*int`, `*string`, `*MaStruct`).
- La valeur zéro d'un pointeur est `nil`.
- Utilisez un pointeur comme argument de fonction (`func maFonction(ptr *MonType)`) si vous voulez que la fonction puisse modifier la variable originale passée en argument (en passant son adresse : `maFonction(&maVariable)`).
- L'accès aux champs d'un pointeur de struct est simplifié : `monPtrDeStruct.Champ`.
Bien que les pointeurs aient d'autres implications (gestion mémoire, performance), cette compréhension de base est suffisante pour aborder la plupart des scénarios courants en Go, notamment la modification de structs et l'interaction avec les fonctions et méthodes.