Puntatori contro valori nei parametri e valori di ritorno

In Go ci sono vari modi per restituire un valore di struct o una sua porzione. Per quelli individuali che ho visto:

 type MyStruct struct { Val int } func myfunc() MyStruct { return MyStruct{Val: 1} } func myfunc() *MyStruct { return &MyStruct{} } func myfunc(s *MyStruct) { s.Val = 1 } 

Capisco le differenze tra questi. Il primo restituisce una copia della struct, il secondo un puntatore al valore di struct creato all’interno della funzione, il terzo si aspetta una struttura esistente da passare e sostituisce il valore.

Ho visto tutti questi modelli essere utilizzati in vari contesti, mi chiedo quali sono le migliori pratiche per quanto riguarda questi. Quando useresti quale? Ad esempio, il primo potrebbe essere ok per le piccole strutture (perché il sovraccarico è minimo), il secondo per quelle più grandi. E il terzo se vuoi essere estremamente efficiente in termini di memoria, perché puoi facilmente riutilizzare una singola istanza di struttura tra le chiamate. Ci sono buone pratiche per quando usare quale?

Allo stesso modo, la stessa domanda riguardante le fette:

 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} } } 

Ancora: quali sono le migliori pratiche qui. So che le slice sono sempre dei puntatori, quindi non è utile restituire un puntatore a una porzione. Tuttavia, dovrei restituire una porzione di valori struct, una porzione di puntatori alle struct, dovrei passare un puntatore a una slice come argomento (un pattern utilizzato nell’API Go App Engine )?

tl; dr :

  • I metodi che usano i puntatori del ricevitore sono comuni; la regola generale per i ricevitori è , “In caso di dubbio, utilizzare un puntatore.”
  • Fette, mappe, canali, stringhe, valori di funzione e valori di interfaccia sono implementati internamente con puntatori e un puntatore ad essi è spesso ridondante.
  • Altrove, usa i puntatori per le grandi strutture o le strutture che dovrai modificare e, in caso contrario, passare i valori , perché confondere le cose cambiate di sorpresa tramite un puntatore.

Un caso in cui dovresti usare spesso un puntatore:

  • I ricevitori sono puntatori più spesso di altri argomenti. Non è insolito che i metodi modifichino la cosa su cui sono chiamati, o che i tipi denominati siano strutture di grandi dimensioni, quindi la guida è di default ai puntatori tranne in rari casi.
    • Lo strumento Copyfighter di Jeff Hodges cerca automaticamente ricevitori non piccoli passati per valore.

Alcune situazioni in cui non è necessario puntatori:

  • Le linee guida per la revisione del codice suggeriscono il passaggio di piccole strutture come type Point struct { latitude, longitude float64 } e forse anche cose un po ‘più grandi, come valori, a meno che la funzione chiamata non sia in grado di modificarle sul posto.

    • La semantica del valore evita situazioni di aliasing in cui un incarico qui cambia di sorpresa un valore laggiù.
    • Non è Go-y sacrificare la semantica pulita per un po ‘di velocità, e a volte passare piccole strutture per valore è in realtà più efficiente, perché evita errori di cache o allocazioni di heap.
    • Quindi, la pagina dei commenti di revisione del codice di Go Wiki suggerisce di passare per valore quando le strutture sono piccole e probabilmente rimarranno tali.
    • Se il “grande” taglio sembra vago, lo è; probabilmente molte strutture sono in un intervallo in cui un puntatore o un valore è OK. Come limite inferiore, i commenti di revisione del codice suggeriscono fette (tre parole macchina) sono ragionevoli da utilizzare come destinatari del valore. Come qualcosa di più vicino a un limite superiore, bytes.Replace richiede 10 parole di args (tre sezioni e un int ).
  • Per le sezioni , non è necessario passare un puntatore per modificare gli elementi dell’array. io.Reader.Read(p []byte) cambia i byte di p , ad esempio. È discutibilmente un caso speciale di “trattare piccole strutture come valori”, visto che internamente state passando attorno a una piccola struttura chiamata intestazione di una sezione (vedere la spiegazione di Russ Cox (rsc) ). Allo stesso modo, non è necessario un puntatore per modificare una mappa o comunicare su un canale .

  • Per le sezioni ti ricolleghi (modifica l’inizio / la lunghezza / la capacità di), le funzioni integrate come append accettano un valore di slice e ne restituiscono uno nuovo. Lo imiterei; evita l’aliasing, la restituzione di una nuova slice aiuta a richiamare l’attenzione sul fatto che un nuovo array potrebbe essere allocato ed è familiare ai chiamanti.

    • Non è sempre pratico seguire questo schema. Alcuni strumenti come le interfacce di database o i serializzatori devono essere aggiunti a una sezione il cui tipo non è noto al momento della compilazione. A volte accettano un puntatore a una sezione in un parametro di interface{} .
  • Mappe, canali, stringhe e valori di interfaccia e funzione , come le slice, sono riferimenti interni o strutture che contengono già riferimenti, quindi se stai solo cercando di evitare di copiare i dati sottostanti, non devi passare loro i puntatori . (rsc ha scritto un post separato su come sono memorizzati i valori dell’interfaccia ).

    • Potrebbe ancora essere necessario passare i puntatori nel caso più raro che si desideri modificare la struttura del chiamante: flag.StringVar accetta una *string per tale motivo, ad esempio.

