A popularidade de Go explodiu nos últimos anos. A pesquisa HackerEarth Developer de 2020 descobriu que Go era a linguagem de programação mais procurada entre os experientes desenvolvedores e alunos. A Pesquisa do desenvolvedor Stack Overflow de 2021 relatou resultados semelhantes, sendo o Go uma das quatro principais linguagens com as quais os desenvolvedores desejam trabalhar.

Dada sua popularidade, é importante que os desenvolvedores da web dominem o Go, e talvez um dos componentes mais críticos do Go seja seus ponteiros. Este artigo explicará as diferentes maneiras como os ponteiros podem ser criados e os tipos de problemas que os ponteiros corrigem.

O que é Go?

Go é uma linguagem compilada estaticamente feita pelo Google. Existem muitos motivos pelos quais Go é uma escolha tão popular para a construção de software robusto, confiável e eficiente. Um dos maiores atrativos é a abordagem simples e concisa do Go para escrever software, que é aparente na implementação de ponteiros na linguagem.

Passando argumentos no Go

Ao escrever software em qualquer linguagem, os desenvolvedores devem considerar qual código pode sofrer mutação em sua base de código.

Quando você começa a compor funções e métodos e passa todos os diferentes tipos de estruturas de dados em seu código, precisa ter cuidado com o que deve ser passado por valor e o que deve ser passado por referência.

Passar um argumento por valor é como passar uma cópia impressa de algo. Se o titular da cópia rabiscar nela ou destruí-la, a cópia original que você possui permanece inalterada.

Passar por referência é como compartilhar uma cópia original com alguém. Se eles mudarem algo, você pode ver-e ter que lidar com-as mudanças que eles fizeram.

Vamos começar com um pedaço de código realmente básico e veja se você consegue descobrir por que ele não está fazendo o que esperamos.

