Ponteiros são um dos conceitos fundamentais em Go, permitindo que você trabalhe com referências a valores em vez de cópias. Diferente de linguagens como C, Go não permite aritmética de ponteiros, o que torna seu uso mais seguro, mas ainda assim poderoso. Nesta aula, vamos entender como criar, usar e gerenciar ponteiros, incluindo a função new e o valor nil.

Ponteiros são úteis para modificar valores em funções, evitar cópias desnecessárias de grandes estruturas e implementar estruturas de dados como listas ligadas. Vamos mergulhar nos detalhes.

& e *

Em Go, o operador & é usado para obter o endereço de memória de uma variável, enquanto o operador * é usado para desreferenciar um ponteiro, ou seja, acessar o valor apontado. Eles são complementares e essenciais para trabalhar com ponteiros.

Por exemplo, se você tem uma variável x, &x retorna um ponteiro para x. Se p é um ponteiro, *p retorna o valor no endereço apontado por p. A sintaxe é limpa e direta.

package main

import "fmt"

func main() {
    x := 10
    p := &x  // p é um ponteiro para int, apontando para x
    fmt.Println(p)  // imprime o endereço de memória (ex: 0xc0000180a8)
    fmt.Println(*p) // imprime 10, o valor de x através do ponteiro

    *p = 20         // altera o valor de x através do ponteiro
    fmt.Println(x)  // imprime 20
}

Note que p é do tipo *int (ponteiro para int). O operador * também é usado na declaração de tipos ponteiros. Por exemplo, var p *int declara p como um ponteiro para inteiro.

Por que Go tem ponteiros

Go foi projetado para ser uma linguagem de sistemas eficiente, e ponteiros são uma ferramenta essencial para controle de memória e desempenho. No entanto, Go evita os perigos de ponteiros em C, como aritmética de ponteiros e acesso a memória inválida, tornando o código mais seguro.

Ponteiros são usados para:

  • Modificar variáveis em funções: Sem ponteiros, Go passa argumentos por valor, o que significa que a função recebe uma cópia. Com ponteiros, você pode alterar o valor original.
  • Evitar cópias de grandes estruturas: Passar um ponteiro para uma struct grande é mais eficiente do que copiar a struct inteira.
  • Implementar estruturas de dados: Listas ligadas, árvores e grafos geralmente requerem referências entre nós, que são implementadas com ponteiros.
  • Interface com código C: Em chamadas de sistema ou bibliotecas C, ponteiros são necessários.
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

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

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

Sem ponteiros, a função birthday receberia uma cópia e a idade não seria alterada.

new

A função new aloca memória para um tipo e retorna um ponteiro para o valor zero do tipo. É uma forma de criar ponteiros de maneira limpa, sem precisar declarar uma variável separadamente.

package main

import "fmt"

func main() {
    p := new(int)   // p é *int, aponta para um int com valor 0
    fmt.Println(*p) // 0
    *p = 42
    fmt.Println(*p) // 42
}

Comparado com a criação de uma variável e uso de &, new é mais conciso quando você precisa apenas de um ponteiro para um valor zero. No entanto, para valores não zero, você deve usar a inicialização literal.

// Equivalente sem new:
var x int
p := &x
// ou
p := &int{} // não funciona, pois & requer uma variável

Na prática, new é menos usado porque a inicialização literal com &T{} (para structs) ou &variavel é mais comum. Mas new é útil para tipos básicos e em alguns padrões como fábricas.

Nil pointers e segurança

Um ponteiro pode ser nil, indicando que não aponta para nenhum valor. Tentar desreferenciar um ponteiro nil causa um pânico em tempo de execução (runtime panic). Por isso, é importante verificar se um ponteiro é nil antes de usá-lo.

package main

import "fmt"

func main() {
    var p *int // p é nil
    fmt.Println(p) // <nil>
    // fmt.Println(*p) // isso causaria panic: runtime error: invalid memory address or nil pointer dereference

    if p != nil {
        fmt.Println(*p)
    } else {
        fmt.Println("p é nil, não pode ser desreferenciado")
    }
}

Go não permite aritmética de ponteiros, o que elimina uma grande classe de bugs. Além disso, o coletor de lixo gerencia a memória, então você não precisa se preocupar com liberação manual. Essas características tornam os ponteiros em Go seguros e fáceis de usar, mantendo a eficiência.

Boas práticas:

  • Sempre verifique se um ponteiro é nil antes de desreferenciá-lo, especialmente em funções que recebem ponteiros como parâmetros.
  • Evite retornar ponteiros para variáveis locais se elas não escaparem para o heap; o compilador gerencia isso, mas é bom entender o escopo.
  • Use ponteiros apenas quando necessário; para tipos pequenos, passar por valor é mais simples e eficiente.

Referências

Exercícios

  1. Crie uma função swap que recebe dois ponteiros para inteiros e troca os valores apontados.

    ✓ Resposta:
    func swap(a, b *int) {
        temp := *a
        *a = *b
        *b = temp
    }
  2. Escreva um programa que declare uma variável s do tipo string com valor "Hello", crie um ponteiro para s, e altere o valor para "World" usando o ponteiro. Imprima o antes e depois.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := "Hello"
        p := &s
        fmt.Println(s) // Hello
        *p = "World"
        fmt.Println(s) // World
    }
  3. Use a função new para criar um ponteiro para um float64, atribua o valor 3.14 através do ponteiro e imprima o valor.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        p := new(float64)
        *p = 3.14
        fmt.Println(*p)
    }
  4. Crie uma struct Rect com campos width e height (float64). Escreva uma função scale que recebe um ponteiro para Rect e um fator, e multiplica ambos os campos pelo fator. Teste.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Rect struct {
        width, height float64
    }
    
    func scale(r *Rect, factor float64) {
        r.width *= factor
        r.height *= factor
    }
    
    func main() {
        rect := Rect{10, 20}
        scale(&rect, 2)
        fmt.Println(rect) // {20, 40}
    }
  5. Escreva um programa que declare um ponteiro p para int sem inicializá-lo, verifique se é nil, e se for, atribua um novo int com valor 100 usando new. Depois imprima o valor.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        var p *int
        if p == nil {
            p = new(int)
            *p = 100
        }
        fmt.Println(*p) // 100
    }