Criar novos tipos de dados é uma parte importante do trabalho de todo programador. Na maioria dos idiomas, uma definição de tipo consiste em uma descrição de seus campos e métodos. No Golang, além disso, você precisa decidir qual semântica de destinatário para os métodos do novo tipo usar: valor (valor) ou ponteiro (ponteiro). À primeira vista, essa decisão pode parecer secundária, porque na maioria dos casos o programa funcionará com qualquer semântica do destinatário. Portanto, muitas pessoas pulam esse ponto e escrevem código, e ainda não o descobriram, o que é afetado pela semântica do destinatário do método. E para descobrir, você precisa se aprofundar um pouco mais no funcionamento do Golang.
Considere um pequeno exemplo. Defina uma estrutura de
gato com um campo
Nome e o método
sayHello (string de pessoa) . A seguir,
por um método , chamarei uma função associada a um tipo específico, um
objeto uma variável que possui métodos e o
destinatário do método será a variável indicada entre parênteses após a palavra
func na descrição do método.
type cat struct { Name string } func (c *cat) sayHello(person string) { fmt.Println(fmt.Sprintf("Meow, meow, %s!", person) }
Se definirmos um ponteiro para
cat e solicitarmos o campo
Name , obviamente obteremos um erro, pois o campo é chamado de
nil :
var c *cat
https://play.golang.org/p/L3FnRJXKqs0No entanto, quando o método
sayHello () for chamado na mesma variável, não haverá erro:
var c *cat
https://play.golang.org/p/EMoFgKL1HEiPor que
nada pode chamar um método neste exemplo, e como isso é explicado em termos da arquitetura da própria linguagem? Isso se torna possível porque o método no Go é açúcar sintático ou, em outras palavras, um invólucro em torno de uma função que possui um dos argumentos do destinatário. Quando o
método c.sayHello ("Humano") é chamado, a construção
(* cat) .sayHello (c, s) (
https://play.golang.org/p/X9leJeIvxcA ) será realmente chamada. Ao chamar o método
nil do exemplo acima, praticamente chamamos a função
nil nos argumentos, e isso já é uma situação bastante normal. Portanto, no Go
nil, é o destinatário correto dos métodos.
Como o receptor do método é realmente um argumento, as recomendações para o uso da semântica "valor" ou "ponteiro" para o receptor do método são semelhantes às recomendações para argumentos de função. Eles, por sua vez, são inferidos da regra básica de Go:
argumentos sempre são passados para a função por valor . Isso significa que a transferência de qualquer argumento para a função ocorre através da cópia: se a função aceitar uma estrutura como entrada, uma cópia completa dessa estrutura entrará dentro dela; se levar um ponteiro para um objeto, uma nova variável virá com um ponteiro para o mesmo objeto. Isso pode ser visto comparando o endereço da variável antes de passá-lo para a função com o endereço do argumento dentro da função (
https://play.golang.org/p/oc2ssC_Irs8 ,
https://play.golang.org/p/FeQa2HUdX0a ).
Quando a passagem de link é usada:
- Para grandes estruturas. O ponteiro ocupa apenas uma palavra da máquina (32, 64 bits, dependendo do sistema). Portanto, ao chamar um método com um ponteiro no receptor, copiar o ponteiro é mais barato do que copiar o objeto inteiro, como seria o caso passando por valor.
- Se o método chamado modifica os dados do próprio objeto. Quando o destinatário é transferido por referência, o método pode afetar o estado do objeto de chamada, fazendo alterações indiretamente. O que é impossível ao passar por valor.
Ao usar a transferência de valor:
- Para tipos internos simples, como números, strings, bool. Ao usar o ponteiro, quase a mesma quantidade de memória é usada como o objeto desse tipo, e o custo de sua manutenção pelo coletor de lixo aumenta, como será descrito abaixo.
- Para fatias, bem como outros tipos de referência: mapa e canais - não faz sentido usar um ponteiro. Eles próprios já são um ponteiro.
- Com o multiencadeamento, a passagem por valor é segura, diferente da passagem por referência.
- Para pequenas estruturas. Nesses casos, a transmissão por valor é mais eficiente. Isso ocorre porque os dados internos dos métodos são colocados em um quadro separado da pilha. Depois de sair de uma função, seu quadro é limpo. Quando vasculhamos algo ao longo do ponteiro, transferimos esses dados da pilha para a pilha, de onde esses dados podem estar disponíveis para outras funções. Aumentar a pilha cria uma carga adicional para o coletor de lixo, cuja operação reduz a velocidade do programa em uma média de 25%. Ao usar a transferência valor por valor, os dados permanecem na pilha e nenhum trabalho adicional do coletor de lixo é necessário.
Quando você precisa pensar na semântica do destinatário:
- O tipo de destinatário pode variar de acordo com a área de assunto. Em um de seus discursos, Bill Kennedy deu um bom exemplo com o tipo de usuário que descreve o usuário. Quando passado por valor, uma cópia será criada para o usuário. Isso levará ao fato de que várias cópias do mesmo usuário podem coexistir no programa ao mesmo tempo, que podem ser alteradas independentemente, o que não corresponde à área de assunto, porque o usuário real é sempre um e não pode ser descrito em momentos diferentes por conjuntos diferentes dados.
- Outra maneira segura de determinar o tipo de destinatário para um método é usar o método construtor para seu tipo. Se o construtor retornar um valor / ponteiro, ao criar uma entidade, presume-se que eles continuarão a trabalhar com ele como um valor / ponteiro. Portanto, também é melhor usar a mesma semântica no receptor do método.
- Existe uma regra não escrita, que viola o compilador, mas seu código definitivamente não vai melhorar com isso. Se um dos métodos de tipo usar um ponteiro / valor como receptor, para manter a consistência, os métodos restantes deverão usar um ponteiro / valor. Os métodos de tipo não devem ter um hash de receptores de valor e ponteiro.
Qual é o resultado
No Go Value, semântica significa copiar um valor; semântica de ponteiro significa dar acesso a um valor. Isso se aplica aos argumentos dos métodos e a seus destinatários. Para tipos internos, como números, linhas, fatias, mapas, canais e estruturas pequenas, você quase sempre precisa usar a transferência baseada em valor. Para estruturas que ocupam uma grande quantidade de memória e estruturas cujo estado pode ser indiretamente alterado por seus métodos, você deve usar a transferência por referência. Além disso, a semântica do destinatário pode depender da área de assunto descrita pelo tipo, da semântica retornada em sua fábrica e da semântica do destinatário já usada em outros métodos desse tipo.