Ponteiros são uma das características mais importantes de Go, permitindo que você trabalhe diretamente com endereços de memória. Eles são usados para modificar variáveis passadas para funções, alocar memória dinamicamente e criar estruturas de dados eficientes. Nesta aula, vamos explorar os operadores & e *, a função new, e como lidar com ponteiros nulos de forma segura.

Em Go, ponteiros são simples e seguros: não há aritmética de ponteiros como em C, e o coletor de lixo gerencia a memória automaticamente. No entanto, entender ponteiros é essencial para programar eficientemente em Go.

& e *

O operador & é usado para obter o endereço de memória de uma variável. Ele retorna um ponteiro para aquela variável. Já o operador * é usado para dereferenciar um ponteiro, ou seja, acessar o valor armazenado no endereço apontado.

Exemplo básico:

package main

import "fmt"

func main() {
    x := 42
    p := &x         // p é um ponteiro para int, aponta para x
    fmt.Println(p)  // imprime o endereço de memória (ex: 0xc0000a6000)
    fmt.Println(*p) // imprime 42 (valor de x)
    
    *p = 21        // altera o valor de x através do ponteiro
    fmt.Println(x)  // imprime 21
}

Os ponteiros em Go têm um tipo específico: *T é um ponteiro para um valor do tipo T. Você pode declarar ponteiros explicitamente:

var p *int
fmt.Println(p) // nil, pois não foi inicializado

Por que Go tem ponteiros

Go tem ponteiros para permitir modificar variáveis passadas para funções sem precisar copiar grandes estruturas de dados. Em Go, tudo é passado por valor (cópia), então se você passar uma struct grande, ela será copiada inteira. Com ponteiros, você pode passar apenas o endereço, economizando memória e permitindo que a função modifique o original.

Exemplo:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func birthday(p *Person) {
    p.Age++ // modifica o original
}

func main() {
    bob := Person{Name: "Bob", Age: 30}
    birthday(&bob)
    fmt.Println(bob.Age) // 31
}

Sem ponteiros, a função birthday receberia uma cópia e a idade não seria incrementada. Além disso, ponteiros são essenciais para implementar estruturas de dados como listas encadeadas, árvores e grafos, onde cada nó precisa referenciar outros nós.

new

A função new aloca memória para um tipo e retorna um ponteiro para ela. O valor alocado é o valor zero do tipo (0 para inteiros, false para booleanos, nil para slices/maps/ponteiros, etc.).

Exemplo:

package main

import "fmt"

func main() {
    p := new(int)
    fmt.Println(*p) // 0
    *p = 10
    fmt.Println(*p) // 10
}

Note que new é diferente de make. make é usado apenas para slices, maps e canais, pois eles precisam de inicialização interna. new é para qualquer tipo e retorna um ponteiro.

Nil pointers e segurança

Um ponteiro não inicializado tem valor nil. Tentar dereferenciar um ponteiro nulo causa um pânico (panic) em tempo de execução. Por isso, é importante sempre verificar se um ponteiro é nil antes de usá-lo.

Exemplo de perigo:

var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference

Para evitar isso, verifique com if p != nil:

if p != nil {
    fmt.Println(*p)
} else {
    fmt.Println("p is nil")
}

Em Go, é comum que funções retornem ponteiros e também um erro. Sempre verifique o erro antes de usar o ponteiro. Além disso, Go é seguro contra ponteiros selvagens: não há aritmética de ponteiros, então você não pode acessar memória arbitrária.

Boas práticas

Use ponteiros quando precisar modificar o valor original ou evitar cópias grandes. Evite ponteiros para tipos pequenos como ints, a menos que necessário. Prefira retornar valores (não ponteiros) para tipos simples. Em APIs, documente se um parâmetro de ponteiro pode ser nil.

Referências

Exercícios

  1. Escreva uma função que recebe um ponteiro para um int e dobra o valor apontado.

    ✓ Resposta:
    func double(p *int) {
        *p *= 2
    }
  2. Use a função new para criar um ponteiro para string, atribua "Olá" e imprima o valor.

    ✓ Resposta:
    p := new(string)
    *p = "Olá"
    fmt.Println(*p)
  3. Explique por que o código abaixo causa pânico e como corrigi-lo:

    var p *int
    fmt.Println(*p)

    ✓ Resposta: O ponteiro p é nil, então dereferenciá-lo causa pânico. Corrigir verificando se p não é nil:
    if p != nil {
        fmt.Println(*p)
    } else {
        fmt.Println("p é nil")
    }
  4. Crie uma struct chamada Contador com um campo valor int. Escreva um método Incrementar que recebe um ponteiro para Contador e incrementa o valor.

    ✓ Resposta:
    type Contador struct {
        valor int
    }
    
    func (c *Contador) Incrementar() {
        c.valor++
    }
    
    func main() {
        c := Contador{valor: 0}
        c.Incrementar()
        fmt.Println(c.valor) // 1
    }
  5. Escreva uma função que recebe um ponteiro para um slice de inteiros e adiciona o número 10 ao slice. (Dica: slices são referências, mas você pode modificar o slice original via ponteiro?)

    ✓ Resposta: Slices já são referências, mas para modificar o slice original (como adicionar elementos), você precisa de um ponteiro para o slice, pois append pode realocar o array subjacente. Exemplo:
    func adicionar(s *[]int) {
        *s = append(*s, 10)
    }
    
    func main() {
        nums := []int{1, 2, 3}
        adicionar(&nums)
        fmt.Println(nums) // [1 2 3 10]
    }