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
type ArbitraryType intArbitrary 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
type IntegerType intIntegerType 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
func Sizeof(x ArbitraryType) uintptrRetorna o tamanho da variável x em bytes, não incluindo o tamanho do conteúdo referenciado. Por exemplo:
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
16Offsetof
func Offsetof(x ArbitraryType) uintptrEsta 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:
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
8Alignof
Se não entender o que é alinhamento de memória, pode ir para: Go Language Memory Alignment Explained - Juejin (juejin.cn)
func Alignof(x ArbitraryType) uintptrO 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:
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))
}8 24
8 16
4 4Pointer
type Pointer *ArbitraryTypePointer é 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.Pointerpode ser convertido para qualquer tipo de ponteirouintptrpode ser convertido paraunsafe.Pointerunsafe.Pointerpode ser convertido parauintptr
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:
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.3Estas 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:
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088Uma 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:
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 dereferencePortanto, 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.
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:
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
2Add
func Add(ptr Pointer, len IntegerType) PointerAdd retorna o Pointer atualizado com o deslocamento len, equivalente a Pointer(uintptr(ptr) + uintptr(len)):
Pointer(uintptr(ptr) + uintptr(len))Por exemplo:
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
3SliceData
func SliceData(slice []ArbitraryType) *ArbitraryTypeEsta 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:
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:
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
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypeA 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.
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
func StringData(str string) *byteMesma 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:
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
func String(ptr *byte, len IntegerType) stringMesma 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:
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.
