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

Em Go, existem várias maneiras de retornar um valor struct ou uma fatia dele. Para os indivíduos que eu 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 dentro da function, o terceiro espera que uma estrutura existente seja passada e substitui o valor.

Eu vi todos esses padrões serem usados ​​em vários contextos, eu estou querendo saber quais são as melhores práticas em relação a estes. Quando você usaria qual? Por exemplo, o primeiro pode ser ok para pequenas estruturas (porque a sobrecarga é mínima), a segunda para as maiores. E o terceiro, se você quiser ser extremamente eficiente em termos de memory, porque é possível reutilizar facilmente uma única instância de estrutura entre as chamadas. Há alguma prática recomendada para quando usar qual?

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. Eu sei que as fatias são sempre pointers, então retornar um ponteiro para uma fatia não é útil. No entanto, devo retornar uma fatia de valores de estrutura, uma fatia de pointers para structs, devo passar um ponteiro para uma fatia como argumento (um padrão usado na API do Go App Engine )?

tl; dr :

  • Métodos usando pointers de receptor são comuns; A regra básica para os receptores é : “Em caso de dúvida, use um ponteiro”.
  • Fatias, mapas, canais, cadeias de caracteres, valores de function e valores de interface são implementados com pointers internamente, e um ponteiro para eles é geralmente redundante.
  • Em outros lugares, use pointers para grandes estruturas ou estruturas que você terá que alterar e, caso contrário, passar valores , porque confundir as coisas com surpresa por meio de um ponteiro.

Um caso em que você costuma usar um ponteiro:

  • Receptores são pointers com mais freqüência do que outros argumentos. Não é incomum que os methods modifiquem a coisa para a qual são chamados ou que os tipos nomeados sejam grandes estruturas, portanto, a orientação é padronizar os pointers, exceto em casos raros.
    • A ferramenta de 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 pointers:

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

    • Semântica de valor evita situações de alias onde uma atribuição aqui muda um valor por lá de surpresa.
    • Não é o Go-y 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 a passagem de valor quando as estruturas são pequenas e provavelmente permanecerão assim.
    • Se o corte “grande” parece vago, é; sem dúvida, muitas estruturas estão em um intervalo em que um ponteiro ou um valor é OK. Como limite inferior, os comentários de revisão de código sugerem que fatias (três palavras de máquina) são razoáveis ​​para usar como receptores de valor. Como algo mais próximo de um limite superior, o bytes.Replace recebe 10 palavras de argumentos (três fatias e um int ).
  • Para fatias , você não precisa passar um ponteiro para alterar elementos da matriz. io.Reader.Read(p []byte) altera os bytes de p , por exemplo. É discutivelmente um caso especial de “tratar pequenas estruturas como valores”, já que internamente você está passando por uma pequena estrutura chamada header de fatia (veja 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ê vai usar novamente (alterar o início / comprimento / capacidade de), funções embutidas como append aceitar um valor de fatia e retornar um novo. Eu imitaria isso; evita o aliasing, o retorno de 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 database ou serializadores precisam append a uma fatia cujo tipo não é conhecido em tempo de compilation. Às vezes, eles aceitam um ponteiro para uma fatia em um parâmetro de interface{} .
  • Mapas, canais, strings e valores de function e interface , como fatias, são internamente referências ou estruturas que contêm referências já, portanto, se você está apenas tentando evitar a cópia dos dados subjacentes, não é necessário passar pointers para eles. . (O rsc escreveu um post separado sobre como os valores da interface são armazenados ).

    • Você ainda pode precisar passar pointers no caso mais raro que você deseja modificar o struct do chamador: flag.StringVar leva uma *string por esse motivo, por exemplo.

Onde você usa pointers:

  • Considere se sua function deve ser um método em qualquer estrutura para a qual você precise de um ponteiro. As pessoas esperam muitos methods em x para modificar x , então fazer a estrutura modificada do receptor pode ajudar a minimizar a surpresa. Existem orientações sobre quando os receptores devem ser pointers.

  • Funções que possuem 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 em prol da reutilização de memory é uma otimização que eu demoraria até que fique 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 em todos os usuários:

    1. Para evitar alocações, a análise de fuga de Go é sua amiga. Às vezes, você pode ajudar 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 o bytes.Buffer .
    2. Considere um método Reset() para colocar um object de volta 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 methods modify-in-place e funções create-from-scratch como pares correspondentes, por conveniência: o existingUser.LoadFromJSON(json []byte) error pode ser quebrado pelo NewUserFromJSON(json []byte) (*User, error) . Mais uma vez, ele empurra a escolha entre preguiça e beliscar alocações para o chamador individual.
    4. Os chamadores que buscam reciclar a memory podem deixar o sync.Pool lidar com alguns detalhes. Se uma alocação específica cria muita pressão na memory, você tem certeza de que sabe quando a alocação não é mais usada e não tem uma otimização melhor disponível, sync.Pool pode ajudar. (CloudFlare publicou um post útil (pre- sync.Pool ) sobre recyclerview.)
    5. Curiosamente, para construtores complicados, new(Foo).Reset() podem, às vezes, evitar uma alocação quando NewFoo() não o faria. Não é idiomático; Cuidado tentando aquele em casa.

Finalmente, se suas fatias devem ser de pointers: fatias de valores podem ser úteis e salvar alocações e erros de cache. Pode haver bloqueadores:

  • A API para criar seus itens pode forçar pointers em você, por exemplo, você precisa chamar NewFoo() *Foo vez de deixar o Go inicializar com o valor zero .
  • Os tempos de vida desejados dos itens podem não ser todos iguais. A fatia inteira é liberada imediatamente; Se 99% dos itens não forem mais úteis, mas você tiver pointers para o outro 1%, todo o array permanecerá alocado.
  • Movendo itens ao redor pode causar problemas. Notavelmente, append itens de cópias quando cresce o array subjacente . Os pointers que você obteve antes do append apontam para o local errado depois, a cópia pode ser mais lenta para estruturas enormes e, por exemplo, para sync.Mutex cópia do sync.Mutex não é permitida. Inserir / excluir no meio e classificar de forma semelhante mover itens ao redor.

Em termos gerais, fatias de valor podem fazer sentido se você colocar todos os itens na posição inicial e não movê-los (por exemplo, não append mais após a configuração inicial), ou se continuar movendo-os, mas tem certeza tudo bem (não / uso cuidadoso de pointers para itens, itens são pequenos o suficiente para copiar eficientemente, etc.). Às vezes você tem que pensar ou medir os detalhes da sua situação, mas esse é um guia aproximado.