Uint and uintptr in golang 非类型安全指针

2021年11月23日 阅读数:11
这篇文章主要向大家介绍Uint and uintptr in golang 非类型安全指针,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

 

 

Understanding uintptr in Golang - Welcome To Golang By Example https://golangbyexample.com/understanding-uintptr-golang/html

Overview

This is an unsigned integer type which is large enough to hold any pointer address. Therefore its size is platform dependent. It is just an integer representation of an address.git

 

Properties

  • A uintptr can be converted to unsafe.Pointer and viceversa. Later we will talk about where conversion of uintptr to unsafe.Pointer is useful.
  • Arithmetic can be performed on the uintptr. Do note here arithmetic cannot be performed in a pointer in Go or unsafe.Pointer in Go.
  • uintptr even though it holds a pointer address, is just a value and does not reference any object. Therefore
    • Its value will not be updated if the corresponding object moves. Eg When goroutine stack changes
    • The corresponding object can be garbage collected. The GC does not consider uintptr as live references and hence they can be garbage collected.

 

Purpose

uintptr can be used for below purposes:github

  • One purpose of uintptr is to be used along with unsafe.Pointer for unsafe memory access. Arithmetic operations cannot be performed on unsafe.Pointer. To perform such arithmetic
    • unsafe. Pointer is converted to uintptr
    • arithmetic is then performed on uintptr
    • uintptr is converted back to unsafe.Pointer  to access the object now pointed by the address

Be careful that the above steps should be atomic with respect to Garbage Collector, otherwise it could lead to issues. For eg after the first step 1, the referenced object is liable to be collection. If that happens then after step 3, the pointer will be an invalid Go pointer and can crash the program. Look at the unsafe package documentation.golang

https://golang.org/pkg/unsafe/#Pointerexpress

It lists down when the above conversion can be safe. See the below code for the scenario mentioned above.编程

In the below code we are doing arithmetic like below to get to address of field “b” in struct sample and then printing the value at that address. This below code is atomic with reference to the garbage collector.windows

p := unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.b))
package main
import (
    "fmt"
    "unsafe" ) type sample struct { a int b string } func main() { s := &sample{a: 1, b: "test"} //Getting the address of field b in struct s p := unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Offsetof(s.b)) //Typecasting it to a string pointer and printing the value of it fmt.Println(*(*string)(p)) }

Output:数组

test
  • Another purpose of uintptr is when you want to save the pointer address value for printing it or storing it. Since the address is just stored and does not reference anything, the corresponding object can be garbage collected.

See below code where we are converting an unsafe.Pointer to uintptr and printing it. Also, note as mentioned before too one the unsafe.Pointer is converted to uinptr, the reference is lost and the reference variable can be garbage collected.安全

package main

import (
    "fmt"
    "unsafe" ) type sample struct { a int b string } func main() { s := &sample{ a: 1, b: "test", } //Get the address as a uintptr startAddress := uintptr(unsafe.Pointer(s)) fmt.Printf("Start Address of s: %d\n", startAddress) }

Output:架构

The output will be dependent upon the machine as it is an address.

Start Address of s: 824634330992

 atomic.LoadUintptr() Function in Golang With Examples - GeeksforGeeks https://www.geeksforgeeks.org/atomic-loaduintptr-function-in-golang-with-examples/

 

In Go language, atomic packages supply lower-level atomic memory that is helpful is implementing synchronization algorithms. The LoadUintptr() function in Go language is used to atomically load *addr. This function is defined under the atomic package. Here, you need to import “sync/atomic” package in order to use these functions.

Syntax:

func LoadUintptr(addr *uintptr) (val uintptr)

Here, addr indicates address.

Note: (*uintptr) is the pointer to a uintptr value. And uintptr is an integer type that is too large that it can contain the bit pattern of any pointer.

Return value: It returns the value loaded to the *addr.

 

 
 

 

Example 1:

 
// Program to illustrate the usage of
// LoadUintptr function in Golang
  
// Including main package
package main
  
// importing fmt and sync/atomic
import (
     "fmt"
     "sync/atomic"
)
  
// Main function
func main() {
  
     // Assigning values
     // to the uintptr
     var (
         i uintptr = 98
         j uintptr = 255
         k uintptr = 6576567667788
         l uintptr = 5
     )
  
     // Calling LoadUintptr method
     // with its parameters
     load_1 := atomic.LoadUintptr(&i)
     load_2 := atomic.LoadUintptr(&j)
     load_3 := atomic.LoadUintptr(&k)
     load_4 := atomic.LoadUintptr(&l)
  
     // Displays uintptr value
     // loaded in the *addr
     fmt.Println(load_1)
     fmt.Println(load_2)
     fmt.Println(load_3)
     fmt.Println(load_4)
}

Output:

98
255
6576567667788
5

Example 2:

 
// Program to illustrate the usage of
// LoadUintptr function in Golang
  
// Including main package
package main
  
// Importing fmt and sync/atomic
import (
     "fmt"
     "sync/atomic"
)
  
// Main function
func main() {
  
     // Declaring u
     var u uintptr
  
     // For loop
     for i := 1; i < 1000; i += 1 {
  
         // Function with
         // AddUintptr method
         go func() {
             atomic.AddUintptr(&u, 9)
         }()
     }
  
     // Prints loaded values address
     fmt.Println(atomic.LoadUintptr(&u))
}

Output:

1818   // A random value is returned in each run

In the above example, the new values are returned from AddUintptr() method in each call until the loop stops, LoadUintptr() method loads these new uintptr values. And these values are stored in different addresses which can be random one so, the output of the LoadUintptr() method here in each run is different. So, here a random value is returned in the output.

 

go - Uint and uintptr in golang - Stack Overflow https://stackoverflow.com/questions/69182402/uint-and-uintptr-in-golang

 非类型安全指针 - Go语言101(通俗版Go白皮书) https://gfw.go101.org/article/unsafe.html

 unsafe - The Go Programming Language https://golang.google.cn/pkg/unsafe/#Pointer

type Pointer

Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:

- A pointer value of any type can be converted to a Pointer.
- A Pointer can be converted to a pointer value of any type.
- A uintptr can be converted to a Pointer.
- A Pointer can be converted to a uintptr.

Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care.

The following patterns involving Pointer are valid. Code not using these patterns is likely to be invalid today or to become invalid in the future. Even the valid patterns below come with important caveats.

Running "go vet" can help find uses of Pointer that do not conform to these patterns, but silence from "go vet" is not a guarantee that the code is valid.

(1) Conversion of a *T1 to Pointer to *T2.

Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, this conversion allows reinterpreting data of one type as data of another type. An example is the implementation of math.Float64bits:

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

(2) Conversion of a Pointer to a uintptr (but not back to Pointer).

Converting a Pointer to a uintptr produces the memory address of the value pointed at, as an integer. The usual use for such a uintptr is to print it.

Conversion of a uintptr back to Pointer is not valid in general.

A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.

The remaining patterns enumerate the only valid conversions from uintptr to Pointer.

(3) Conversion of a Pointer to a uintptr and back, with arithmetic.

If p points into an allocated object, it can be advanced through the object by conversion to uintptr, addition of an offset, and conversion back to Pointer.

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

The most common use of this pattern is to access fields in a struct or elements of an array:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

It is valid both to add and to subtract offsets from a pointer in this way. It is also valid to use &^ to round pointers, usually for alignment. In all cases, the result must continue to point into the original allocated object.

Unlike in C, it is not valid to advance a pointer just beyond the end of its original allocation:

// INVALID: end points outside allocated space.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

// INVALID: end points outside allocated space.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))

Note that both conversions must appear in the same expression, with only the intervening arithmetic between them:

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)

Note that the pointer must point into an allocated object, so it may not be nil.

// INVALID: conversion of nil pointer
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

(4) Conversion of a Pointer to a uintptr when calling syscall.Syscall.

The Syscall functions in package syscall pass their uintptr arguments directly to the operating system, which then may, depending on the details of the call, reinterpret some of them as pointers. That is, the system call implementation is implicitly converting certain arguments back from uintptr to pointer.

If a pointer argument must be converted to uintptr for use as an argument, that conversion must appear in the call expression itself:

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

The compiler handles a Pointer converted to a uintptr in the argument list of a call to a function implemented in assembly by arranging that the referenced allocated object, if any, is retained and not moved until the call completes, even though from the types alone it would appear that the object is no longer needed during the call.

For the compiler to recognize this pattern, the conversion must appear in the argument list:

// INVALID: uintptr cannot be stored in variable
// before implicit conversion back to Pointer during system call.
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

(5) Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.

Package reflect's Value methods named Pointer and UnsafeAddr return type uintptr instead of unsafe.Pointer to keep callers from changing the result to an arbitrary type without first importing "unsafe". However, this means that the result is fragile and must be converted to Pointer immediately after making the call, in the same expression:

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

As in the cases above, it is invalid to store the result before the conversion:

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))

(6) Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.

As in the previous case, the reflect data structures SliceHeader and StringHeader declare the field Data as a uintptr to keep callers from changing the result to an arbitrary type without first importing "unsafe". However, this means that SliceHeader and StringHeader are only valid when interpreting the content of an actual slice or string value.

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

In this usage hdr.Data is really an alternate way to refer to the underlying pointer in the string header, not a uintptr variable itself.

In general, reflect.SliceHeader and reflect.StringHeader should be used only as *reflect.SliceHeader and *reflect.StringHeader pointing at actual slices or strings, never as plain structs. A program should not declare or allocate variables of these struct types.

