Entendendo ponteiros em Go

Introdução

Quando você criar software em Go, você estará escrevendo funções e métodos. Você passa dados para essas funções como argumentos. Às vezes, a função precisa de uma cópia local dos dados e você quer que o original permaneça inalterado. Por exemplo, se você for um banco e tiver uma função que mostra ao usuário as alterações de seu saldo – dependendo do tipo de conta (poupança/investimentos) que o usuário escolha. Neste caso, você não vai querer alterar o saldo real do cliente antes que ele escolha o tipo de conta/plano de investimentos, mas apenas usar a informação em cálculos. Esse parâmetro é chamado de passagem por valor porque você está enviando o valor da variável para a função, mas não a variável em si.

Outras vezes, você pode querer que a função seja capaz de alterar os dados na variável original. Por exemplo, quando o cliente bancário faz um depósito em sua conta, você quer que a função de depósito possa acessar o saldo real, não uma cópia. Nesse caso, você não precisa enviar os dados reais para a função; precisa apenas dizer à função onde os dados estão localizados na memória. Um tipo de dados chamado ponteiro retém o endereço da memória dos dados, mas não os dados em si. O endereço da memória diz à função onde encontrar os dados, mas não o valor dos dados. Você pode passar o ponteiro para a função em vez dos dados e a função poderá, então, alterar a variável original em seu lugar original. Isso é chamado de passagem por referência, porque o valor da variável não é passado para a função, apenas sua localização.

Neste artigo, você criará e usará ponteiros para compartilhar o acesso ao espaço de memória de uma variável.

Definindo e usando ponteiros

Quando você usa um ponteiro para uma variável, há alguns elementos de sintaxe diferentes que você precisa entender. O primeiro é o uso do “E comercial” (&). Se colocar um e comercial na frente de um nome de variável, você está declarando que quer obter o endereço, ou um ponteiro para aquela variável. O segundo elemento de sintaxe é o uso do asterisco (*) ou de operador de desreferenciação. Quando declara uma variável de ponteiro, você segue o nome da variável para a qual o ponteiro aponta, prefixado com um *, desta forma:

var myPointer *int32 = &someint 

Isso cria o myPointer como um ponteiro para uma variável int32 e inicializa o ponteiro com o endereço de someint. O ponteiro não contém de fato um int32, apenas o endereço de um.

Vejamos um ponteiro para uma string. O código a seguir declara tanto um valor de uma string quanto um ponteiro para uma string:

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer) }  

Execute o programa com o seguinte comando:

  • go run main.go

Quando você executar o programa, ele imprimirá o valor da variável, além do endereço onde a variável está armazenada (o endereço do ponteiro). O endereço de memória é um número hexadecimal e não se destina para ficar legível para humanos. Na prática, você provavelmente nunca verá um endereço de memória como resultado. Estamos mostrando a você somente a título de ilustração. Como cada programa é criado em seu próprio espaço de memória quando é executado, o valor do ponteiro será diferente cada vez que você o executar e será diferente do resultado mostrado aqui:

Outputcreature = shark pointer = 0xc0000721e0 

A primeira variável que definimos chamamos de creature e a definimos como sendo igual a uma string com o valor de shark. Depois, criamos outra variável chamada pointer. Desta vez, definimos o valor da variável pointer para o endereço da variável creature. Armazenamos o endereço de um valor em uma variável, usando o símbolo do e comercial (&). Isso significa que a variável pointer está armazenando o endereço da variável creature, não o valor real.

É por isso que, quando imprimimos o valor de pointer, recebemos o valor de 0xc0000721e0, que é o endereço onde a variável creature está armazenada na memória do computador no momento.

Se quiser imprimir o valor da variável para a qual a variável pointer está apontando, será necessário desreferenciar essa variável. O código a seguir usa o operador * para desreferenciar a variável pointer e recuperar seu valor:

main.go

 package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer) } 

Se executar esse código, você verá o seguinte resultado:

Outputcreature = shark pointer = 0xc000010200 *pointer = shark 

A última linha que acabamos de adicionar desreferencia a variável pointer e imprime o valor armazenado naquele endereço.

Se quiser modificar o valor armazenado na localização da variável pointer, é possível usar o operador de desreferenciamento também:

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer)      *pointer = "jellyfish"     fmt.Println("*pointer =", *pointer) }  

Execute este código para ver o resultado:

Outputcreature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish 

Definimos o valor ao qual a variável pointer se refere, usando o asterisco (*) na frente do nome variável e, em seguida, fornecendo um novo valor de jellyfish. Como pode ver, ao imprimirmos o valor desreferenciado, ele ficou definido como jellyfish.

Você pode não ter percebido isso, mas também alteramos o valor da variável creature. Isso acontece porque a variável pointer está, na verdade, apontando para o endereço da variável creature. Isso significa que, se alterarmos o valor para o qual a variável pointer aponta, também vamos alterar o valor da variável creature.

main.go

package main  import "fmt"  func main() {     var creature string = "shark"     var pointer *string = &creature      fmt.Println("creature =", creature)     fmt.Println("pointer =", pointer)      fmt.Println("*pointer =", *pointer)      *pointer = "jellyfish"     fmt.Println("*pointer =", *pointer)      fmt.Println("creature =", creature) } 

