Manipulação de Ponteiros em Go: unsafe.Pointer vs uintptr

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.

Publicado em 9-26 17:03