// INVALID: a directly-declared header will not hold Data as a reference.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost
type Pointer *ArbitraryType

func Add

func Add(ptr Pointer, len IntegerType) Pointer

The function Add adds len to ptr and returns the updated pointer Pointer(uintptr(ptr) + uintptr(len)). The len argument must be of integer type or an untyped constant. A constant len argument must be representable by a value of type int; if it is an untyped constant it is given type int. The rules for valid uses of Pointer still apply.

 

非类型安全指针

咱们已经从Go中的指针一文中学习到关于指针的各类概念和规则。 从那篇文章中,咱们得知,相对于C指针,Go指针有不少限制。 好比,Go指针不支持算术运算,而且对于任意两个指针值,极可能它们不能转换到对方的类型。

事实上,在那篇文章中解释的指针的完整称呼应该为类型安全指针。 虽然类型安全指针有助于咱们轻松写出安全的代码,可是有时候施加在类型安全指针上的限制也确实致使咱们不能写出最高效的代码。

实际上,Go也支持限制较少的非类型安全指针。 非类型安全指针和C指针相似,它们都很强大,但同时也都很危险。 在某些情形下,经过非类型安全指针的帮助,咱们能够写出效率更高的代码; 但另外一方面,使用非类型安全指针也致使咱们可能轻易地写出潜在的不安全的代码,这些潜在的不安全点很难在它们产生危害以前被及时发现。

使用非类型安全指针的另一个较大的风险是Go中目前提供的非类型安全指针机制并不受到Go 1 兼容性保证的保护。 使用了非类型安全指针的代码可能从从此的某个Go版本开始将再也不能编译经过,或者运行行为发生了变化。

若是出于种种缘由,你确实但愿在你的代码中使用非类型安全指针,你不只须要提防上述风险,你还需遵照Go官方文档中列出的非类型安全指针使用模式,并清楚地知晓使用非类型安全指针带来的效果。不然,你很难使用非类型安全指针写出安全的代码。

关于unsafe标准库包

非类型安全指针在Go中为 一种特别的类型。 咱们必须引入 unsafe标准库包来使用非类型安全指针。 非类型安全指针 unsafe.Pointer被声明定义为:
type Pointer *ArbitraryType

固然,这不是一个普通的类型定义。这里的ArbitraryType仅仅是暗示unsafe.Pointer类型值能够被转换为任意类型安全指针(反之亦然)。换句话说,unsafe.Pointer相似于C语言中的void*

非类型安全指针是指底层类型为unsafe.Pointer的类型。

非类型安全指针的零值也使用预声明的nil标识符来表示。

在Go 1.17以前, unsafe标准库包只提供了三个函数:
  • func Alignof(variable ArbitraryType) uintptr。 此函数用来取得一个值在内存中的地址对齐保证(address alignment guarantee)。 注意,同一个类型的值作为结构体字段和非结构体字段时地址对齐保证多是不一样的。 固然,这和具体编译器的实现有关。对于目前的标准编译器,同一个类型的值作为结构体字段和非结构体字段时的地址对齐保证老是相同的。 gccgo编译器对这两种情形是区别对待的。
  • func Offsetof(selector ArbitraryType) uintptr。 此函数用来取得一个结构体值的某个字段的地址相对于此结构体值的地址的偏移。 在一个程序中,对于同一个结构体类型的不一样值的对应相同字段,此函数的返回值老是相同的。
  • func Sizeof(variable ArbitraryType) uintptr。 此函数用来取得一个值的尺寸(亦即此值的类型的尺寸)。 在一个程序中,对于同一个类型的不一样值,此函数的返回值老是相同的。
注意:
  • 这三个函数的返回值的类型均为内置类型uintptr。下面咱们将了解到uintptr类型的值能够转换为非类型安全指针(反之亦然)。
  • 尽管这三个函数之一的任何调用的返回结果在同一个编译好的程序中老是一致的,可是这样的一个调用在不一样架构的操做系统中(或者使用不一样的编译器编译时)的返回值多是不同的。
  • 这三个函数的调用老是在编译时刻被估值,估值结果为类型为uintptr的常量。
  • 传递给Offsetof函数的实参必须为一个字段选择器形式value.field。 此选择器能够表示一个内嵌字段,但此选择器的路径中不能包含指针类型的隐式字段
一个使用了这三个函数的例子:
package main

