Unsafe: quando e como usar com responsabilidade

1. O pacote unsafe e seus perigos innerentes

O pacote unsafe do Go é uma porta de entrada para operações de baixo nível que violam as garantias de segurança de tipo da linguagem. Enquanto o Go foi projetado para ser seguro, previsível e livre de comportamento indefinido, unsafe permite manipular memória arbitrariamente, ignorando o sistema de tipos.

Os riscos são reais: corrupção de memória, data races, panics misteriosos e comportamento indefinido que pode variar entre versões do compilador. A filosofia do Go sempre priorizou segurança e previsibilidade — unsafe é o contraponto necessário para cenários onde o desempenho bruto ou a interoperabilidade com C são requisitos inegociáveis.

A regra de ouro: use unsafe apenas quando você entende exatamente o layout de memória dos seus dados e não existe alternativa segura viável.

2. Operações fundamentais do pacote unsafe

O pacote expõe três tipos principais:

import "unsafe"

var x int32 = 42

// unsafe.Pointer: ponteiro genérico sem tipo
ptr := unsafe.Pointer(&x)

// uintptr: endereço numérico (cuidado com GC!)
addr := uintptr(ptr)

// Sizeof, Alignof, Offsetof: inspeção de structs
type Exemplo struct {
    A bool    // 1 byte
    B int64   // 8 bytes (alinha a 8)
    C string  // 16 bytes (pointer + len)
}

fmt.Println(unsafe.Sizeof(Exemplo{}))    // 32 (com padding)
fmt.Println(unsafe.Alignof(Exemplo{}))   // 8
fmt.Println(unsafe.Offsetof(Exemplo{}.B)) // 8 (após padding)

O perigo do uintptr é que o garbage collector não rastreia endereços numéricos — se o objeto original for movido, o uintptr se torna inválido silenciosamente.

3. Casos de uso legítimos e controlados

Interoperabilidade com C via cgo

/*
#include <stdlib.h>
*/
import "C"
import "unsafe"

func stringParaC(s string) *C.char {
    return (*C.char)(unsafe.Pointer(unsafe.StringData(s)))
}

Serialização binária de alto desempenho

type Header struct {
    Magic   uint32
    Version uint16
    Flags   uint8
    _       [1]byte // padding
}

func headerToBytes(h *Header) []byte {
    return unsafe.Slice((*byte)(unsafe.Pointer(h)), unsafe.Sizeof(*h))
}

Acesso a campos não exportados (com extrema cautela)

type segredo struct { valor int }

func LerSegredo(s *segredo) int {
    return *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.valor)))
}

Isso quebra encapsulamento e pode falhar se a struct mudar de versão.

4. Conversões seguras de slice e string

Transformação []byte ↔ string sem cópia

func BytesToString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

Essas funções são zero-allocation, mas perigosas: modificar o []byte resultante de StringToBytes corrompe a string original, quebrando a imutabilidade.

Conversão de []T para []byte

type Pixel struct { R, G, B, A byte }

func PixelsToBytes(pixels []Pixel) []byte {
    if len(pixels) == 0 {
        return nil
    }
    return unsafe.Slice((*byte)(unsafe.Pointer(&pixels[0])), len(pixels)*int(unsafe.Sizeof(Pixel{})))
}

Útil para enviar dados para GPU ou sockets, mas requer que o backing array não seja realocado durante o uso.

5. Pattern unsafe + reflect: inspeção e manipulação avançada

import "reflect"

func MakeSlice(typ reflect.Type, len, cap int) interface{} {
    sliceType := reflect.SliceOf(typ)
    slice := reflect.MakeSlice(sliceType, len, cap)

    // Acessa o ponteiro do slice via unsafe
    slicePtr := unsafe.Pointer(slice.Pointer())

    // Cria um header de slice customizado
    type SliceHeader struct {
        Data unsafe.Pointer
        Len  int
        Cap  int
    }

    header := (*SliceHeader)(unsafe.Pointer(&slice))
    header.Len = len
    header.Cap = cap

    return slice.Interface()
}

Isso permite criar slices com capacidade maior que o comprimento sem realocar, mas viola contratos internos do runtime.

6. Boas práticas e mitigação de riscos

// Isolar código unsafe em pacote pequeno e bem testado
package bufferpool

import "unsafe"

// PoolBytes converte []byte para string sem cópia
// PRÉ-CONDIÇÃO: o slice não deve ser modificado após a conversão
func PoolBytes(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}

Use //go:linkname com moderação para acessar símbolos internos:

//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64

Documente explicitamente pré-condições e invariantes de memória. Escreva testes que verifiquem comportamento com race detector (-race).

7. Alternativas modernas e quando evitá-lo

  • encoding/binary para serialização segura e portável
  • Generics (Go 1.18+) para algoritmos polimórficos sem unsafe
  • sync.Pool para reutilização de buffers sem manipulação direta de ponteiros
  • io.Reader/Writer para streaming de dados
// Alternativa segura com generics
func BytesTo[T any](b []byte) T {
    var v T
    buf := bytes.NewReader(b)
    binary.Read(buf, binary.LittleEndian, &v)
    return v
}

8. Exemplo prático: buffer pool com zero-copy

package main

import (
    "fmt"
    "sync"
    "unsafe"
)

type BufferPool struct {
    pool sync.Pool
}

func NewBufferPool(size int) *BufferPool {
    return &BufferPool{
        pool: sync.Pool{
            New: func() interface{} {
                buf := make([]byte, 0, size)
                return &buf
            },
        },
    }
}

func (bp *BufferPool) Get() []byte {
    buf := bp.pool.Get().(*[]byte)
    return *buf
}

func (bp *BufferPool) Put(buf []byte) {
    buf = buf[:0]
    bp.pool.Put(&buf)
}

// Versão unsafe: converte para string sem cópia
func (bp *BufferPool) GetString() string {
    buf := bp.Get()
    return unsafe.String(unsafe.SliceData(buf), len(buf))
}

func main() {
    pool := NewBufferPool(1024)
    buf := pool.Get()
    buf = append(buf, "hello"...)
    s := pool.GetString() // zero-copy, mas perigoso!
    fmt.Println(s)
    pool.Put(buf)
}

Benchmark comparativo:

func BenchmarkSafe(b *testing.B) {
    pool := NewBufferPool(1024)
    for i := 0; i < b.N; i++ {
        buf := pool.Get()
        buf = append(buf, "data"...)
        s := string(buf) // cópia
        _ = s
        pool.Put(buf)
    }
}

func BenchmarkUnsafe(b *testing.B) {
    pool := NewBufferPool(1024)
    for i := 0; i < b.N; i++ {
        buf := pool.Get()
        buf = append(buf, "data"...)
        s := unsafe.String(unsafe.SliceData(buf), len(buf))
        _ = s
        pool.Put(buf)
    }
}

A versão unsafe elimina a alocação de string, mas exige que o buffer não seja modificado após a conversão. O ganho de performance (~30-50% em benchmarks) vem com o custo de complexidade e risco de bugs sutis.

Referências