Pointeurs vs. Valeurs en Go : La frontière entre performance et sécurité
\n\nImaginez 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\nGo 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\nPourquoi ce sujet est-il si important ?
\n\nContrairement à 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
Passons directement aux choses sérieuses et regardons le code.
\n\nExemple n°1 : Petite structure
\n\nCommençons par le simple. Supposons que nous ayons une structure Point avec deux coordonnées :
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\nDans 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\nConclusion : 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\nExemple n°2 : Grande structure
\n\nImaginons maintenant que nous travaillions avec des données utilisateur :
\n\ntype 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\nIci, 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.
Lançons un simple benchmark :
\n\nfunc 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\nRésultat (sur du matériel réel) :
\n- \n
- PassByValue : ~120 ns/op, 0 allocations \n
- PassBy \n