Golang

Ponteiros: conceito, uso e quando evitar Já leu

8 min de leitura

Ponteiros: conceito, uso e quando evitar
Ponteiros são um dos tópicos que mais intimidam iniciantes vindos de linguagens como Python, JavaScript ou Java — onde o programador raramen

Ponteiros são um dos tópicos que mais intimidam iniciantes vindos de linguagens como Python, JavaScript ou Java — onde o programador raramente precisa pensar em endereços de memória. Em C e C++, ponteiros são poderosos mas perigosos: aritmética de ponteiros, ponteiros soltos e corrupção de memória são problemas reais.

Go encontra um meio-termo deliberado. Ponteiros existem e são necessários, mas a aritmética de ponteiros não é permitida, e o garbage collector cuida da memória automaticamente. O resultado é um sistema de ponteiros seguro e útil, sem as armadilhas clássicas do C.


O que é um ponteiro

Toda variável em um programa ocupa um espaço na memória, e esse espaço tem um endereço — um número que identifica sua localização. Um ponteiro é simplesmente uma variável que armazena esse endereço em vez de um valor direto.

Em Go, o tipo de um ponteiro para int é escrito como *int. O tipo de um ponteiro para string é *string. A regra é sempre *Tipo.

Dois operadores são fundamentais para trabalhar com ponteiros:

  • & — operador de endereço: obtém o endereço de uma variável
  • * — operador de desreferência: acessa o valor no endereço armazenado
package main

import "fmt"

func main() {
    x := 42

    p := &x  // p é um ponteiro para x; seu tipo é *int

    fmt.Println(x)   // 42     — valor de x
    fmt.Println(p)   // 0xc000...  — endereço de x
    fmt.Println(*p)  // 42     — valor no endereço apontado por p

    *p = 100         // modifica x através do ponteiro
    fmt.Println(x)   // 100
}

Alterar *p altera x diretamente, pois ambos apontam para o mesmo espaço na memória.


O valor zero de um ponteiro

O valor zero de qualquer tipo ponteiro em Go é nil. Um ponteiro nil não aponta para nenhum endereço válido. Tentar desreferenciar um ponteiro nil causa um pânico em tempo de execução:

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

Sempre verifique se um ponteiro é nil antes de desreferenciá-lo quando sua origem não é garantida:

func imprimirValor(p *int) {
    if p == nil {
        fmt.Println("ponteiro nulo, nada a imprimir")
        return
    }
    fmt.Println(*p)
}

Por que ponteiros existem: passagem por valor vs por referência

Em Go, todos os argumentos de função são passados por valor — o que significa que a função recebe uma cópia do argumento, não o original. Modificações dentro da função não afetam a variável original:

package main

import "fmt"

func tentarDobrar(n int) {
    n = n * 2  // modifica apenas a cópia local
}

func main() {
    x := 10
    tentarDobrar(x)
    fmt.Println(x) // 10 — x não foi alterado
}

Para que a função modifique o valor original, é necessário passar um ponteiro:

func dobrar(n *int) {
    *n = *n * 2  // modifica o valor no endereço recebido
}

func main() {
    x := 10
    dobrar(&x)
    fmt.Println(x) // 20 — x foi modificado
}

Ponteiros com structs

O uso mais comum de ponteiros em Go é com structs. Passar uma struct grande por valor copia todos os seus campos — o que pode ser custoso. Passar um ponteiro para a struct é mais eficiente e permite que a função modifique os campos originais:

package main

import "fmt"

type Retangulo struct {
    Largura  float64
    Altura   float64
}

func escalar(r *Retangulo, fator float64) {
    r.Largura *= fator
    r.Altura *= fator
}

func area(r *Retangulo) float64 {
    return r.Largura * r.Altura
}

func main() {
    r := Retangulo{Largura: 5.0, Altura: 3.0}

    fmt.Println(area(&r)) // 15

    escalar(&r, 2.0)

    fmt.Println(area(&r)) // 60
}

Go oferece uma conveniência importante: ao acessar campos de uma struct através de um ponteiro, não é necessário desreferenciar manualmente. Em vez de escrever (*r).Largura, Go permite simplesmente r.Largura — o compilador faz a desreferência automaticamente.


Criando ponteiros com new

Além do operador &, Go oferece a função embutida new para alocar memória para um tipo e retornar um ponteiro para ela. O valor alocado é inicializado com o valor zero do tipo:

p := new(int)      // aloca um int zerado, retorna *int
fmt.Println(*p)    // 0

