Skip to content

unsafe

Endereço da documentação oficial: unsafe package - unsafe - Go Packages

A biblioteca padrão unsafe é uma biblioteca fornecida oficialmente para programação de baixo nível. As operações fornecidas por este pacote podem pular o sistema de tipos do Go para ler e escrever memória diretamente. Este pacote pode não ser portável, e o governo afirma que este pacote não está protegido pelos critérios de compatibilidade Go 1. Mesmo assim, unsafe ainda é usado em muitos projetos, incluindo bibliotecas padrão fornecidas oficialmente.

TIP

A razão pela qual não é portável é que alguns resultados de operações dependem da implementação do sistema operacional, e diferentes sistemas podem ter resultados diferentes.

ArbitraryType

go
type ArbitraryType int

Arbitrary pode ser traduzido como arbitrário, aqui representa qualquer tipo, e não é equivalente a any. Na verdade, este tipo não pertence ao pacote unsafe, aparece aqui apenas para fins de documentação.

IntegerType

go
type IntegerType int

IntegerType representa qualquer tipo de número inteiro. Na verdade, este tipo não pertence ao pacote unsafe, aparece aqui apenas para fins de documentação.

Não é necessário se preocupar muito com esses dois tipos acima, eles são apenas representantes. Ao usar as funções do pacote unsafe, o editor até avisará que o tipo não corresponde. O tipo real delas é o tipo específico que você passou.

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

Retorna o tamanho da variável x em bytes, não incluindo o tamanho do conteúdo referenciado. Por exemplo:

go
func main() {
  var ints byte = 1
  fmt.Println(unsafe.Sizeof(ints))

  var floats float32 = 1.0
  fmt.Println(unsafe.Sizeof(floats))

  var complexs complex128 = 1 + 2i
  fmt.Println(unsafe.Sizeof(complexs))

  var slice []int = make([]int, 100)
  fmt.Println(unsafe.Sizeof(slice))

  var mp map[string]int = make(map[string]int, 0)
  fmt.Println(unsafe.Sizeof(mp))

  type person struct {
    name string
    age  int
  }
  fmt.Println(unsafe.Sizeof(person{}))

  type man struct {
    name string
  }
  fmt.Println(unsafe.Sizeof(man{}))
}
1
4
16
24
8
24
16

Offsetof

go
func Offsetof(x ArbitraryType) uintptr

Esta função é usada para representar o deslocamento de campos dentro de uma struct, então x deve ser um campo de struct, ou seja, o valor de retorno é o número de bytes entre o endereço inicial da struct e o endereço inicial do campo. Por exemplo:

go
func main() {
   type person struct {
      name string
      age  int
   }
   p := person{
      name: "aa",
      age:  11,
   }
   fmt.Println(unsafe.Sizeof(p))
   fmt.Println(unsafe.Offsetof(p.name))
   fmt.Println(unsafe.Sizeof(p.name))
   fmt.Println(unsafe.Offsetof(p.age))
   fmt.Println(unsafe.Sizeof(p.age))
}
24
0
16
16
8

Alignof

Se não entender o que é alinhamento de memória, pode ir para: Go Language Memory Alignment Explained - Juejin (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

O tamanho de alinhamento geralmente é o mínimo entre o tamanho da palavra da máquina em bytes e Sizeof. Por exemplo, em uma máquina amd64, o tamanho da palavra é 64 bits, ou seja, 8 bytes. Por exemplo:

go
func main() {
  type person struct {
    name string
    age  int32
  }
  p := person{
    name: "aa",
    age:  11,
  }
  fmt.Println(unsafe.Alignof(p), unsafe.Sizeof(p))
  fmt.Println(unsafe.Alignof(p.name), unsafe.Sizeof(p.name))
  fmt.Println(unsafe.Alignof(p.age), unsafe.Sizeof(p.age))
}
go
8 24
8 16
4 4

Pointer

go
type Pointer *ArbitraryType

Pointer é um "ponteiro" que pode apontar para qualquer tipo, seu tipo é *ArbitraryType. Este tipo deve ser usado em conjunto com uintptr para realmente aproveitar o poder do pacote unsafe. Na descrição da documentação oficial, o tipo unsafe.Pointer pode realizar quatro operações especiais, respectivamente:

  • Qualquer tipo de ponteiro pode ser convertido para unsafe.Pointer
  • unsafe.Pointer pode ser convertido para qualquer tipo de ponteiro
  • uintptr pode ser convertido para unsafe.Pointer
  • unsafe.Pointer pode ser convertido para uintptr

Essas quatro operações especiais constituem a base de todo o pacote unsafe, e é exatamente através dessas quatro operações que se pode escrever código capaz de ignorar o sistema de tipos e acessar memória diretamente. Recomenda-se ter cuidado especial ao usar.

TIP

unsafe.Pointer não pode ser desreferenciado, e também não pode ter seu endereço tomado.

(1) Converter *T1 para unsafe.Pointer e depois converter para *T2

Existem os tipos *T1 e *T2. Supondo que T2 não seja maior que T1 e ambos tenham layout de memória equivalente, é permitido converter dados do tipo T2 para T1. Por exemplo:

go
func main() {
   fmt.Println(Float64bits(12.3))
   fmt.Println(Float64frombits(Float64bits(12.3)))
}

func Float64bits(f float64) uint64 {
   return *(*uint64)(unsafe.Pointer(&f))
}

func Float64frombits(b uint64) float64 {
   return *(*float64)(unsafe.Pointer(&b))
}
4623113902481840538
12.3

Estas duas funções são na verdade duas funções do pacote math. As mudanças de tipo no processo são as seguintes:

float64 -> *float64 -> unsafe.Pointer ->  *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64

(2) Converter unsafe.Pointer para uintptr

Ao converter unsafe.Pointer para uintptr, o endereço apontado pelo primeiro será usado como o valor do último. uintptr armazena um endereço, a diferença é que o primeiro é sintaticamente um ponteiro, uma referência, enquanto o último é apenas um valor inteiro. Por exemplo:

go
func main() {
   num := 1
   fmt.Println(unsafe.Pointer(&num))
   fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}
0xc00001c088
0xc00001c088

Uma diferença maior está no tratamento de coleta de lixo. Como unsafe.Pointer é uma referência, não será coletado quando necessário, enquanto o último é apenas um valor e naturalmente não terá esse tratamento especial. Outro ponto a ser notado é que quando o endereço do elemento apontado pelo ponteiro se move, o GC atualizará o endereço antigo referenciado pelo ponteiro, mas não atualizará o valor salvo em uintptr. Por exemplo, o código abaixo pode ter problemas:

go
func main() {
   num := 16
   address := uintptr(unsafe.Pointer(&num))
   np := (*int64)(unsafe.Pointer(address))
   fmt.Println(*np)
}

Em alguns casos, após o GC mover a variável, o endereço apontado por address já é inválido, e usar esse valor para criar um ponteiro causará panic:

panic: runtime error: invalid memory address or nil pointer dereference

Portanto, não é recomendado salvar o valor convertido de Pointer para uintptr.

(3) Converter uintptr para unsafe.Pointer

A maneira abaixo pode obter um ponteiro através de uintptr. Desde que o ponteiro seja válido, não haverá situação de endereço inválido como no exemplo dois. Pointer e ponteiros de tipo em si não suportam aritmética de ponteiros, mas uintptr é apenas um valor inteiro e pode realizar operações matemáticas. Realizar operações matemáticas em uintptr e depois converter para Pointer pode completar a aritmética de ponteiros.

go
p = unsafe.Pointer(uintptr(p) + offset)

Desta forma, pode-se acessar alguns elementos internos de alguns tipos apenas através de um ponteiro, como arrays e structs, independentemente de seus elementos internos estarem expostos externamente. Por exemplo:

go
func main() {
  type person struct {
    name string
    age  int32
  }
  p := &person{"jack", 18}
  pp := unsafe.Pointer(p)
  fmt.Println(*(*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.name))))
  fmt.Println(*(*int32)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.age))))

  s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  ps := unsafe.Pointer(&s[0])
  fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 8)))
  fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 16)))
}
jack
18
2

