Ponteiros vs. valores em parâmetros e valores de retorno

329

No Go, existem várias maneiras de retornar um structvalor ou fatia dele. Para os individuais que eu já vi:

type MyStruct struct {
    Val int
}

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

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

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

Eu entendo as diferenças entre estes. O primeiro retorna uma cópia da estrutura, o segundo um ponteiro para o valor da estrutura criado na função, o terceiro espera que uma estrutura existente seja passada e substitui o valor.

Já vi todos esses padrões serem usados ​​em vários contextos, estou me perguntando quais são as melhores práticas em relação a eles. Quando você usaria qual? Por exemplo, o primeiro pode ser aprovado para estruturas pequenas (porque a sobrecarga é mínima), o segundo para estruturas maiores. E a terceira, se você deseja ser extremamente eficiente em termos de memória, porque é possível reutilizar facilmente uma única instância de estrutura entre as chamadas. Existem práticas recomendadas para quando usar quais?

Da mesma forma, a mesma pergunta sobre fatias:

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

Novamente: quais são as melhores práticas aqui. Sei que as fatias são sempre ponteiros, portanto, não é útil retornar um ponteiro para uma fatia. No entanto, devo retornar uma fatia dos valores de struct, uma fatia de ponteiros para estruturas, devo passar um ponteiro para uma fatia como argumento (um padrão usado na API do Go App Engine )?

Zef Hemel
fonte
1
Como você diz, isso realmente depende do caso de uso. Todos são válidos dependendo da situação - este é um objeto mutável? queremos uma cópia ou ponteiro? etc. Mas você não mencionou o uso de new(MyStruct):) Mas não há realmente nenhuma diferença entre os diferentes métodos de alocação de ponteiros e retorná-los.
Not_a_Golfer
15
Isso está literalmente acima da engenharia. As estruturas devem ser bem grandes, pois retornar um ponteiro torna seu programa mais rápido. Apenas não se preocupe, código, perfil, corrija se for útil.
Volker
1
Existe apenas uma maneira de retornar um valor ou um ponteiro, e é retornar um valor ou um ponteiro. Como você os aloca é uma questão separada. Use o que funciona para sua situação e escreva um código antes de se preocupar.
JimB
3
Aliás, só por curiosidade, eu disse isso. Retornar estruturas vs. ponteiros parece ter aproximadamente a mesma velocidade, mas passar ponteiros para funções abaixo das linhas é significativamente mais rápido. Embora não esteja em um nível, isso importa
Not_a_Golfer 8/14
1
@ Not_a_Golfer: Eu diria que apenas a alocação bc é feita fora da função. Também a comparação de valores versus ponteiros depende do tamanho dos padrões de acesso à estrutura e à memória após o fato. Copiar itens do tamanho de uma linha de cache é o mais rápido possível e a velocidade dos ponteiros de desreferenciamento do cache da CPU é muito diferente de desreferenciá-los da memória principal.
JimB

Respostas:

392

tl; dr :

  • Métodos usando ponteiros de receptor são comuns; a regra básica para os receptores é : "Em caso de dúvida, use um ponteiro".
  • Fatias, mapas, canais, cadeias, valores de função e valores de interface são implementados com ponteiros internamente, e um ponteiro para eles geralmente é redundante.
  • Em outros lugares, use ponteiros para grandes estruturas ou estruturas que você precisará alterar e, caso contrário, transmita valores , porque fazer com que as coisas sejam alteradas de surpresa por meio de um ponteiro é confuso.

Um caso em que você costuma usar um ponteiro:

  • Os receptores são indicadores com mais frequência do que outros argumentos. Não é incomum que os métodos modifiquem a coisa em que são chamados ou que tipos nomeados sejam estruturas grandes, portanto, a orientação é padronizar os ponteiros, exceto em casos raros.
    • A ferramenta copyfighter de Jeff Hodges procura automaticamente por receptores não minúsculos passados ​​por valor.