O resultado obtido fica parecido com o seguinte:

Outputcreature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish 

Embora esse código demonstre como um ponteiro funciona, essa não é a maneira típica pela qual você usaria ponteiros em Go. O uso de ponteiros é mais comum ao definirmos argumentos de funções e valores de retorno, ou ao definirmos métodos em tipos personalizados. Vejamos como você usaria os ponteiros com funções para compartilhar o acesso a uma variável.

Novamente, tenha em mente que estamos imprimindo o valor de pointer para ilustrar que ele é um ponteiro. Na prática, você não usaria o valor de um ponteiro, a não ser para fazer referência ao valor subjacente para recuperar ou atualizar aquele valor.

Destinatários do ponteiro para função

Quando você escreve uma função, você pode definir os argumentos a serem passados por valor ou por referência. Passar por valor significa dizer que uma cópia desse valor é enviada para a função e quaisquer alterações feitas no argumento – dentro daquela_ função_ – afetam somente a variável naquela função e não o local de onde ela foi passada. No entanto, se você passar por referência, ou seja, se passar um ponteiro para aquele argumento, você poderá alterar o valor de dentro da função e também alterar o valor da variável original que foi passado. Você pode ler mais sobre como definir funções em nosso artigo sobre Como definir e chamar funções em Go.

Ao decidir quando passar um ponteiro – ao invés de enviar um valor, o importante é saber se você deseja que o valor mude ou não. Se não quiser que o valor mude, envie-o como um valor. Se quiser que a função para a qual você está passando sua variável possa alterá-la, então você a passaria como um ponteiro.

Para saber a diferença, primeiramente, vejamos uma função que está passando um argumento por value [valor]:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) }  

O resultado obtido fica parecido com o seguinte:

Output1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark} 

Primeiro, criamos um tipo personalizado chamado Creature. Ele tem um campo chamado Species, que é uma string. Na função main, criamos uma instância do nosso novo tipo chamado creature e definimos o campo Species como shark. Na sequência, imprimimos a variável para mostrar o valor atual armazenado dentro da variável creature.

Em seguida, chamamos changeCreature e enviamos uma cópia da variável creature.

Definimos a função changeCreature como aquela que adota um argumento chamado creature – sendo do tipo Creature que definimos anteriormente. Então, mudamos o valor do campo Species para jellyfish e o imprimimos. Note que dentro da função changeCreature, o valor de Species agora é jellyfish e ela imprime 2) {Species:jellyfish}. Isso acontece porque temos permissão para alterar o valor dentro do escopo da nossa função.

No entanto, quando a última linha da função main imprime o valor de creature, o valor de Species ainda é shark. A razão pela qual o valor não mudou é porque passamos a variável por valor. Isso significa que uma cópia do valor foi criada na memória e passada para a função changeCreature. Isso nos permite ter uma função que pode fazer alterações em quaisquer argumentos enviados – conforme necessário. Tais alterações não vão afetar nenhuma daquelas variáveis fora da função.

Em seguida, vamos alterar a função changeCreature para que receba um argumento por referência. Podemos fazer isso alterando o tipo de creature para um ponteiro usando o operador asterisco (*). Em vez de passar uma creature, agora estamos passando um ponteiro para uma creature, ou uma *creature. No exemplo anterior, creature é uma struct que tem um valor Species como sendo shark. O *creature é um ponteiro, não uma struct; assim, o seu valor é uma localização de memória e é isso o que passamos para changeCreature().

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(&creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Execute este código para ver o seguinte resultado:

Output1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish} 

Agora, observe que, quando alteramos o valor de Species para jellyfish na função changeCreature, isso também altera o valor original definido na função main. Isso acontece porque passamos a variável creature por referência, o que permite o acesso ao valor original e que pode alterá-lo conforme necessário.

Portanto, se quiser que uma função seja capaz de alterar um valor, será necessário passá-la por referência. Para passar por referência, você passa o ponteiro para a variável e não a variável em si.

No entanto, algumas vezes você pode não ter um valor real definido para um ponteiro. Nesses casos, é possível ter um pânico no programa. Vejamos como isso acontece e como se planejar para aquele problema em potencial.

Ponteiros nil

Todas as variáveis em Go têm um valor zero. Isso é verdade mesmo em relação a um ponteiro. Se declarar um ponteiro para um tipo, mas não atribuir um valor, o valor zero será nil. nil é uma maneira de dizer para a variável que nothing has been initialized [nada foi inicializado].

No programa a seguir, vamos definir um ponteiro para um tipo Creature, mas jamais iremos instanciar tal instância real de uma Creature e lhe atribuir seu endereço para a variável de ponteiro creature. O valor será nil e não poderemos fazer referência a nenhum dos campos ou métodos que seriam definidos no tipo Creature:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

O resultado obtido fica parecido com o seguinte:

Output1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86]  goroutine 1 [running]: main.changeCreature(0x0)         /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26     main.main()             /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98         exit status 2 

