Retour au cours

backend / go

Pointeurs

Leçon 91 exercice

Explication

Ce que vous allez apprendre

  • Comprendre ce que représente une adresse mémoire avec &x et *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 pointeur nil d'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.

SituationPasser par valeurPasser par pointeur
La fonction doit modifier l'argumentImpossible sans pointeurObligatoire
La struct est volumineuseCopie coûteuse à chaque appelCopie uniquement de l'adresse (8 octets)
La valeur peut être absente/optionnelleImpossible à représenternil 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.

go
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é

  • &x prend l'adresse, *p dé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 nil représente naturellement une valeur optionnelle/absente.

Exercices pratiques

1 disponible
1

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é.

Résoudre l’exercice →