Algumas situações em que você não precisa de ponteiros:

  • As diretrizes de revisão de código sugerem a passagem de pequenas estruturas como type Point struct { latitude, longitude float64 }, e talvez até um pouco maiores, como valores, a menos que a função que você está chamando precise modificá-las.

    • A semântica de valores evita situações de aliasing em que uma atribuição aqui altera um valor ali de surpresa.
    • Não é uma opção fácil sacrificar a semântica limpa por um pouco de velocidade, e às vezes passar pequenas estruturas por valor é realmente mais eficiente, porque evita falhas de cache ou alocações de heap.
    • Portanto, a página de comentários de revisão de código do Go Wiki sugere passar valor quando as estruturas são pequenas e provavelmente continuarão assim.
    • Se o ponto de corte "grande" parece vago, é; indiscutivelmente, muitas estruturas estão em um intervalo em que um ponteiro ou um valor está OK. Como limite inferior, os comentários da revisão do código sugerem que fatias (três palavras-máquina) são razoáveis ​​para uso como receptores de valor. Como algo mais próximo de um limite superior, são bytes.Replacenecessários dez palavras 'args (três fatias e um int).
  • Para fatias , você não precisa passar um ponteiro para alterar os elementos da matriz. io.Reader.Read(p []byte)altera os bytes de p, por exemplo. É sem dúvida um caso especial de "tratar pequenas estruturas como valores", já que internamente você está passando por uma pequena estrutura chamada cabeçalho de fatia (consulte a explicação de Russ Cox (rsc) ). Da mesma forma, você não precisa de um ponteiro para modificar um mapa ou se comunicar em um canal .

  • Para fatias, você redimensionará (altere o início / comprimento / capacidade de), funções internas como appendaceitar um valor de fatia e retornar um novo. Eu imitaria isso; evita alias, retornar uma nova fatia ajuda a chamar a atenção para o fato de que uma nova matriz pode ser alocada e é familiar para os chamadores.

    • Nem sempre é prático seguir esse padrão. Algumas ferramentas, como interfaces de banco de dados ou serializadores, precisam ser anexadas a uma fatia cujo tipo não é conhecido no momento da compilação. Às vezes, eles aceitam um ponteiro para uma fatia em um interface{}parâmetro.
  • Mapas, canais, seqüências de caracteres e valores de função e interface , como fatias, são referências ou estruturas internas que já contêm referências; portanto, se você está apenas tentando evitar a cópia dos dados subjacentes, não precisa passar ponteiros para eles . (rsc escreveu um post separado sobre como os valores da interface são armazenados ).

    • Você ainda pode precisar passar ponteiros nos casos mais raros em que deseja modificar a estrutura do chamador: flag.StringVarleva a *stringpor esse motivo, por exemplo.

Onde você usa ponteiros:

  • Considere se sua função deve ser um método em qualquer estrutura para a qual você precise de um ponteiro. As pessoas esperam que muitos métodos xsejam modificados x, portanto, tornar a estrutura modificada o receptor pode ajudar a minimizar a surpresa. Existem diretrizes sobre quando os receptores devem ser indicadores.

  • As funções que têm efeitos em seus parâmetros não receptores devem deixar isso claro no godoc, ou melhor ainda, no godoc e no nome (como reader.WriteTo(writer)).

  • Você menciona aceitar um ponteiro para evitar alocações, permitindo a reutilização; alterar as APIs para reutilizar a memória é uma otimização que eu atrasaria até ficar claro que as alocações têm um custo não trivial e, em seguida, procuraria uma maneira que não force a API mais complicada para todos os usuários:

    1. Para evitar alocações, a análise de fuga da Go é sua amiga. Às vezes, você pode ajudá-lo a evitar alocações de heap criando tipos que podem ser inicializados com um construtor trivial, um literal simples ou um valor zero útil como bytes.Buffer.
    2. Considere um Reset()método para colocar um objeto novamente em um estado em branco, como alguns tipos de stdlib oferecem. Os usuários que não se importam ou não podem salvar uma alocação não precisam chamá-la.
    3. Considere a possibilidade de escrever métodos de modificação no local e funções de criação a partir do zero como pares correspondentes, para maior comodidade: eles existingUser.LoadFromJSON(json []byte) errorpodem ser envolvidos NewUserFromJSON(json []byte) (*User, error). Mais uma vez, ele opta pela escolha entre preguiça e alocação de beliscões para o chamador individual.
    4. Os chamadores que procuram reciclar a memória podem sync.Poollidar com alguns detalhes. Se uma alocação específica cria muita pressão de memória, você tem certeza de que sabe quando a alocação não é mais usada e não possui uma otimização melhor disponível, sync.Poolpode ajudar. (A CloudFlare publicou umasync.Pool publicação útil (pré- ) no blog sobre reciclagem.)

Por fim, se suas fatias devem ter ponteiros: fatias de valores podem ser úteis e poupar alocações e erros de cache. Pode haver bloqueadores:

  • A API para criar seus itens pode forçar ponteiros para você, por exemplo, você precisa chamar, em NewFoo() *Foovez de deixar Go inicializar com o valor zero .
  • A vida útil desejada dos itens pode não ser a mesma. A fatia inteira é liberada de uma só vez; se 99% dos itens não forem mais úteis, mas você tiver ponteiros para o outro 1%, toda a matriz permanecerá alocada.
  • Mover itens pode causar problemas. Notavelmente, appendcopia itens quando aumenta a matriz subjacente . Os indicadores que você obteve antes do appendponto para o lugar errado depois, a cópia pode ser mais lenta para estruturas enormes e, por exemplo, a sync.Mutexcópia não é permitida. Insira / exclua no meio e classifique da mesma forma os itens.

Em termos gerais, as fatias de valor podem fazer sentido se você colocar todos os seus itens no lugar com antecedência e não os mover (por exemplo, não mais appends após a configuração inicial) ou se você continuar movendo-os, mas você tem certeza de que é OK (não / uso cuidadoso de ponteiros para itens, os itens são pequenos o suficiente para copiar com eficiência, etc.). Às vezes você precisa pensar ou medir as especificidades da sua situação, mas esse é um guia aproximado.