Dove usi i puntatori:

  • Considera se la tua funzione dovrebbe essere un metodo su qualunque struttura hai bisogno di un puntatore. La gente si aspetta un sacco di metodi su x per modificare x , quindi rendendo la struttura modificata il ricevitore può aiutare a minimizzare la sorpresa. Ci sono linee guida su quando i ricevitori dovrebbero essere dei puntatori.

  • Le funzioni che hanno effetti sui loro parametri non-ricevitore dovrebbero chiarire ciò nel Godoc, o meglio ancora, nel Godoc e nel nome (come reader.WriteTo(writer) ).

  • Si menziona l’accettazione di un puntatore per evitare allocazioni consentendo il riutilizzo; cambiare le API per il riutilizzo della memoria è un’ottimizzazione che ritarderei fino a quando non sarà chiaro che le allocazioni hanno un costo non banale, e quindi cercherò un modo che non imponga l’API più complicata a tutti gli utenti:

    1. Per evitare allocazioni, l’ analisi di fuga di Go è tua amica. A volte è ansible evitare l’allocazione dell’heap creando tipi che possono essere inizializzati con un costruttore banale, un letterale semplice o un valore zero utile come bytes.Buffer .
    2. Considera un metodo Reset() per rimettere un object in uno stato vuoto, come offrono alcuni tipi di stdlib. Gli utenti che non si preoccupano o non possono salvare un’allocazione non devono chiamarlo.
    3. Considerare la possibilità di scrivere metodi modificabili sul posto e funzioni create-from-scratch come coppie corrispondenti, per comodità: l’ existingUser.LoadFromJSON(json []byte) error potrebbe essere NewUserFromJSON(json []byte) (*User, error) da NewUserFromJSON(json []byte) (*User, error) . Ancora una volta, spinge la scelta tra la pigrizia e l’assegnazione di pizzicotti al singolo chiamante.
    4. I chiamanti che cercano di riciclare la memoria possono lasciare che sync.Pool gestisca alcuni dettagli. Se una particolare allocazione crea molta pressione sulla memoria, sei sicuro di sapere quando l’allocazione non è più utilizzata e non hai una migliore ottimizzazione disponibile, sync.Pool può aiutarti. (CloudFlare ha pubblicato un post sul blog utile (pre- sync.Pool ) sul riciclaggio.)
    5. Curiosamente, per i costruttori complicati, new(Foo).Reset() volte può evitare un’allocazione quando NewFoo() non lo fa. Non idiomatico; attento a provare quello a casa.

Infine, se le sezioni dovrebbero essere di puntatori: fette di valori possono essere utili e salvare le allocazioni e le carenze della cache. Ci possono essere bloccanti:

  • L’API per creare i tuoi oggetti potrebbe NewFoo() *Foo puntare su di te, ad esempio devi chiamare NewFoo() *Foo piuttosto che lasciar inizializzare con il valore zero .
  • Le durate desiderate degli articoli potrebbero non essere tutte uguali. L’intera fetta viene liberata contemporaneamente; se il 99% degli elementi non è più utile ma si hanno puntatori all’altro 1%, tutto l’array rimane allocato.
  • Spostare oggetti potrebbe causare problemi. In particolare, append copie degli elementi quando aumenta la matrice sottostante . Puntatori che hai prima che l’ append punti nel posto sbagliato dopo, la copia può essere più lenta per le enormi strutture e, ad esempio, per la sync.Mutex copia di sync.Mutex non è consentita. Inserisci / elimina al centro e l’ordinamento sposta gli oggetti in modo simile.

In generale, le fette di valore possono avere senso se prendi tutti i tuoi oggetti in primo piano e non li sposti (ad esempio, non append più append dopo la configurazione iniziale), o se continui a spostarli ma sei sicuro va bene (no / uso attento dei puntatori agli articoli, gli articoli sono abbastanza piccoli da poter essere copiati efficientemente, ecc.). A volte devi pensare o misurare le specifiche della tua situazione, ma questa è una guida approssimativa.

Tre ragioni principali per cui si desidera utilizzare i ricevitori del metodo come puntatori:

  1. “In primo luogo e, cosa più importante, il metodo deve modificare il ricevitore? Se lo fa, il ricevitore deve essere un puntatore.”

  2. “La seconda è la considerazione dell’efficienza: se il ricevitore è grande, una grande struttura, ad esempio, sarà molto più economico usare un ricevitore puntatore.”

  3. “Il prossimo è la consistenza: se alcuni dei metodi del tipo devono avere ricevitori di puntatori, anche il resto dovrebbe esserlo, quindi il set di metodi è coerente indipendentemente da come viene usato il tipo”

Riferimento: https://golang.org/doc/faq#methods_on_values_or_pointers

Modifica: Un’altra cosa importante è conoscere il “tipo” che stai inviando a funzionare. Il tipo può essere un ‘tipo valore’ o ‘tipo riferimento’. Vedi la figura seguente:

inserisci la descrizione dell'immagine qui

Anche se le sezioni e le mappe fungono da riferimenti, potremmo volerli passare come indicatori in scenari come modificare la lunghezza della sezione nella funzione.