import "fmt"
import "unsafe" func main() { var x struct { a int64 b bool c string } const M, N = unsafe.Sizeof(x.c), unsafe.Sizeof(x) fmt.Println(M, N) // 16 32 fmt.Println(unsafe.Alignof(x.a)) // 8 fmt.Println(unsafe.Alignof(x.b)) // 1 fmt.Println(unsafe.Alignof(x.c)) // 8 fmt.Println(unsafe.Offsetof(x.a)) // 0 fmt.Println(unsafe.Offsetof(x.b)) // 8 fmt.Println(unsafe.Offsetof(x.c)) // 16 } 

 

下面是一个展现了上面提到的最后一个注意点的例子:
package main

import "fmt"
import "unsafe" func main() { type T struct { c string } type S struct { b bool } var x struct { a int64 *S T } fmt.Println(unsafe.Offsetof(x.a)) // 0 fmt.Println(unsafe.Offsetof(x.S)) // 8 fmt.Println(unsafe.Offsetof(x.T)) // 16 // 此行能够编译过,由于选择器x.c中的隐含字段T为非指针。 fmt.Println(unsafe.Offsetof(x.c)) // 16 // 此行编译不过,由于选择器x.b中的隐含字段S为指针。 //fmt.Println(unsafe.Offsetof(x.b)) // error // 此行能够编译过,可是它将打印出字段b在x.S中的偏移量. fmt.Println(unsafe.Offsetof(x.S.b)) // 0 } 

注意,上面程序中的注释所暗示的输出结果是此程序在AMD64架构上使用标准编译器1.17版本编译时的结果。

unsafe包提供的这三个函数看上去并不怎么危险。 它们的原型在之后的Go 1版本中几乎不可能会发生改变。 Rob Pike甚至曾经将这几个函数挪到其它包中。 unsafe包的危险性基本上来自于非类型安全指针。它们和C指针同样危险,这是Go安全指针想方设法设法去避免的。

Go 1.17引入了一个新类型和两个新函数。 此新类型为 IntegerType。它的定义以下。 此类型不表明着一个具体类型,它只是表示任意整数类型(有点泛型的意思)。
type IntegerType int
Go 1.17引入的两个函数为:
  • func Add(ptr Pointer, len IntegerType) Pointer。 此函数在一个(非安全)指针表示的地址上添加一个偏移量,而后返回表示新地址的一个指针。 此函数以一种更正规的形式部分地覆盖了下面将要介绍的使用模式3中展现的合法用法。
  • func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType。 此函数用来从一个任意(安全)指针派生出一个指定长度的切片。
Go 1.17引入的这两个函数具备必定的危险性,需谨慎使用。下面时使用了这两个函数的一个例子。
package main

import (
	"fmt"
	"unsafe" ) func main() { a := [16]int{3: 3, 9: 9, 11: 11} fmt.Println(a) eleSize := int(unsafe.Sizeof(a[0])) p9 := &a[9] up9 := unsafe.Pointer(p9) p3 := (*int)(unsafe.Add(up9, -6 * eleSize)) fmt.Println(*p3) // 3 s := unsafe.Slice(p9, 5)[:3] fmt.Println(s) // [9 0 11] fmt.Println(len(s), cap(s)) // 3 5 t := unsafe.Slice((*int)(nil), 0) fmt.Println(t == nil) // true // 下面是两个不正确的调用。由于它们 // 的返回结果引用了未知的内存块。 _ = unsafe.Add(up9, 7 * eleSize) _ = unsafe.Slice(p9, 8) } 

非类型安全指针相关的类型转换

目前(Go 1.17),Go支持下列和非类型安全指针相关的类型转换:
  • 一个类型安全指针值能够被显式转换为一个非类型安全指针类型,反之亦然。
  • 一个uintptr值能够被显式转换为一个非类型安全指针类型,反之亦然。 可是,注意,一个nil非类型安全指针类型不该该被转换为uintptr并进行算术运算后再转换回来。

经过使用这些转换规则,咱们能够将任意两个类型安全指针转换为对方的类型,咱们也能够将一个安全指针值和一个uintptr值转换为对方的类型。

然而,尽管这些转换在编译时刻是合法的,可是它们中一些在运行时刻并不是是合法和安全的。 这些转换摧毁了Go的类型系统(不包括非类型安全指针部分)精心设立的内存安全屏障。 咱们必须遵循本文后面要介绍的一些用法指示来使用非类型安全指针才能写出合法并安全的代码。

咱们须要知道的一些事实

在开始介绍合法的非类型安全指针使用模式以前,咱们须要知道一些事实。

事实一:非类型安全指针值是指针但uintptr值是整数

每个非零安全或者不安全指针值均引用着另外一个值。可是一个uintptr值并不引用任何值,它被看做是一个整数,尽管经常它存储的是一个地址的数字表示。

Go是一门支持垃圾回收的语言。 当一个Go程序在运行中,Go运行时(runtime)将不时地检查哪些内存块将再也不被程序中的任何仍在使用中的值所引用而且回收这些内存块。 指针在这一过程当中扮演着重要的角色。值与值之间和内存块与值之间的引用关系是经过指针来表征的。

既然一个uintptr值是一个整数,那么它能够参与算术运算。

下一节中的例子将展现指针和uintptr值的不一样。

事实二:再也不被使用的内存块的回收时间点是不肯定的

在运行时刻,一次新的垃圾回收过程可能在一个不肯定的时间启动,而且此过程可能须要一段不肯定的时长才能完成。 因此一个再也不被使用的内存块的回收时间点是不肯定的。

一个例子:
import "unsafe"

// 假设此函数不会被内联(inline)。
//go:noinline
func createInt() *int { return new(int) } func foo() { p0, y, z := createInt(), createInt(), createInt() var p1 = unsafe.Pointer(y) // 和y同样引用着同一个值 var p2 = uintptr(unsafe.Pointer(z)) // 此时,即便z指针值所引用的int值的地址仍旧存储 // 在p2值中,可是此int值已经再也不被使用了,因此垃圾 // 回收器认为能够回收它所占据的内存块了。另外一方面, // p0和p1各自所引用的int值仍旧将在下面被使用。 // uintptr值能够参与算术运算。 p2 += 2; p2--; p2-- *p0 = 1 // okay *(*int)(p1) = 2 // okay *(*int)(unsafe.Pointer(p2)) = 3 // 危险操做! } 

在上面这个例子中,值p2仍旧在使用这个事实并不能保证曾经被z指针值所引用的int值所占的内存块必定尚未被回收。 换句话说,当*(*int)(unsafe.Pointer(p2)) = 3被执行的时候,此内存块有可能已经被回收了。 因此,继续经过解引用值p2中存储的地址是很是危险的,由于此内存块可能已经被从新分配给其它值使用了。

事实三:一个值的地址在程序运行中可能改变

详情请阅读内存块一文(见连接所指一节的尾部)。 这里咱们只须要知道当一个协程的栈的大小改变时,开辟在此栈上的内存块须要移动,从而相应的值的地址将改变。

事实四:一个值的生命范围可能并无代码中看上去的大

好比中下面这个例子,值t仍旧在使用中并不能保证被值t.y所引用的值仍在被使用。

type T struct {
	x int
	y *[1<<23]byte } func bar() { t := T{y: new([1<<23]byte)} p := uintptr(unsafe.Pointer(&t.y[0])) ... // 使用t.x和t.y // 一个聪明的编译器可以觉察到值t.y将不会再被用到, // 因此认为t.y值所占的内存块能够被回收了。 *(*byte)(unsafe.Pointer(p)) = 1 // 危险操做! println(t.x) // ok。继续使用值t,但只使用t.x字段。 } 

 

事实五:*unsafe.Pointer是一个类型安全指针类型

是的,类型*unsafe.Pointer是一个类型安全指针类型。 它的基类型为unsafe.Pointer。 既然它是一个类型安全指针类型,根据上面列出的类型转换规则,它的值能够转换为类型unsafe.Pointer,反之亦然。

一个例子:
package main

import "unsafe"

func main() { x := 123 // 类型为int p := unsafe.Pointer(&x) // 类型为unsafe.Pointer pp := &p // 类型为*unsafe.Pointer p = unsafe.Pointer(pp) pp = (*unsafe.Pointer)(p) } 

如何正确地使用非类型安全指针?

unsafe标准库包的文档中列出了六种非类型安全指针的使用模式。 下面将对它们逐一进行讲解。

使用模式一:将类型*T1的一个值转换为非类型安全指针值,而后将此非类型安全指针值转换为类型*T2

利用前面列出的非类型安全指针相关的转换规则,咱们能够将一个*T1值转换为类型*T2,其中T1T2为两个任意类型。 然而,咱们只有在T1的尺寸不小于T2而且此转换具备实际意义的时候才应该实施这样的转换。

经过将一个*T1值转换为类型*T2,咱们也能够将一个T1值转换为类型T2

一个这样的例子是 math标准库包中的 Float64bits函数。 此函数将一个 float64值转换为一个 uint64值。 在此转换过程当中,此 float64值在内存中的每一个位(bit)都保持不变。 函数 math.Float64frombits为此转换的逆转换。
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) } func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) } 

请注意,函数调用math.Float64bits(aFloat64)的结果和显式转换uint64(aFloat64)的结果不一样。

在下面这个例子中,咱们使用此模式将一个 []MyString值和一个 []string值转换为对方的类型。 结果切片和被转换的切片将共享底层元素。(这样的转换是不可能经过安全的方式来实现的。)
package main

import (
	"fmt"
	"unsafe" ) func main() { type MyString string ms := []MyString{"C", "C++", "Go"} fmt.Printf("%s\n", ms) // [C C++ Go] // ss := ([]string)(ms) // 编译错误 ss := *(*[]string)(unsafe.Pointer(&ms)) ss[1] = "Rust" fmt.Printf("%s\n", ms) // [C Rust Go] // ms = []MyString(ss) // 编译错误 ms = *(*[]MyString)(unsafe.Pointer(&ss)) } 

固然,从Go 1.17开始,咱们也可使用unsafe.Slice((*string)(&ms[0]), len(ms))来实现此类型转换。

此模式在实践中的另外一个应用是将一个再也不使用的字节切片转换为一个字符串(从而避免对底层字节序列的一次开辟和复制)。以下例所示:
func ByteSlice2String(bs []byte) string { return *(*string)(unsafe.Pointer(&bs)) } 

此实现借鉴于strings标准库包中的Builder类型的String方法的实现。 字节切片的尺寸比字符串的尺寸要大,而且它们的底层结构相似,因此此转换(对于当前的主流Go编译器来讲)是安全的。 即便这样,此实现也只推荐在标准库中使用,而不推荐在用户代码中使用。 在用户代码中,最好尽可能使用文末提供的另外一种实现。

反过来,下面这个例子中的转换是非法的,由于字符串的尺寸比字节切片的尺寸小。
func String2ByteSlice(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) // 危险 } 

在后面的模式六中展现了一种合法的(无需复制底层字节序列便可)将一个字符串转换为字节切片的实现。

注意:当运用上面展现的使用非类型安全指针将一个字节切片转换为字符串的技巧时,请确保结果字符串在使用过程当中绝对不修改此字节切片中的字节值。

使用模式二:将一个非类型安全指针值转换为一个uintptr值,而后使用此uintptr值。

此模式不是颇有用。通常咱们将最终的转换结果uintptr值输出到日志中用来调试,可是有不少其它安全而且简洁的途径也能够实现此目的。

一个例子:
package main

import "fmt"
import "unsafe" func main() { type T struct{a int} var t T fmt.Printf("%p\n", &t) // 0xc6233120a8 println(&t) // 0xc6233120a8 fmt.Printf("%x\n", uintptr(unsafe.Pointer(&t))) // c6233120a8 } 

输出地址在每次运行中可能都会不一样。

使用模式三:将一个非类型安全指针转换为一个uintptr值,而后此uintptr值参与各类算术运算,再将算术运算的结果uintptr值转回非类型安全指针。

转换先后的非类型安全指针必须指向同一个内存块。一个例子:
package main

import "fmt"
import "unsafe" type T struct { x bool y [3]int16 } const N = unsafe.Offsetof(T{}.y) const M = unsafe.Sizeof(T{}.y[0]) func main() { t := T{y: [3]int16{123, 456, 789}} p := unsafe.Pointer(&t) // "uintptr(p) + N + M + M"为t.y[2]的内存地址。 ty2 := (*int16)(unsafe.Pointer(uintptr(p)+N+M+M)) fmt.Println(*ty2) // 789 } 

其实,对于这样地址加减运算,更推荐使用上面介绍的Go 1.17中引入的unsafe.Add函数来完成。

注意:在上面这个例子中,转换 unsafe.Pointer(uintptr(p) + N + M + M)不该该像下面这样被拆成两行。 请阅读下面的代码中的注释以获取缘由。
func main() { t := T{y: [3]int16{123, 456, 789}} p := unsafe.Pointer(&t) // ty2 := (*int16)(unsafe.Pointer(uintptr(p)+N+M+M)) addr := uintptr(p) + N + M + M // ...(一些其它操做) // 从这里到下一行代码执行以前,t值将再也不被任何值 // 引用,因此垃圾回收器认为它能够被回收了。一旦 // 它真地被回收了,下面继续使用t.y[2]值的曾经 // 的地址是非法和危险的!另外一个危险的缘由是 // t的地址在执行下一行以前可能改变(见事实三)。 // 另外一个潜在的危险是:若是在此期间发生了一些 // 操做致使协程堆栈大小改变的状况,则记录在addr // 中的地址将失效。 ty2 := (*int16)(unsafe.Pointer(addr)) fmt.Println(*ty2) } 

这样的bug是很是微妙和很难被觉察到的,而且爆发出来的概率是至关得低。 一旦这样的bug爆发出来,将很让人摸不到头脑。这也是使用非类型安全指针被认为是危险操做的缘由之一。

中间uintptr值能够参与&^清位运算来进行内存对齐计算,只要保证转换先后的非类型安全指针同时指向同一个内存块,整个转换就是合法安全的。

另外一个须要注意的细节是最好不要将一个内存块的结尾边界地址存储在一个(安全或非安全)指针中。 这样作将致使紧随着此内存块的另外一个内存块由于被引用而不会被垃圾回收掉,或者由于造成非法指针而致使程序崩溃(取决于具体编译器实现)。 请阅读这个问答以获取更多解释。

使用模式四:将非类型安全指针值转换为uintptr值并传递给syscall.Syscall函数调用。

经过对上一个使用模式的解释,咱们知道像下面这样含有uintptr类型的参数的函数定义是危险的。
// 假设此函数不会被内联。
func DoSomething(addr uintptr) { // 对处于传递进来的地址处的值进行读写... } 

上面这个函数是危险的缘由在于此函数自己不能保证传递进来的地址处的内存块必定没有被回收。 若是此内存块已经被回收了或者被从新分配给了其它值,那么此函数内部的操做将是非法和危险的。

然而, syscall标准库包中的 Syscall函数的原型为:
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) 

那么此函数是如何保证处于传递给它的地址参数值a1a2a3处的内存块在此函数执行过程当中必定没有被回收和被移动呢? 此函数没法作出这样的保证。事实上,是编译器作出了这样的保证。 这是syscall.Syscall这样的函数的特权。其它自定义函数没法享受到这样的待遇。

咱们能够认为编译器针对每一个syscall.Syscall函数调用中的每一个被转换为uintptr类型的非类型安全指针实参添加了一些指令,从而保证此非类型安全指针所引用着的内存块在此调用返回以前不会被垃圾回收和移动。

注意:在Go 1.15以前,类型转换表达式uintptr(anUnsafePointer)能够呈现为相关实参的子表达式。 可是,从Go 1.15开始,使用此模式的要求变得略加严格:相关实参必须呈现为uintptr(anUnsafePointer)这种形式。

下面这个调用是安全的:
syscall.Syscall(syscall.SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n)) 
但下面这个调用则是危险的:
u := uintptr(unsafe.Pointer(p)) // 被p所引用着的值在此时有可能会被回收掉, // 或者它的地址已经发生了改变。 syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n)) // 相关实参必须呈现为"uintptr(anUnsafePointer)" // 这种形式。事实上,Go 1.15以前,此调用是合法的; // 可是Go 1.15略改了一点规则。 syscall.Syscall(SYS_XXX, uintptr(uintptr(fd)), uint(uintptr(unsafe.Pointer(p))), uintptr(n)) 

 

再提醒一次,此使用模式不适用于其它自定义函数。

使用模式五:将reflect.Value.Pointer或者reflect.Value.UnsafeAddr方法的uintptr返回值当即转换为非类型安全指针。

reflect标准库包中的Value类型的PointerUnsafeAddr方法都返回一个uintptr值,而不是一个unsafe.Pointer值。 这样设计的目的是避免用户不引用unsafe标准库包就能够将这两个方法的返回值(若是是unsafe.Pointer类型)转换为任何类型安全指针类型。

这样的设计须要咱们将这两个方法的调用的uintptr结果当即转换为非类型安全指针。 不然,将出现一个短暂的可能致使处于返回的地址处的内存块被回收掉的时间窗。 此时间窗是如此短暂以致于此内存块被回收掉的概率很是之低,于是这样的编程错误形成的bug的重现概率亦十分得低。

好比,下面这个调用是安全的:
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) 
而下面这个调用是危险的:
u := reflect.ValueOf(new(int)).Pointer() // 在这个时刻,处于存储在u中的地址处的内存块 // 可能会被回收掉。 p := (*int)(unsafe.Pointer(u)) 

 

注意:此使用模式也适用于Windows系统中的syscall.Proc.Callsyscall.LazyProc.Call系统调用。

使用模式六:将一个reflect.SliceHeader或者reflect.StringHeader值的Data字段转换为非类型安全指针,以及其逆转换。

和上一小节中提到的一样的缘由,reflect标准库包中的SliceHeaderStringHeader类型的Data字段的类型被指定为uintptr,而不是unsafe.Pointer

咱们能够将一个字符串的指针值转换为一个*reflect.StringHeader指针值,从而能够对此字符串的内部进行修改。 相似地,咱们能够将一个切片的指针值转换为一个*reflect.SliceHeader指针值,从而能够对此切片的内部进行修改。

一个使用 reflect.StringHeader的例子:
package main

import "fmt"
import "unsafe" import "reflect" func main() { a := [...]byte{'G', 'o', 'l', 'a', 'n', 'g'} s := "Java" hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) hdr.Data = uintptr(unsafe.Pointer(&a)) hdr.Len = len(a) fmt.Println(s) // Golang // 如今,字符串s和切片a共享着底层的byte字节序列, // 从而使得此字符串中的字节变得能够修改。 a[2], a[3], a[4], a[5] = 'o', 'g', 'l', 'e' fmt.Println(s) // Google } 

 

一个使用了 reflect.SliceHeader的例子:
package main

import (
	"fmt"
	"unsafe" "reflect" ) func main() { a := [6]byte{'G', 'o', '1', '0', '1'} bs := []byte("Golang") hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) hdr.Data = uintptr(unsafe.Pointer(&a)) hdr.Len = 2 hdr.Cap = len(a) fmt.Printf("%s\n", bs) // Go bs = bs[:cap(bs)] fmt.Printf("%s\n", bs) // Go101 } 

 

通常说来,咱们只应该从一个已经存在的字符串值获得一个 *reflect.StringHeader指针, 或者从一个已经存在的切片值获得一个 *reflect.SliceHeader指针, 而不该该从一个 StringHeader值生成一个字符串,或者从一个 SliceHeader值生成一个切片。 好比,下面的代码是不安全的:
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte))) // 在此时刻,上一行代码中刚开辟的数组内存块已经再也不被任何值 // 所引用,因此它能够被回收了。 hdr.Len = 5 s := *(*string)(unsafe.Pointer(&hdr)) // 危险! 

 

下面是一个展现了如何经过使用非类型安全途径将一个字符串转换为字节切片的例子。 和使用类型安全途径进行转换不一样,使用非类型安全途径避免了复制一份底层字节序列。
package main

import (
	"fmt"
	"reflect" "strings" "unsafe" ) func String2ByteSlice(str string) (bs []byte) { strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) sliceHdr.Data = strHdr.Data sliceHdr.Cap = strHdr.Len sliceHdr.Len = strHdr.Len return } func main() { // str := "Golang" // 对于官方标准编译器来讲,上面这行将使str中的字节 // 开辟在不可修改内存区。因此这里咱们使用下面这行。 str := strings.Join([]string{"Go", "land"}, "") s := String2ByteSlice(str) fmt.Printf("%s\n", s) // Goland s[5] = 'g' fmt.Println(str) // Golang } 

 

reflect标准库包中SliceHeaderStringHeader类型的文档提到这两个结构体类型的定义不保证在之后的版本中不发生改变。好在目前的两个主流Go编译器(标准编译器和gccgo编译器)都承认当前版本中的定义。这也能够看做是使用非类型安全指针的另外一个(较低的)潜在风险。

注意:当使用上面展现的使用非类型安全指针将一个字符串转换为字节切片时,请确保结果此源字符串的生命期内务必不要修改结果字节切片中的字节值(上面的例子违背了此原则)。 事实上,更为推荐的是最好永远不要修改结果字节切片中的字节值。此非类型安全方式的目的主要是为了在局部感知范围内避免一次内存开辟,而不是一种通用的方式。

咱们可使用相似的实现(以下所示)来将一个字节切片转换为字符串。 此实现被模式一中展现的方法略为安全一些(可是也更慢一些)。
func ByteSlice2String(bs []byte) (str string) { sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) strHdr.Data = sliceHdr.Data strHdr.Len = sliceHdr.Len return } 