twotwotwo
fonte
12
O que significa grandes estruturas? Existe um exemplo de uma estrutura grande e uma estrutura pequena?
O usuário sem chapéu
1
Substitua o valor de 80 bytes de args no amd64?
Tim Wu
2
A assinatura é Replace(s, old, new []byte, n int) []byte; s, old e new são três palavras cada ( cabeçalhos de fatia são(ptr, len, cap) ) e n intsão uma palavra; portanto, 10 palavras, que em oito bytes / palavra são 80 bytes.
twotwotwo
6
Como você define grandes estruturas? Quão grande é grande?
Andy Aldo
3
@AndyAldo Nenhuma das minhas fontes (comentários de revisão de código etc.) define um limite, então decidi dizer que é uma decisão judicial em vez de aumentar o limite. Três palavras (como uma fatia) são bastante consistentemente tratadas como elegíveis para serem um valor no stdlib. Encontrei uma instância de um receptor de valor de cinco palavras agora (text / scanner.Position), mas não leria muito sobre isso (também é passado como um ponteiro!). Se não houver benchmarks, etc., eu faria o que parecer mais conveniente para facilitar a leitura.
twotwotwo
10

Três razões principais para você querer usar receptores de método como ponteiros:

  1. "Primeiro, e mais importante, o método precisa modificar o receptor? Se o fizer, o receptor deve ser um ponteiro."

  2. "A segunda é a consideração da eficiência. Se o receptor é grande, uma grande estrutura, por exemplo, será muito mais barato usar um receptor de ponteiro".

  3. "Em seguida, vem a consistência. Se alguns dos métodos do tipo precisam ter receptores de ponteiro, o restante também deve, portanto, o conjunto de métodos é consistente, independentemente de como o tipo é usado"

Referência: https://golang.org/doc/faq#methods_on_values_or_pointers

Editar: Outra coisa importante é saber o "tipo" real que você está enviando para funcionar. O tipo pode ser um 'tipo de valor' ou 'tipo de referência'.

Mesmo que as fatias e os mapas funcionem como referências, podemos passá-los como ponteiros em cenários como alterar o comprimento da fatia na função.

Santosh Pillai
fonte
1
Para 2, qual é o ponto de corte? Como sei se minha estrutura é grande ou pequena? Além disso, existe uma estrutura pequena o suficiente para que seja mais eficiente usar um valor em vez de ponteiro (para que ele não precise ser referenciado a partir da pilha)?
precisa saber é o seguinte
Eu diria que quanto mais o número de campos e / ou estruturas aninhadas no interior, maior a estrutura. Não tenho certeza se existe um corte específico ou uma maneira padrão de saber quando uma estrutura pode ser chamada de "grande" ou "grande". Se eu estiver usando ou criando uma estrutura, eu saberia se é grande ou pequeno com base no que disse acima. Mas sou só eu!
Santosh Pillai
2

Um caso em que você geralmente precisa retornar um ponteiro é ao construir uma instância de algum recurso com estado ou compartilhável . Isso geralmente é feito por funções prefixadas com New.

Como eles representam uma instância específica de algo e podem precisar coordenar alguma atividade, não faz muito sentido gerar estruturas duplicadas / copiadas representando o mesmo recurso - portanto, o ponteiro retornado atua como o identificador do próprio recurso .

Alguns exemplos:

Em outros casos, os ponteiros são retornados apenas porque a estrutura pode ser muito grande para copiar por padrão:


Como alternativa, o retorno de ponteiros diretamente pode ser evitado retornando uma cópia de uma estrutura que contém o ponteiro internamente, mas talvez isso não seja considerado idiomático:

nobar
fonte
Implícito nesta análise é que, por padrão, as estruturas são copiadas por valor (mas não necessariamente seus membros indiretos).
No4 set19
2

Se você puder (por exemplo, um recurso não compartilhado que não precise ser passado como referência), use um valor. Pelas seguintes razões:

  1. Seu código será mais agradável e legível, evitando operadores de ponteiros e verificações nulas.
  2. Seu código estará mais seguro contra pânico de ponteiro nulo.
  3. Seu código geralmente será mais rápido: sim, mais rápido! Por quê?

Razão 1 : você alocará menos itens na pilha. Alocar / desalocar da pilha é imediato, mas alocar / desalocar no Heap pode ser muito caro (tempo de alocação + coleta de lixo). Você pode ver alguns números básicos aqui: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Razão 2 : especialmente se você armazenar valores retornados em fatias, seus objetos de memória serão mais compactados na memória: repetir uma fatia em que todos os itens são contíguos é muito mais rápido do que repetir uma fatia em que todos os itens são ponteiros para outras partes da memória . Não para a etapa de indireção, mas para o aumento de erros de cache.

Disjuntor de mito : uma linha de cache x86 típica tem 64 bytes. A maioria das estruturas é menor que isso. O tempo de copiar uma linha de cache na memória é semelhante a copiar um ponteiro.

Somente se uma parte crítica do seu código for lenta, eu tentaria alguma micro-otimização e verificaria se o uso de ponteiros melhora um pouco a velocidade, com o custo de menos legibilidade e manutenção.

Mario
fonte