package main import (“fmt”) func main ( ) {número:=0 add10 (número) fmt.Println (número)//Logs 0} func add10 (número int) {número=número + 10}

No exemplo acima, eu estava tentando fazer o add10 ( ) incremento de função número 10, mas não parece estar funcionando. Ele apenas retorna 0. Este é exatamente o problema que os ponteiros resolvem.

Usando ponteiros no Go

Se quisermos fazer o primeiro trecho de código funcionar, podemos usar ponteiros.

Em Go, cada argumento de função é passado por valor, o que significa que o valor é copiado e passado, e ao alterar o valor do argumento no corpo da função, nada muda com a variável subjacente.

As únicas exceções a esta regra são fatias e mapas. Eles podem ser passados ​​por valor e, como são tipos de referência, qualquer mudança feita no local de onde são passados ​​mudará a variável subjacente.

A maneira de passar argumentos para funções que outras linguagens consideram “por referência” é utilizando ponteiros.

Vamos corrigir nosso primeiro exemplo e explicar o que está acontecendo.

package main import (“fmt”) func main () {number:=0 add10 (& number) fmt.Println (number)//10! Aha! Funcionou! } func add10 (number * int) {* number=* number + 10}

Sintaxe do ponteiro de endereçamento

A única grande diferença entre o primeiro trecho de código e o segundo era o uso de * e &. Esses dois operadores realizam operações conhecidas como desreferenciamento/indireção (*) e referência/recuperação de endereço de memória (&).

Referência e recuperação de endereço de memória usando &

Se você siga o trecho de código da função principal em diante, o primeiro operador que alteramos foi usar um”e”comercial & na frente do argumento de número que passamos para a função add10.

Isso obtém o endereço de memória de onde armazenamos a variável na CPU. Se você adicionar um log ao primeiro fragmento de código , verá um endereço de memória representado por hexadecimal. Ele se parecerá com isto: 0xc000018030 (ele mudará cada vez que você logar).

Esta string ligeiramente críptica essencialmente aponta para um endereço na CPU onde sua variável está armazenada. É assim que Go compartilha a referência de variável, então as mudanças podem ser vistas por todos os outros lugares que têm acesso ao ponteiro ou endereço de memória.

Dereferenciando memória usando *

Se o único o que temos agora é um endereço de memória, adicionar 10 a 0xc000018030 pode não ser exatamente o que precisamos. É aqui que desreferenciar a memória é útil.

Podemos, usando o ponteiro, deferir o endereço da memória na variável para a qual ele aponta e, em seguida, fazer as contas. Podemos ver isso no trecho de código acima na linha 14:

* número=* número + 10

Aqui, estamos desreferenciando nosso endereço de memória para 0 e, em seguida, adicionando 10 a ele.

Agora, o exemplo de código deve funcionar como inicialmente esperado. Compartilhamos uma única variável na qual as mudanças são refletidas, e não pela cópia do valor.

Existem algumas extensões no modelo mental que criamos que serão úteis para entender melhor os indicadores.

Usando ponteiros nil no Go

Tudo no Go recebe um valor 0 quando inicializado pela primeira vez.

Por exemplo, quando você cria uma string, o padrão é uma string vazia (“”) a menos que você atribua algo a ele.

Aqui estão todos os valores zero :

0 para todos os tipos int 0.0 para float32, float64, complex64 e complex128 falso para bool””para string nil para interfaces, fatias, canais, mapas, ponteiros e funções

Isto é o mesmo para ponteiros. Se você criar um ponteiro, mas não apontá-lo para nenhum endereço de memória, ele será nulo.

package main import (“fmt”) func main () {var ponteiro * string fmt.Println (ponteiro)//}

Usando e desreferenciando ponteiros

package main import (“fmt”) func main () {var ageOfSon=10 var levelINGame=& ageOfSon var década=& levelInGame ageOfSon=11 fmt.Println (ageOfSon) fmt.Println (* levelInGame) fmt.Println (** década)}

Você pode ver aqui que estávamos tentando reutilizar a variável ageOfSon em muitos lugares em nosso código, então podemos continuar apontando coisas para outros ponteiros.

Mas na linha 15, temos que cancelar a referência de um ponteiro e, em seguida, cancelar a referência do próximo ponteiro para o qual ele estava apontando.

Isso está utilizando o operador que já conhecemos, *, mas é também encadeando o próximo ponteiro a ser desreferenciado.

Isso pode parecer confuso, mas ajudará se você já viu essa ** sintaxe antes, ao observar outras implementações de ponteiro.

Criando um Ponteiro Go com uma sintaxe de ponteiro alternativa

A maneira mais comum de criar ponteiros é usar a sintaxe que discutimos anteriormente. Mas também há sintaxe alternativa que você pode usar para criar ponteiros usando o novo () função.

Vejamos um exemplo de snippet de código .

package main import (“fmt”) func main () {ponteiro:=new (int)//Isso inicializará o int com seu valor zero de 0 fmt.Println (ponteiro)//Aha! É um ponteiro para: 0xc000018030 fmt.Println (* ponteiro)//Ou, se desreferenciarmos: 0}

A sintaxe é apenas ligeiramente diferente, mas todos os princípios que já discutimos são os mesmos.

Equívocos comuns de ponteiros Go

Para revisar tudo o que aprendemos, existem alguns equívocos frequentemente repetidos ao usar ponteiros que são úteis para discutir.

Uma frase comumente repetida sempre que ponteiros são discutidos é que eles têm mais desempenho, o que, intuitivamente, faz sentido.

Se você passou uma estrutura grande, por exemplo, em várias chamadas de função diferentes, você pode ver como copiar essa estrutura várias vezes nas diferentes funções pode diminuir o desempenho do seu programa.

Mas passar ponteiros no Go é frequentemente mais lento do que passar valores copiados.

Isso ocorre porque quando ponteiros são passados ​​para funções, Go precisa executar um análise de escape para descobrir se o valor precisa ser armazenado na pilha ou no heap.

A passagem de valor permite que todas as variáveis ​​sejam armazenadas na pilha, o que significa que a coleta de lixo pode ser ignorada para essa variável.

Confira este programa de exemplo aqui :

func main () {a:=make ([] * int, 1e9) para i:=0; i 10; i ++ {start:=time.Now () runtime.GC () fmt.Printf (“GC levou% s \ n”, time.Since (start))} runtime.KeepAlive (a)}

Ao alocar um bilhão ponteiros, o coletor de lixo pode demorar mais de meio segundo. Isso é menos do que um nanossegundo por ponteiro. Mas pode somar, especialmente quando os ponteiros são usados ​​intensamente em uma base de código enorme com requisitos intensos de memória.

Se você usar o mesmo código acima sem usar ponteiros, o coletor de lixo pode executar mais de 1.000 vezes mais rápido .

Teste o desempenho de seus casos de uso, já que não existem regras rígidas e rápidas. Lembre-se de que o mantra “Os ponteiros são sempre mais rápidos” não é verdadeiro em todos os cenários.

Conclusão

Espero que este tenha sido um resumo útil. Nele, abordamos o que são os ponteiros Go, as diferentes maneiras como podem ser criados, os problemas que eles resolvem, bem como alguns problemas a serem considerados em seus casos de uso.

Quando aprendi sobre os ponteiros, Eu li uma infinidade de bases de código grandes e bem escritas no GitHub (como Docker por exemplo) para tentar entender quando e quando não usar ponteiros, e encorajo você a fazer o mesmo.

Foi muito útil consolidar meu conhecimento e entender de forma prática as diferentes abordagens que as equipes usam para usar os ponteiros em todo o seu potencial.

Há muitas questões a serem consideradas, como:

O que nossos testes de desempenho indicam? Qual é a convenção geral na base de código mais ampla? Isso faz sentido para este caso de uso específico? É simples de ler e entender o que está acontecendo aqui?

A decisão de quando e como usar os ponteiros é feita caso a caso e espero que agora você tenha um entendimento completo de quando utilizar melhor os ponteiros em seus projetos.