Conceitos Fundamentais
Primeiramente, vamos estabelecer as diferenças cruciais:
- uintptr é um valor numérico que representa um endereço de memória. Não é um ponteiro e não mantém relação de referência com o objeto no endereço. O coletor de lixo não preservará um objeto apenas porque existe um valor do tipo uintptr apontando para ele.
- unsafe.Pointer é um ponteiro, similar ao
void *em C. Mantém uma relação de referência com o objeto no endereço, e o coletor de lixo irá preservar o objeto se um unsafe.Pointer estiver apontando para ele. - Qualquer ponteiro pode ser convertido para unsafe.Pointer
- unsafe.Pointer pode ser convertido para qualquer ponteiro
- uintptr pode ser convertido para unsafe.Pointer
- unsafe.Pointer pode ser convertido para uintptr
- Ponteiros não podem ser convertidos diretamente para uintptr
Por que existe o tipo uintptr?
Teoricamente, um ponteiro é apenas um valor numérico, ou seja, um uint. No entanto, em Go, unsafe.Pointer não pode ser convertido diretamente para uint, apenas para uintptr.
var valor float64 = 3.14
var ponteiro *float64 = &valor
_ = int(ponteiro) // Erro de compilação: cannot convert unsafe.Pointer(ponteiro) (type unsafe.Pointer) to type uint
Mas podemos converter um unsafe.Pointer para uintptr:
var valor float64 = 3.14
var ponteiro *float64 = &valor
var endereco uintptr = uintptr(unsafe.Pointer(ponteiro))
numero := uint(endereco)
fmt.Printf("%d %d\n", endereco, numero) // Os valores impressos são idênticos
Podemos entender uintptr como um uint especializado para operações com ponteiros. É importante notar que ponteiros não podem ser convertidos diretamente para uintptr:
var dado float64
uintptr(&dado) // Erro: não é possível converter *float64 para uintptr
Exemplo Prático
Se ainda está confuso com as explicações acima, vamos analisar um caso concreto:
package dados
type Usuario struct {
Nome string
idade int // campo não exportado
}
No código acima, definimos uma estrutura Usuario no pacote dados, exportando apenas o campo Nome. Isso significa que em outros pacotes só podemos manipular Usuario.Nome diretamente, mas não Usuario.idade. Contudo, podemos usar o pacote unsafe para contornar essa limitação:
package main
import (
"fmt"
"unsafe"
"projeto/dados"
)
func main() {
usuario := &dados.Usuario{
Nome: "Ana Silva",
}
fmt.Println(usuario)
// *Usuario não pode ser convertido diretamente para *string
// Primeiro convertemos *Usuario para unsafe.Pointer, depois para *string
nomePtr := (*string)(unsafe.Pointer(usuario))
*nomePtr = "Carla Oliveira"
// Para acessar o campo não exportado 'idade':
// 1. Obtemos o endereço de Nome
// 2. Adicionamos o tamanho do campo Nome para encontrar o endereço de idade
// 3. Convertemos o resultado para um ponteiro de int
idadePtr := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(usuario)) + unsafe.Sizeof(usuario.Nome))))
*idadePtr = 25
fmt.Println(usuario)
}
A saída será:
$ go run main.go
&{Ana Silva 0}
&{Carla Oliveira 25}
É importante notar que a seguinte linha de código é extensa:
idadePtr := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(usuario)) + unsafe.Sizeof(usuario.Nome))))
Evite dividí-la em duas partes, como:
temp := uintptr(unsafe.Pointer(usuario)) + unsafe.Sizeof(usuario.Nome)
idadePtr := (*int)(unsafe.Pointer(temp))
Isso porque na segunda linha, não há mais nenhum ponteiro apontando para usuario, o que pode fazer com que o objeto seja coletado pelo lixo. Nesse caso, temp se tornaria um ponteiro selvagem, apontando para um local indefinido, o que é extremamente perigoso.
Outro motivo é que embora a versão atual do Go (1.14) não migre objetos na memória, versões futuras podem implementar essa funcionalidade. Se a migração de memória ocorrer, endereços de ponteiros mudarão, e o código dividido poderia apresentar problemas graves.
Adicionalmente, no Go 1.3, quando a pilha precisa crescer, ela pode ser movida. Para o código abaixo:
var objeto int
fmt.Println(uintptr(unsafe.Pointer(&objeto)))
funcaoGrande() // funcaoGrande() aumenta a pilha
fmt.Println(uintptr(unsafe.Pointer(&objeto)))
É perfeitamente possível que dois endereços diferentes sejam impressos.
Através deste exemplo, fica claro por que o pacote se chama "unsafe" - seu uso realmente apresenta riscos, então evite utilizá-lo sempre que possível.
Pesquisei sobre unsafe.Pointer porque precisava realizar operações atômicas em um ambiente multithread para evitar condições de corrida, utilizando atomic.LoadPointer(addr *unsafe.Pointer). Posteriormente, descobri que o pacote atomic oferece uma estrutura atomic.Value cujos métodos me permitiram evitar o uso explícito de unsafe.Pointer. Se você está utilizando atomic.LoadPointer(), considere verificar se atomic.Value pode resolver seu problema - esta é uma sugestão baseada na minha experiência.