Conceptual article

Entendendo ponteiros em Go

GoDevelopment

Introdução

Quando você criar software em Go, você estará escrevendo funções e métodos. Você envia 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 da 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 enviar 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 é enviado 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 do 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. Na verdade, o ponteiro não contém um int32, mas 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 à leitura por humanos. Na prática, é provável que você nunca veja 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:

Output
creature = 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:

Output
creature = shark pointer = 0xc000010200 *pointer = shark

A última linha que adicionamos agora 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, você também poderá usar o operador de desreferenciamento:

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:

Output
creature = 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 passou a ser definido como jellyfish.

Talvez você não tenha percebido, 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:

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

Embora esse código ilustra como um ponteiro funciona, não se trata da maneira usual que você usaria os ponteiro em Go. É mais comum usar os ponteiros 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 se trata de 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.

Ponteiros receptores da 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 enviado. Você pode ler mais sobre como definir funções em nosso artigo sobre Como definir e chamar funções em Go.

Decidir entre o momento de enviar um ponteiro - ao invés de enviar um valor - é predominantemente uma questão de saber se você deseja ou não que o valor mude. Se não quiser que o valor mude, envie-o como um valor. Se quiser que a função para a qual você está enviando sua variável possa alterá-la, então você a enviaria como um ponteiro.

Para saber a diferença, primeiramente, vejamos uma função que está enviando 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) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

O resultado obtido fica parecido com o seguinte:

Output
1) {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 enviada 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, porém, 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 enviar uma creature, agora estamos enviando um ponteiro para uma creature, ou uma *creature. No exemplo anterior, creature é um struct cujo valor de Species é shark. O *creature é um ponteiro, não um struct; assim, o seu valor é uma localização de memória e é isso o que enviamos para a função changeCreature().

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(&creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

Execute este código para ver o seguinte resultado:

Output
1) {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, você precisará passá-la por referência. Para passar por referência, você envia 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 tal possível problema.

Ponteiros nil

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

No programa a seguir, vamos definir um ponteiro para um tipo Creature, mas jamais iremos instanciar a instância real de uma Creature e lhe atribuir seu endereço para a variável do 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) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

O resultado obtido fica parecido com o seguinte:

Output
1) <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 tentar 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 isso, o programa não tem, na verdade, onde armazenar o valor e, portanto, ele entra em pânico.

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

Esta é uma abordagem comum para a verificação quanto ao valor nil:

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

Na verdade, você quer assegurar que não haja um ponteiro nil que tenha sido enviado para a sua função ou método. Se tiver um ponteiro com valor nil, é provável que você queira somente retornar ou mesmo retornar um erro para mostrar que um argumento inválido foi enviado 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) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

Adicionamos uma verificação na função changeCreature para ver se o valor do argumento creature era nil. Se o valor for nil, nós imprimimos “creature is nil” e o return fora da função. Caso contrário, devemos prosseguir e alterar o valor do campo Species. Se executarmos o programa agora, vamos receber o seguinte resultado:

Output
1) <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 cenário.

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) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

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

Output
1) &{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 examinar de que modo o uso de ponteiros e valores afeta a definição de métodos em um tipo.

Ponteiros receptores de método

Um receptor 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 receptor 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 da 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 comportamentos diferentes. A grande diferença é que, se você definir um método com um receptor do valor, você não vai conseguir fazer alterações na instância daquele tipo no qual o método foi definido.

Haverá ocasiões em que você irá querer que seu método consiga atualizar a instância da variável que você estiver usando. Para permitir que isso ocorra, você vai querer transformar um ponteiro em receptor.

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) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

Se executarmos o programa, teremos o seguinte resultado:

Output
1) {Species:shark} 2) {Species:shark}

Embora tenhamos definido o valor de Species para uma string vazia no método Reset, note que, ao imprimimos o valor de nossa variável creature na função main, o valor permanece definido como shark. Isso acontece porque definimos o método Reset como tendo um receptor value. Isso significa que o método terá acesso apenas a uma cópia da variável creature.

Se quisermos conseguir alterar a instância da variável creature nos métodos, precisaremos defini-los como tendo um receptor 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) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

Note que, dessa vez, adicionamos 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.

Output
1) {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 passado por valor ou passado por referência irá afetar quais partes do seu programa conseguirão fazer alterações a outras 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.

Creative Commons License