Quando executamos o programa, ele imprime o valor da variável creature e o valor é <nil>. Depois, chamamos a função changeCreature e, quando essa função tenta definir o valor do campo Species, o programa entra em pânico. Isso acontece porque, na verdade, nenhuma instância da variável foi criada. Por conta disso, o programa não tem, de fato, onde armazenar o valor e, por isso, ele entra em pânico.

Quando você está recebendo um argumento como um ponteiro, na linguagem Go é comum que você verifique se tal argumento estava ou não nil – antes de realizar qualquer operação nele, no intuito de evitar que o programa entre em pânico.

Esta é uma abordagem comum para verificar em relação a nil:

if someVariable == nil {     // print an error or return from the method or fuction } 

Na prática, você quer certificar-se que você não tem um ponteiro nil ali, que tenha sido enviado para a sua função ou método. Se tiver, você provavelmente vai querer devolver, ou retornar um erro para mostrar que um argumento inválido foi passado para a função ou método. O código a seguir demonstra como verificar se há nil:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     if creature == nil {         fmt.Println("creature is nil")         return     }      creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Adicionamos uma verificação em changeCreature para ver se o valor do argumento creature era nil. Se era, imprimiríamos creature is nil e retornaríamos o resultado da função. Caso contrário, devemos prosseguir e alterar o valor do campo Species. Se executarmos o programa, vamos receber a seguinte saída:

Output1) <nil> creature is nil 3) <nil> 

Note que, embora ainda tenhamos um valor nil para a variável creature, não estamos mais em pânico, pois estamos inspecionando quanto a tal hipótese.

Por fim, se criarmos uma instância do tipo Creature e a atribuirmos para a variável creature, o programa irá, então, alterar o valor como esperado:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func main() {     var creature *Creature     creature = &Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     changeCreature(creature)     fmt.Printf("3) %+vn", creature) }  func changeCreature(creature *Creature) {     if creature == nil {         fmt.Println("creature is nil")         return     }      creature.Species = "jellyfish"     fmt.Printf("2) %+vn", creature) } 

Agora que temos uma instância do tipo Creature, o programa será executado e vamos obter o seguinte resultado esperado:

Output1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish} 

Quando você está trabalhando com ponteiros, existe a possibilidade do programa entrar em pânico. Para evitar o pânico, você deve verificar se um valor de ponteiro é nil antes de tentar acessar qualquer um dos campos ou métodos definidos nele.

Em seguida, vamos ver como o uso de ponteiros e valores afeta a definição de métodos em um tipo.

Destinatários do ponteiro para método

Um receiver [destinatário] em Go consiste no argumento que foi definido numa declaração de método. Examine o código a seguir:

type Creature struct {     Species string }  func (c Creature) String() string {     return c.Species } 

O destinatário neste método é c Creature. Ele está declarando que a instância c é do tipo Creature e você fará referência àquele tipo através daquela variável de instância.

Assim como o comportamento das funções é diferente, considerando se você envia um argumento como um ponteiro ou um valor, os métodos também têm comportamento diferente. A grande diferença é que, se você definir um método com o destinatário do valor, você não vai conseguir fazer alterações na instância daquele tipo no qual o método foi definido.

Haverá vezes em que você irá querer que seu método consiga atualizar a instância da variável que você estiver usando. Para se planejar para isso, você vai querer fazer um ponteiro para o destinatário.

Vamos adicionar um método Reset ao nosso tipo Creature que definirá o campo Species para uma string vazia:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func (c Creature) Reset() {     c.Species = "" }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     creature.Reset()     fmt.Printf("2) %+vn", creature) } 

Se executarmos o programa, vamos receber a seguinte saída:

Output1) {Species:shark} 2) {Species:shark} 

Note que, quando imprimimos o valor de nossa variável creature na função main, o valor ainda está definido como shark – mesmo que no método Reset tenhamos definido o valor de Species em uma string vazia. Isso acontece porque definimos o método Reset como tendo um destinatário para o value. Isso significa que o método terá acesso apenas a uma cópia da variável creature.

Se quisermos poder alterar a instância da variável creature nos métodos, precisaremos defini-los como tendo um destinatário para o pointer:

main.go

package main  import "fmt"  type Creature struct {     Species string }  func (c *Creature) Reset() {     c.Species = "" }  func main() {     var creature Creature = Creature{Species: "shark"}      fmt.Printf("1) %+vn", creature)     creature.Reset()     fmt.Printf("2) %+vn", creature) } 

Note que adicionamos agora um asterisco (*) na frente do tipo Creature quando definimos o método Reset. Isso significa que a instância de Creature que é enviada para o método Reset passará a ser um ponteiro e, como tal, quando nós o alterarmos, isso irá afetar a instância original daquela variável.

Output1) {Species:shark} 2) {Species:} 

O método Reset agora mudou o valor do campo Species.

Conclusão

Definir uma função ou método com um parâmetro de passar por value ou passar por reference irá afetar quais partes do seu programa conseguirão fazer alterações às demais partes. Controlar quando tal variável poderá ser alterada permitirá que você escreva softwares mais robustos e previsíveis. Agora que você aprendeu sobre ponteiros, você pode ver como eles são usados em interfaces.