一样地,请确保结果此结果字符串的生命期内务必不要修改实参字节切片中的字节值。

最后,顺便举一个违背了模式三的使用原则的例子:
package main

import (
	"fmt"
	"reflect" "unsafe" ) func Example_Bad() *byte { var str = "godoc" hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) pbyte := (*byte)(unsafe.Pointer(hdr.Data + 2)) return pbyte // *pbyte == 'd' } func main() { fmt.Println(string(*Example_Bad())) } 
下面是两个正确的实现:
func Example_Good1() *byte { var str = "godoc" hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) pbyte := (*byte)(unsafe.Pointer( uintptr(unsafe.Pointer(hdr.Data)) + 2)) return pbyte } // 从Go 1.17开始也可使用此实现。 func Example_Good2() *byte { var str = "godoc" hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) pbyte := (*byte)(unsafe.Add(unsafe.Pointer(hdr.Data), 2)) return pbyte } 

上面这几个例子借鉴自Bryan C. Mills在slack中发表的一个留言。

总结一下

从上面解释中,咱们得知,对于某些情形,非类型安全机制能够帮助咱们写出运行效率更高的代码。 可是,使用非类型安全指针也使得咱们可能轻易地写出一些重现概率很是低的微妙的bug。 一个含有这样的bug的程序极可能在很长一段时间内都运行正常,可是忽然变得不正常甚至崩溃。 这样的bug很难发现和调试。

咱们只应该在不得不使用非类型安全机制的时候才使用它们。 特别地,当咱们使用非类型安全机制时,请务必遵循上面列出的使用模式。

重申一次,咱们应该知晓当前的非类型安全机制规则和使用模式可能在之后的Go版本中彻底失效。 固然,目前没有任何迹象代表这种变化将很快会来到。 可是,一旦发生这种变化,本文中列出的当前是正确的代码将变得再也不安全甚至编译不经过。 因此,在实践中,请尽可能保证可以将使用了非类型安全机制的代码轻松改成使用安全途径实现。

最后值得提一下的是,Go官方工具链1.14中加入了一个-gcflags=all=-d=checkptr编译器动态分析选项(在Windows平台上推荐使用工具链1.15+)。 当此选项被使用的时候,编译出的程序在运行时会监测到不少(但并不是全部)非类型安全指针的错误使用。一旦错误的使用被监测到,恐慌将产生。 感谢Matthew Dempsky实现了此特性

 

上一篇: Actor model
下一篇: 单元测试