ProgrammationDéveloppeur Go, Développeur Backend

Expliquez les particularités de la transmission et du retour de grandes structures depuis des fonctions en Go, et comment cela influence les performances et le comportement du programme.

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En Go, les structures (struct) sont par défaut transmises et retournées par valeur. Cela signifie qu'à l'appel d'une fonction ou au retour de celle-ci, la structure entière est copiée. Pour les petites structures, cela est transparent, mais pour les grandes, la question est critique.

Historique de la question

À l'origine, Go était orienté vers un travail efficace avec un faible nombre d'allocations. Cependant, le danger d'une copie involontaire de grandes données est apparu lorsque les structures utilisent de nombreux champs et objets imbriqués. Les performances de telles opérations peuvent en pâtir, et parfois la différence ne se révèle qu'au cours du profilage ou en raison de la gêne du GC.

Problème

Si une structure a une grande taille, sa copie à chaque appel de fonction, retour ou affectation s'avère coûteuse. Cela conduit à :

  • une augmentation du temps d'exécution ;
  • une charge sur le GC (copy-on-write pour les grands champs, retard dans le nettoyage de la mémoire) ;
  • des erreurs lorsque les modifications apportées à la copie ne se retrouvent pas dans l'original.

Solution

Pour les grandes structures, il est recommandé de passer et de retourner un pointeur vers la structure (*T), et non l'objet lui-même. Cela réduit les frais généraux et assure le travail avec une seule instance de données.

Exemple de code :

package main import "fmt" type Large struct { Data [1024]int } // Passage par valeur (incorrect pour les grands objets) func ValueProcess(l Large) { l.Data[0] = 123 // ne modifie que la copie } // Passage par pointeur func PointerProcess(l *Large) { l.Data[0] = 456 // modifie l'original } func main() { a := Large{} ValueProcess(a) fmt.Println("Après ValueProcess:", a.Data[0]) // 0 PointerProcess(&a) fmt.Println("Après PointerProcess:", a.Data[0]) // 456 }

Caractéristiques clés :

  • Toutes les structures sont par défaut copiées par valeur ;
  • Le passage de l'adresse (pointeur) permet d'éviter la copie ;
  • Le retour par valeur peut être efficacement optimisé par le compilateur pour les petites structures, mais pas pour les grandes.

Questions pièges.

1. Peut-on retourner un pointeur sur une variable locale de structure depuis une fonction en Go ?

Oui. Go garantit la validité de tels pointeurs, en déplaçant automatiquement dans le tas les valeurs pointées (escape to heap).

func NewLarge() *Large { l := Large{} return &l }

2. L'original changera-t-il si on passe une structure par valeur à une fonction et qu'on modifie les champs à l'intérieur ?

Non : seule la copie changera, l'original restera le même en dehors de la fonction.

3. Faut-il toujours utiliser des pointeurs pour les structures ?

Non. Pour les petites structures (quelques champs), le passage par valeur est sûr et souvent préférable (immutable/value-semantic), économisant sur les allocations et réduisant la charge sur le GC.

Erreurs typiques et anti-patrons

  • Retourner de grandes structures et leur passage aux fonctions par valeur sans nécessité ;
  • Utilisation injustifiée de pointeurs pour des struct triviales ;
  • Erreurs de mutabilité des données : mise à jour accidentelle de seulement la copie, et non de l'original.

Exemple de la vie réelle

Cas négatif

Dans un service de journalisation, chaque événement représentait une grande structure et était retourné par les fonctions par valeur — chaque changement copiait entièrement la structure.

Avantages :

  • Le code était simple et sûr pour de petites structures.

Inconvénients :

  • Il y a eu une augmentation de la consommation de mémoire, le GC se déclenchait souvent, et le service a commencé à ralentir.

Cas positif

Nous avons opté pour le passage et le retour de structures par pointeur, en modifiant les données via des signatures comme func(l *Large) et func() *Large.

Avantages :

  • Copie minimale, charge réduite sur le GC, traitement plus rapide.

Inconvénients :

  • Il a fallu contrôler la mutabilité, éviter des effets de bord accidentels en travaillant avec un seul objet.