*p = 99
fmt.Println(*p)    // 99

Na prática, new é menos comum do que o uso de & com um literal de struct. As duas formas abaixo são equivalentes:

// Com new
r1 := new(Retangulo)
r1.Largura = 5.0

// Com literal e &
r2 := &Retangulo{Largura: 5.0}

A segunda forma é preferida pela comunidade Go por ser mais explícita sobre o valor inicial.


Ponteiros e interfaces

Um padrão importante que aparecerá com frequência nos módulos seguintes é a distinção entre implementar uma interface com um receiver de valor versus um receiver de ponteiro. Por ora, é suficiente saber que métodos definidos com receiver de ponteiro só são acessíveis quando o valor é endereçável — ou seja, quando se tem um ponteiro para ele.

Esse tópico será explorado em detalhe no artigo sobre métodos e interfaces.


Quando usar ponteiros

A decisão de usar ou não ponteiros segue algumas diretrizes práticas que a comunidade Go consolidou ao longo dos anos.

Use ponteiros quando:

A função precisa modificar o valor original do argumento. Structs grandes são passadas como argumento com frequência e o custo de cópia importa. O tipo representa um recurso com identidade única — como uma conexão de banco de dados, um arquivo aberto ou um mutex — onde cópias não fazem sentido semântico. O valor pode ser nil para representar ausência.

Prefira valores quando:

O tipo é pequeno — inteiros, floats, bools, structs com poucos campos simples. A função não precisa modificar o argumento. O código fica mais simples e legível sem ponteiros. Tipos como time.Time, net/url.URL e a maioria dos tipos da biblioteca padrão são usados por valor.

// Prefer por valor — tipo pequeno, sem necessidade de modificação
func formatarNome(nome string) string {
    return strings.Title(nome)
}

// Preferir por ponteiro — struct grande, função modifica o estado
func (s *Servidor) iniciar(porta int) error {
    s.porta = porta
    s.ativo = true
    return s.escutar()
}

O que Go não permite: aritmética de ponteiros

Em C, é possível incrementar um ponteiro para navegar por posições consecutivas na memória. Em Go, isso é proibido:

x := 10
p := &x
p++  // erro de compilação: invalid operation: p++ (non-numeric type *int)

Essa restrição elimina uma classe inteira de bugs de memória. O pacote unsafe da biblioteca padrão oferece operações de ponteiro de baixo nível para casos extremamente específicos, mas seu nome é um aviso explícito — seu uso em código de aplicação comum é fortemente desencorajado.


Exemplo prático: lista encadeada simples

Para consolidar o conceito, abaixo está uma implementação simples de lista encadeada — uma estrutura de dados que depende fundamentalmente de ponteiros para existir:

package main

import "fmt"

type No struct {
    Valor    int
    Proximo  *No
}

func imprimirLista(inicio *No) {
    for n := inicio; n != nil; n = n.Proximo {
        fmt.Printf("%d ", n.Valor)
    }
    fmt.Println()
}

func main() {
    terceiro := &No{Valor: 30, Proximo: nil}
    segundo  := &No{Valor: 20, Proximo: terceiro}
    primeiro := &No{Valor: 10, Proximo: segundo}

    imprimirLista(primeiro) // 10 20 30
}

Cada nó armazena um ponteiro para o próximo nó. O último nó aponta para nil, sinalizando o fim da lista. Esse padrão de *No dentro da própria struct No é possível porque o Go sabe que um ponteiro tem tamanho fixo, independentemente do tipo para o qual aponta.


Resumo do que foi coberto

Este artigo apresentou o modelo de ponteiros do Go: os operadores & e *, o valor zero nil, a diferença entre passagem por valor e por ponteiro, o uso com structs, a função new e as diretrizes práticas para decidir quando usar ponteiros. A ausência de aritmética de ponteiros foi destacada como uma escolha de segurança consciente da linguagem.


Referências e leituras complementares

Comentários

Mais em Golang

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se vari&aacute;veis s&atilde;o os substantivos de um programa, operadores s&a...

Interfaces: contratos implícitos e polimorfismo
Interfaces: contratos implícitos e polimorfismo

&nbsp; Interfaces existem em Java, C#, TypeScript e diversas outras linguage...

Variáveis, tipos primitivos e declaração curta
Variáveis, tipos primitivos e declaração curta

&nbsp; O sistema de tipos do Go &eacute; sua primeira linha de defesa!&nbsp;...