web-dev-qa-db-fra.com

Pointeurs vs valeurs dans les paramètres et valeurs de retour

Dans Go, il existe différentes manières de renvoyer une valeur ou une tranche de struct. Pour ceux que j'ai vus:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

Je comprends les différences entre ceux-ci. Le premier retourne une copie de la structure, le second un pointeur sur la valeur de structure créée dans la fonction, le troisième s'attend à ce qu'une structure existante soit transmise et remplace la valeur.

J'ai vu tous ces modèles être utilisés dans divers contextes. Je me demande quelles sont les meilleures pratiques à cet égard. Quand utiliseriez-vous lequel? Par exemple, le premier pourrait convenir aux petites structures (car le temps système est minime), le second aux plus grandes. Et le troisième si vous voulez être extrêmement efficace en termes de mémoire, car vous pouvez facilement réutiliser une seule instance de structure entre les appels. Existe-t-il des meilleures pratiques pour savoir quand utiliser lesquelles?

De même, la même question concernant les tranches:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

Encore une fois: quelles sont les meilleures pratiques ici. Je sais que les tranches sont toujours des pointeurs. Il n'est donc pas utile de renvoyer un pointeur sur une tranche. Cependant, devrais-je renvoyer une tranche de valeurs de structure, une tranche de pointeurs vers des structs, devrais-je passer un pointeur à une tranche en tant qu'argument (un modèle utilisé dans = API du moteur d'application )?

281
Zef Hemel

tl; dr :

  • Les méthodes utilisant des pointeurs de réception sont courantes. la règle de base pour les destinataires est , "En cas de doute, utilisez un pointeur."
  • Les tranches, les cartes, les canaux, les chaînes, les valeurs de fonction et les valeurs d'interface sont implémentés avec des pointeurs en interne, et un pointeur sur eux est souvent redondant.
  • Ailleurs, utilisez les pointeurs pour les grandes structures ou celles que vous devrez modifier, sinon valeurs de passe , car il est déroutant de modifier un élément par surprise via un pointeur.

Un cas où vous devriez souvent utiliser un pointeur:

  • Les récepteurs sont des pointeurs plus souvent que les autres arguments. Il n'est pas rare que les méthodes modifient ce sur quoi elles sont appelées, ou que les types nommés soient de grandes structures, donc , le guidage == = = = =) == par défaut, sauf dans de rares cas.
    • L'outil copyfighter de Jeff Hodges recherche automatiquement les récepteurs non infimes passés par valeur.

Certaines situations où vous n'avez pas besoin de pointeurs:

  • Les directives de révision de code suggèrent de passer de petites structures comme _type Point struct { latitude, longitude float64 }_, et peut-être même des choses un peu plus grandes, comme valeurs, à moins que la fonction que vous appelez doit pouvoir les modifier sur place.

    • La sémantique des valeurs évite les situations de repliement du spectre où une affectation modifie ici une valeur par surprise.
    • Ce n'est pas Go-y de sacrifier une sémantique propre pour un peu de vitesse, et parfois passer de petites structures par valeur est en réalité plus efficace, car cela évite cache misses ou allocations de tas.
    • Donc, Go Wiki's commentaires de révision de code la page suggère de passer par valeur lorsque les structures sont petites et susceptibles de le rester.
    • Si la "grande" coupure semble vague, c'est bien le cas. on peut soutenir que de nombreuses structures sont dans une plage où un pointeur ou une valeur est OK. Comme limite inférieure, les commentaires de révision de code suggèrent que les tranches (trois mots machine) sont raisonnables à utiliser comme récepteurs de valeur. En tant que quelque chose de plus proche d'une limite supérieure, _bytes.Replace_ prend 10 mots d'arguments (trois tranches et un int).
  • Pour les tranches , il n'est pas nécessaire de passer un pointeur pour modifier les éléments du tableau. io.Reader.Read(p []byte) modifie les octets de p, par exemple. C’est un cas particulier de "traiter les petites structures comme des valeurs", puisque vous transmettez en interne une petite structure appelée un en-tête de tranche (voir ) Russ Cox (rsc ) l'explication de ). De même, vous n'avez pas besoin d'un pointeur pour modifier une carte ou communiquer sur un canal .

  • Pour les tranches , vous allez redéfinir (modifier le début/la longueur/la capacité de), des fonctions intégrées telles que append acceptent une valeur de tranche et retourner un nouveau. J'imiterais ça; Pour éviter les alias, le renvoi d'une nouvelle tranche permet d'attirer l'attention sur le fait qu'un nouveau tableau peut être alloué et qu'il est familier pour les appelants.

    • Ce n'est pas toujours pratique de suivre ce modèle. Certains outils tels que interfaces de base de données ou serializers doivent être ajoutés à une tranche dont le type n'est pas connu au moment de la compilation. Ils acceptent parfois un pointeur sur une tranche dans un paramètre _interface{}_.
  • Les cartes, canaux, chaînes, ainsi que les valeurs de fonction et d'interface , comme les tranches, sont des références internes ou des structures contenant déjà des références. Si vous essayez simplement pour éviter de copier les données sous-jacentes, vous n'avez pas besoin de leur renvoyer des pointeurs. (rsc a écrit un article séparé sur la manière dont les valeurs d'interface sont stockées ).

    • Vous pouvez toujours avoir besoin de passer des pointeurs dans le cas le plus rare où vous souhaitez modifier la structure de l'appelant: flag.StringVar prend un _*string_ pour cette raison, par exemple.

Où vous utilisez des pointeurs:

  • Déterminez si votre fonction doit être une méthode sur la structure pour laquelle vous avez besoin d’un pointeur. Les gens attendent beaucoup de méthodes sur x pour modifier x, aussi, rendre la structure modifiée destinée au récepteur peut aider à minimiser les surprises. Il y a guidelines lorsque les destinataires doivent être des pointeurs.

  • Les fonctions qui ont des effets sur leurs paramètres non receveurs devraient le préciser dans le godoc, ou mieux encore, le godoc et le nom (comme reader.WriteTo(writer)).

  • Vous mentionnez accepter un pointeur pour éviter les allocations en permettant la réutilisation; changer les API dans un souci de réutilisation de la mémoire est une optimisation. Il faudrait attendre qu'il soit clair que les allocations ont un coût non négligeable, puis je chercherais un moyen qui ne force pas les API les plus délicates sur tous les utilisateurs:

    1. Pour éviter les allocations, Go analyse d'analyse est votre ami. Vous pouvez parfois l'aider à éviter les allocations de tas en faisant des types pouvant être initialisés avec un constructeur trivial, un littéral simple ou une valeur zéro utile telle que bytes.Buffer .
    2. Considérez une méthode Reset() pour remettre un objet dans un état vide, comme le proposent certains types stdlib. Les utilisateurs qui ne se soucient pas ou ne peuvent pas sauvegarder une allocation ne doivent pas l'appeler.
    3. Envisagez d’écrire des méthodes de modification sur place et des fonctions de création à partir de zéro sous forme de paires, pour plus de commodité: existingUser.LoadFromJSON(json []byte) error pourrait être encapsulé par NewUserFromJSON(json []byte) (*User, error). Encore une fois, cela force le choix entre la paresse et des attributions précises à chaque appelant.
    4. Les appelants cherchant à recycler de la mémoire peuvent laisser sync.Pool gérer certains détails. Si une allocation particulière crée une pression mémoire importante, vous êtes certain de savoir quand l'allocation n'est plus utilisée et vous ne disposez pas d'une meilleure optimisation disponible. _sync.Pool_ peut vous aider. (CloudFlare published un article de blog utile (pre -_sync.Pool_) sur le recyclage.)
    5. Curieusement, pour les constructeurs compliqués, new(Foo).Reset() peut parfois éviter une allocation alors que NewFoo() ne le ferait pas. Pas idiomatique; attention à essayer celui-là à la maison.

Enfin, indiquez si vos tranches doivent être des pointeurs: les tranches de valeurs peuvent être utiles et vous permettent d’économiser les allocations et les erreurs de cache. Il peut y avoir des bloqueurs:

  • L'API pour créer vos éléments peut vous forcer à vous diriger, par exemple. vous devez appeler NewFoo() *Foo plutôt que de laisser Go initialiser avec valeur zéro .
  • La durée de vie souhaitée des éléments peut ne pas être la même. La tranche entière est libérée à la fois; si 99% des éléments ne sont plus utiles mais que vous avez des pointeurs sur l'autre 1%, tout le tableau reste alloué.
  • Le déplacement d'éléments peut vous causer des problèmes. Notamment, append copie les éléments quand il augmente le tableau sous-jacent . Les pointeurs que vous avez obtenus avant le append pointent au mauvais endroit après, la copie peut être plus lente pour les énormes structures, et par exemple pour. _sync.Mutex_ la copie n'est pas autorisée. Insérer/supprimer au milieu et effectuer un tri similaire déplace les éléments.

Globalement, les tranches de valeur peuvent avoir un sens si vous mettez tous vos éléments en place à l’avance et ne les déplacez pas (par exemple, pas plus de appends après la configuration initiale), ou si vous continuez à les déplacer, mais vous vous êtes sûr que tout va bien (pas d'utilisation judicieuse des pointeurs sur les éléments, les éléments sont suffisamment petits pour pouvoir être copiés efficacement, etc.). Parfois, vous devez réfléchir ou mesurer les détails de votre situation, mais c'est un guide approximatif.

341
twotwotwo

Trois raisons principales pour lesquelles vous souhaitez utiliser les récepteurs de méthodes comme pointeurs:

  1. "En premier lieu, et le plus important, la méthode doit-elle modifier le récepteur? Si c'est le cas, le récepteur doit être un pointeur."

  2. "Deuxièmement, il faut tenir compte de l'efficacité. Si le récepteur est grand, une grosse structure par exemple, il sera beaucoup moins coûteux d'utiliser un récepteur pointeur."

  3. "Suivant est la cohérence. Si certaines méthodes du type doivent avoir des récepteurs de pointeur, le reste le devrait aussi, de sorte que le jeu de méthodes est cohérent quelle que soit la façon dont le type est utilisé"

Référence: https://golang.org/doc/faq#methods_on_values_or_pointers

Edit: Une autre chose importante est de connaître le "type" réel que vous envoyez pour fonctionner. Le type peut être un "type de valeur" ou un "type de référence".

Même si les tranches et les cartes servent de références, il peut être utile de les transmettre en tant que pointeurs dans des scénarios tels que la modification de la longueur de la tranche dans la fonction.

10
Santosh Pillai

En règle générale, vous devez renvoyer un pointeur lors de la construction d'une instance d'une ressource avec état ou pouvant être partagée . Cela se fait souvent par des fonctions précédées de New .

Parce qu'ils représentent une instance spécifique de quelque chose et qu'ils peuvent avoir besoin de coordonner une activité, il n'est pas très logique de générer des structures dupliquées/copiées représentant la même ressource - le pointeur renvoyé sert donc de descripteur à la ressource elle-même. .

Quelques exemples:

Dans d'autres cas, les pointeurs sont renvoyés simplement parce que la structure peut être trop grande pour être copiée par défaut:


Vous pouvez également éviter de renvoyer des pointeurs directement en renvoyant une copie d'une structure contenant le pointeur en interne, mais ceci n'est peut-être pas considéré comme idiomatique:

0
nobar