Ponteiros
Nesta aula, exploramos ponteiros em Go: os operadores & e *, a função new, nil pointers e as razões pelas quais Go adota ponteiros de forma segura. Aprenda a manipular referências a dados sem os riscos de aritmética de ponteiros.
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 é
nilantes 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
- Effective Go: Pointers
- Go Specification: Pointer types
- Go Specification: Address operators
- Go Specification: Allocation with new
- Go Specification: Nil
- The Go Blog
Exercícios
Crie uma função
swapque recebe dois ponteiros para inteiros e troca os valores apontados.✓ Resposta:func swap(a, b *int) { temp := *a *a = *b *b = temp }Escreva um programa que declare uma variável
sdo tipo string com valor "Hello", crie um ponteiro paras, 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 }Use a função
newpara 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) }Crie uma struct
Rectcom camposwidtheheight(float64). Escreva uma funçãoscaleque recebe um ponteiro paraRecte 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} }Escreva um programa que declare um ponteiro
ppara int sem inicializá-lo, verifique se é nil, e se for, atribua um novo int com valor 100 usandonew. 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 }