Add

go
func Add(ptr Pointer, len IntegerType) Pointer

Add retorna o Pointer atualizado com o deslocamento len, equivalente a Pointer(uintptr(ptr) + uintptr(len)):

go
Pointer(uintptr(ptr) + uintptr(len))

Por exemplo:

go
func main() {
   s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
   ps := unsafe.Pointer(&s[0])
   fmt.Println(*(*int)(unsafe.Add(ps, 8)))
   fmt.Println(*(*int)(unsafe.Add(ps, 16)))
}
2
3

SliceData

go
func SliceData(slice []ArbitraryType) *ArbitraryType

Esta função recebe um slice e retorna o endereço inicial de seu array subjacente. Se não usar SliceData, só é possível obter o endereço do array subjacente através do ponteiro de seu primeiro elemento, como abaixo:

go
func main() {
  nums := []int{1, 2, 3, 4}
  for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {
    num := *(*int)(p)
    fmt.Println(num)
  }
}

Claro, também é possível obter através do tipo reflect.SliceHeader, mas após a versão 1.20 já foi descontinuado. SliceData foi criado para substituí-lo. O exemplo de uso de SliceData é o seguinte:

go
func main() {
  nums := []int{1, 2, 3, 4}
  for p, i := unsafe.Pointer(unsafe.SliceData(nums)), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(int(0))), i+1 {
    num := *(*int)(p)
    fmt.Println(num)
  }
}

Slice

go
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

A função Slice recebe um ponteiro e um deslocamento de comprimento, e retorna a forma de slice desse segmento de memória. Não envolve cópia de memória durante o processo, e modificar o slice afetará diretamente os dados nesse endereço, e vice-versa. Geralmente é usado em conjunto com SliceData.

go
func main() {
  nums := []int{1, 2, 3, 4}
  numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))
  numsRef1[0] = 2
  fmt.Println(nums)
}
[2 2 3 4]

Modificar os dados do slice numsRef1 fará com que os dados de nums também mudem.

StringData

go
func StringData(str string) *byte

Mesma função que SliceData, mas como a demanda de conversão de string para slice de bytes é frequente, foi separada. O exemplo de uso é o seguinte:

go
func main() {
  str := "hello,world!"
  for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {
    char := *(*byte)(ptr)
    fmt.Println(string(char))
  }
}

Como literais de string são armazenados em segmentos somente leitura no processo, se tentar modificar os dados subjacentes da string aqui, o programa falhará diretamente com fatal. No entanto, para variáveis de string armazenadas na pilha/heap, é totalmente viável modificar seus dados subjacentes em tempo de execução.

String

go
func String(ptr *byte, len IntegerType) string

Mesma função que Slice, recebe um ponteiro de tipo byte e seu deslocamento de comprimento, e retorna sua forma de string. Não envolve cópia de memória durante o processo. Abaixo está um exemplo de conversão de slice de bytes para string:

go
func main() {
  bytes := []byte("hello world")
  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
  fmt.Println(str)
}

StringData e String não envolvem cópia de memória no processo de conversão entre string e slice de bytes, o desempenho é melhor do que a conversão direta de tipo, mas só se aplica a casos de somente leitura. Se planeja modificar dados, é melhor não usar isso.

Golang por www.golangdev.cn edit