Golang不安全类型转换和内存访问

插图由 玛丽亚·莱塔(MariaLetta / free-gophers-pack)组成 ,作者是蕾妮·法兰克(Renee French)。

警告! 您应该避免在应用程序中使用不安全的方法,除非您100%确定自己在做什么。 出于某种原因,它被称为不安全。

Go是强类型的,您必须将变量从一种类型转换为另一种类型,才能在应用程序的不同部分中使用。 但是有时您需要绕过这种类型的安全性。 在每个滴答声都很重要的高负载系统中,可能需要优化瓶颈。 不安全的操作可能会使许多分配安全。 不安全还允许入侵任何结构字段,包括切片,字符串,映射等。

更改字符串示例

字符串在Go中是不可变的。 如果要更改字符串,通常必须分配一个新字符串。 让我们用不安全的软件包破解一点,然后实际更改字符串!

package main

import (
	"fmt"
	"reflect"
	"time"
	"unsafe"
)

func main () {
	a := "Hello. Current time is " + time.Now().String()

	fmt.Println(a)

	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&a))
	*(* byte )(unsafe.Pointer(stringHeader.Data + 5 )) = '!'

	fmt.Println(a)
}

结果:

Hello. Current time is 2020-03-14 21:40:38.36328248 +0300 +03 m=+0.000037994
Hello! Current time is 2020-03-14 21:40:38.36328248 +0300 +03 m=+0.000037994

字符串已更改!

代码段-https: //play.golang.org/p/QJ6hIMz6_gg

这段代码中发生了什么? 步骤如下:

  1. 将字符串指针转换为unsafe.Pointer 。 任何指针都可以转换为unsafe.Pointer ,反之亦然。
    unsafe.Pointer也可以转换为uintptr整数形式的地址。
  2. unsafe.Pointer转换为reflect.StringHeader指针,以获取Data指针。
    reflect.StringHeader类型反映字符串内部运行时结构。 在uintptr字符串由长度值和指向数据的uintptr指针表示。
  3. 将指针向前移动5个字节。 使用uintptr可以执行指针算术。
  4. 将新的uintptn转换为unsafe.Pointer ,然后将其转换为byte指针。 该字节实际上是字符串的一部分。
  5. 通过指针将新值分配给字节。 字符串已更改!

顺便说一句,以下代码将产生细分违规错误。 Go编译器了解字符串是常量,并将其与其他不允许更改的常量一起放入内存。 这就是为什么我在第一个示例中使用time.Now()的原因。

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main () {
	a := "Hello. Have a nice day!"

	fmt.Println(a)

	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&a))
	*(* byte )(unsafe.Pointer(stringHeader.Data + 5 )) = '!'

	fmt.Println(a)
}

结果:

Hello. Have a nice day!
unexpected fault address 0x4c3e31
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x4c3e31 pc=0x48cf33]

代码段-https: //play.golang.org/p/ix70l38QvA4

包裹不安全

https://golang.org/pkg/unsafe/
软件包仅包含3个功能和单个可用类型。

func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type Pointer *ArbitraryType

type ArbitraryType
// represents the type of an arbitrary Go expression,
// not actually part of a package

Offsetof给出struct中字段的偏移量。 Sizeof是内存中变量的大小,不包括引用的内存。 Alignof提供有关变量地址对齐的信息。

使用unsafe.Offsetofunsafe.Sizeof函数的一些示例:

  1. 不安全的数组迭代和类型转换快速分配为零。
  2. 获取有关内存中结构实际大小的信息。
  3. 使用结构指针和字段偏移量直接在内存中更改结构字段。
package main

import (
	"fmt"
	"unsafe"
)

type Bytes400 struct {
	val [ 100 ] int32
}

type TestStruct struct {
	a [ 9 ] int64
	b byte
	c *Bytes400
	d int64
}

func main () {
	array := [ 10 ] uint64 { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }

	var sum int8

	// Unsafe array iteration
	sizeOfUint64 := unsafe.Sizeof(array[ 0 ])
	for i := uintptr ( 0 ); i < 10 ; i++ {
		sum += *(* int8 )(unsafe.Pointer( uintptr (unsafe.Pointer(&array)) + sizeOfUint64*i))
	}
	fmt.Println(sum)

	// Size of struct and offsets of struct fields
	t := TestStruct{b: 42 }
	fmt.Println(unsafe.Sizeof(t))
	fmt.Println(unsafe.Offsetof(t.a), unsafe.Offsetof(t.b), unsafe.Offsetof(t.c), unsafe.Offsetof(t.d))

	fmt.Println(unsafe.Sizeof(Bytes400{}))

	// Change struct field t.b value
	*(* byte )(unsafe.Pointer( uintptr (unsafe.Pointer(&t)) + unsafe.Offsetof(t.b)))++
	fmt.Println(t.b)
}

结果:

55
88
0 72 76 80
400
43

代码段-https: //play.golang.org/p/cSmAUdu3sS9

指针算术和垃圾收集器

一个unsafe.Pointer可以转换为uintptr ,反之亦然。 执行指针算术的唯一方法是使用uintptr 。 一般规则是uintptr是没有指针语义的整数值。 将uintptr用作指向对象的唯一指针是不安全的。 垃圾回收器不知道它,并且对象内存可以重用。 算术运算完成后,最好立即将其转换回unsafe.Pointer

不安全字符串到[] byte转换的基准

package main

import (
	"reflect"
	"unsafe"
)

func SafeBytesToString (bytes [] byte ) string {
	return string (bytes)
}

func SafeStringToBytes (s string ) [] byte {
	return [] byte (s)
}

func UnsafeBytesToString (bytes [] byte ) string {
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))

	return *(* string )(unsafe.Pointer(&reflect.StringHeader{
		Data: sliceHeader.Data,
		Len:  sliceHeader.Len,
	}))
}

func UnsafeStringToBytes (s string ) [] byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
	return *(*[] byte )(unsafe.Pointer(&reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}))
}
go test -bench=.

基准结果:

BenchmarkSafeBytesToString-8       257141380       4.50 ns/op
BenchmarkSafeStringToBytes-8       227980887       5.38 ns/op
BenchmarkUnsafeBytesToString-8     1000000000      0.305 ns/op
BenchmarkUnsafeStringToBytes-8     1000000000      0.274 ns/op

不安全的转换速度快10倍以上! 但是此转换仅分配新的标头,数据保持不变。 因此,如果在转换后更改切片,则字符串也将更改。 以下测试将通过:

func TestUnsafeBytesToString (t *testing.T) {
	bs := [] byte ( "Test" )
	str := UnsafeBytesToString(bs)
	if str != "Test" {
		t.Fail()
	}

	// Test string mutation
	bs[ 0 ] = 't'
	if str != "test" {
		t.Fail()
	}
}

如果您确实需要加快转换速度或侵入go结构内部,请使用不安全的软件包。 在其他情况下,尽量避免它,将它弄乱也不难。

感谢您的阅读,并度过了愉快的一天!

回购代码示例:
https://github.com/kochetkov-av/go-unsafe-type-conversions

From: https://hackernoon.com/golang-unsafe-type-conversions-and-memory-access-odz3yrl

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值