Pointeurs vs. Valeurs en Go : comment ne pas tuer les performances

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Pointeurs vs. Valeurs en Go : La frontière entre performance et sécurité

\n\n

Imaginez la situation : vous codez en Go, tout se passe bien, mais soudain vous remarquez que l'application commence à « ralentir » sur de gros volumes de données. Vous ouvrez le profileur — et vous voyez que les allocs/op (allocations par opération) explosent. Cela vous dit quelque chose ? Il y a de fortes chances que le problème vienne de la façon dont vous transmettez les données aux fonctions et les renvoyez. La question « Pointers vs. values in parameters and return values » est l'une des plus brûlantes sur Stack Overflow (des milliers de vues !), et nous allons l'analyser aujourd'hui en français, de manière vivante et avec des exemples concrets.

\n\n

Go est un langage qui aime la simplicité. Mais cette simplicité est trompeuse : le choix entre la transmission d'une valeur et d'un pointeur peut modifier radicalement la vitesse d'exécution du programme et sa consommation mémoire. Voyons quand utiliser quoi, pour que votre code vole, et non rampe.

\n\n

Pourquoi ce sujet est-il si important ?

\n\n

Contrairement à Java ou Python, où tout est une référence à un objet, en Go vous choisissez explicitement : transmettre une copie de la structure ou son adresse. Cela semble un détail. Mais en réalité, cette décision fondamentale impacte :

\n\n
    \n
  • La vitesse d'exécution — copier de grandes structures prend du temps.
  • \n
  • La consommation mémoire — des copies supplémentaires = des allocations supplémentaires.
  • \n
  • La sécurité des données — les pointeurs permettent de muter l'original, les valeurs non.
  • \n
  • Le travail avec les interfaces — les interfaces en Go sont un mécanisme astucieux qui fonctionne différemment avec les pointeurs et les valeurs.
  • \n
\n\n

Passons directement aux choses sérieuses et regardons le code.

\n\n

Exemple n°1 : Petite structure

\n\n

Commençons par le simple. Supposons que nous ayons une structure Point avec deux coordonnées :

\n\n
type Point struct {\n    X, Y float64\n}\n\n// Passage par valeur\nfunc MoveByValue(p Point, dx, dy float64) Point {\n    p.X += dx\n    p.Y += dy\n    return p\n}\n\n// Passage par pointeur\nfunc MoveByPointer(p *Point, dx, dy float64) {\n    p.X += dx\n    p.Y += dy\n}
\n\n

Dans le premier cas, nous créons une copie de la structure, la modifions et en renvoyons une nouvelle. Dans le second, nous modifions l'original directement. Quelle version est la plus rapide ? Pour une si petite structure (16 octets) — il n'y a presque pas de différence. Copier 16 octets est une opération atomique, et le GC (ramasse-miettes) ne crée pas de charge supplémentaire. De plus, le passage par valeur peut même être plus rapide, car le pointeur nécessite un déréférencement supplémentaire.

\n\n

Conclusion : Pour les petites structures (jusqu'à 4-8 champs de types de base), utilisez les valeurs sans crainte. C'est plus sûr et n'ajoute pas de surcharge.

\n\n

Exemple n°2 : Grande structure

\n\n

Imaginons maintenant que nous travaillions avec des données utilisateur :

\n\n
type User struct {\n    ID        int64\n    Name      string\n    Email     string\n    Avatar    []byte // 1 MB d'image\n    CreatedAt time.Time\n    UpdatedAt time.Time\n}
\n\n

Ici, Avatar est une slice, qui en Go est un type de référence (en-tête : pointeur vers les données, longueur, capacité). Mais la structure elle-même, lors de la copie, ne copiera que l'en-tête de la slice (24 octets), et non les octets eux-mêmes. Cependant, si la structure contient beaucoup de champs, copier 100+ octets à chaque appel de fonction devient déjà perceptible.

\n\n

Lançons un simple benchmark :

\n\n
func BenchmarkPassByValue(b *testing.B) {\n    u := User{ID: 1, Name: "John", Avatar: make([]byte, 1024*1024)}\n    for i := 0; i < b.N; i++ {\n        _ = processUserByValue(u)\n    }\n}\n\nfunc BenchmarkPassByPointer(b *testing.B) {\n    u := &User{ID: 1, Name: "John", Avatar: make([]byte, 1024*1024)}\n    for i := 0; i < b.N; i++ {\n        _ = processUserByPointer(u)\n    }\n}
\n\n

Résultat (sur du matériel réel) :

\n
    \n
  • PassByValue : ~120 ns/op, 0 allocations
  • \n
  • PassBy
  • \n

Blogs

Book Recommendations