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 )?
tl; dr :
Un cas où vous devriez souvent utiliser un pointeur:
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.
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.
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 ).
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:
bytes.Buffer
.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.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.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.)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:
NewFoo() *Foo
plutôt que de laisser Go initialiser avec valeur zéro .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 append
s 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.
Trois raisons principales pour lesquelles vous souhaitez utiliser les récepteurs de méthodes comme pointeurs:
"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."
"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."
"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.
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:
func NewTLSServer(handler http.Handler) *Server
- Instancier un serveur Web pour le testerfunc Open(name string) (*File, error)
- retourne un descripteur d'accès au fichierDans d'autres cas, les pointeurs sont renvoyés simplement parce que la structure peut être trop grande pour être copiée par défaut:
func NewRGBA(r Rectangle) *RGBA
- alloue une image en mémoireVous 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: