必知必会系列-Unsafe

unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让我们尽可能的不要使用它,如果你使用它,看到了这个名字,也会想到尽可能的不要使用它,或者更小心的使用它。 使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺,因为它必然会有意无意中使用很多非公开的实现细节,而这些实现的细节在未来的Go语言中很可能会被改变

unsafe包被广泛地用于比较低级的包, 例如runtime、 os syscall *还有net包等,因为它们需要和操作系统密切配合,但是对于普通的程序一般是不需要使用unsafe包的

虽然这个包不安全,但是它也有它的优势,那就是可以绕过Go的内存安全机制,直接对内存进行读写,所以有时候因为性能的需要,会冒一些风险使用该包,对内存进行操作

Unsafe 主要函数

unsafe.Sizeof函数

以下是Sizeof的函数定义,它接收一个ArbitraryType类型的参数,返回一个uintptr类型的值。这里的ArbitraryType不用关心,他只是一个占位符,我们只需要知道它表示任何类型,也就是我们这个函数可以接收任意类型的数据。

go 复制代码
func Sizeof(x ArbitraryType) uintptr

Sizeof函数返回操作数在内存中的字节大小(返回该类型所占用的内存大小),参数可以是任意类型的表达式,但是它并不会对表达式进行求值。一个 Sizeof 函数调用是一个对应 uintptr 类型的常量表达式,因此返回的结果可以用作数组类型的长度大小,或者用作计算其他的常量。

Sizeof函数可以返回一个类型所占用的内存大小,这个大小只与类型有关,和类型对应的变量存储的内容大小无关,比如bool型占用一个字节、int8也占用一个字节。

go 复制代码
func main() {
    fmt.Println(unsafe.Sizeof("true"))                // 16
    fmt.Println(unsafe.Sizeof(true))                  // 1
    fmt.Println(unsafe.Sizeof(int8(0)))               // 1
    fmt.Println(unsafe.Sizeof(int16(10)))             // 2
    fmt.Println(unsafe.Sizeof(int32(10000000)))       // 4
    fmt.Println(unsafe.Sizeof(int64(10000000000000))) // 8
    fmt.Println(unsafe.Sizeof(int(1)))                // 8
    fmt.Println(unsafe.Sizeof(float64(0)))            // 8
}

Alignof 函数

Alignof返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐可以提高内存读写的性能。获取对齐值还可以使用反射包的函数,reflect.TypeOf(x).Align()

go 复制代码
func Alignof(x ArbitraryType) uintptr

对齐值一般是2^n,最大不会超过8

go 复制代码
func main() {
    var b bool
    var i8 int8
    var i16 int16
    var i64 int64
    var f32 float32
    var s string
    var m map[string]string
    var p *int32

    fmt.Println(unsafe.Alignof(b))   // 1
    fmt.Println(unsafe.Alignof(i8))  // 1
    fmt.Println(unsafe.Alignof(i16)) // 2
    fmt.Println(unsafe.Alignof(i64)) // 8
    fmt.Println(unsafe.Alignof(f32)) // 4
    fmt.Println(unsafe.Alignof(s))   // 8
    fmt.Println(unsafe.Alignof(m))   // 8
    fmt.Println(unsafe.Alignof(p))   // 8
}

Offsetof 函数

Offsetof函数只适用于struct结构体中的字段相对于结构体的内存位置偏移量。结构体的第一个字段的偏移量都是0。该方法同样可以使用反射实现 reflect.TypeOf(xx).Field(i).Offset()

go 复制代码
func main() {
    u := user{}

    fmt.Println(unsafe.Offsetof(u.b))  // 0
    fmt.Println(unsafe.Offsetof(u.i))  // 4
    fmt.Println(unsafe.Offsetof(u.j))  // 8
    fmt.Println(unsafe.Sizeof(u1))     // 16

}

type user struct {
    b byte
    i int32
    j int64
}

官方文档-简洁版

关系图

graph LR unitptr --> unsafe.Pointer unsafe.Pointer --> unitptr *T --> unsafe.Pointer unsafe.Pointer --> *T
go 复制代码
// 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.
  • *T:普通类型指针类型,用于传递对象地址,不能进行指针运算。
  • unsafe.poniter:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(需转换到某一类型的普通指针)
  • uintptr:用于指针运算,GC不把uintptr当指针,uintptr无法持有对象。uintptr类型的目标会被回收

三者关系unsafe.Pointer是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为uintptr进行指针运算,即uintptr是用来与unsafe.Pointer打配合,用于指针运算

官方支持的六种使用姿势

(1) Conversion of a *T1 to Pointer to *T2
go 复制代码
// 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 {
    eturn *(*uint64)(unsafe.Pointer(&f))
}
  • 待转换的目标T2内存占用不能比T1大
  • 两者内存布局需要保持一致

那么下面的两个结构体 满足转换条件吗?

go 复制代码
type Student struct {
    age   int32 // 4字节    
    score int64 // 8字节
}

type Person struct {
    score int64 // 8字节
    age   int32 // 4字节
}
(2) Conversion of a Pointer to a uintptr (but not back to Pointer).
go 复制代码
// 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.
//
  • 可以将 Pointer 类型转换为 uintptr 类型。不应该将 uinptr 转为 Pointer因为uinptr不是指针,表征的地址可能被GC销毁或者改变。即不要使用uintptr赋值给临时变量
go 复制代码
func main() {
   stu := student{age:1, score: 1}
   p := unsafe.Pointer(&stu)
   up := uintptr(p)
   stu=*(*student)(unsafe.Pointer(up)))    // 禁止!!!
}
(3) Conversion of a Pointer to a uintptr and back, with arithmetic.
go 复制代码
// (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)
  • 该方式一般是结合Offsetof 方法获取结构体中变量/数组中元素的地址, p = unsafe.Pointer(uintptr(p) + offset)

  • 指针计算的结果不要溢出 变量本身的地址

  • pointer must point into an allocated object, so it may not be nil.

(4) Conversion of a Pointer to a uintptr when calling syscall.Syscall.
go 复制代码
// (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))
  • syscall.Syscall 系统调用的时候可以允许传递一个 uintptr, 由编译器保证 uintptr 是安全的
(5) Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.
go 复制代码
// (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))
  • 在 reflect 包的 Value.Pointer() 和 Value.UnsafeAddr() 返回了uintptr, 需要立马转换为 Pointer

使用姿势: 需要立马将uintptr转换为unsafe.Pointer

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

(6) Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.
go 复制代码
// (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
  • SliceHeader 和 StringHeader 是 slice 和 string 的内部实现, 里面包含了一个uintptr类型的字段 Data
csharp 复制代码
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n
  • 不允许通过初始化reflect.StringHeader的方式去定义一个字符串

结构体内存布局分析

Go 在编译的时候会按照一定的规则自动进行内存对齐。之所以这么设计是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。如果不进行内存对齐的话,很可能就会增加CPU访问内存的次数。因为 CPU 访问内存时,并不是逐个字节访问,而是以字(word)为单位访问。比如 64位CPU的字长(word size)为8bytes,那么CPU访问内存的单位也是8字节,每次加载的内存数据也是固定的若干字长,如8words(64bytes)、16words(128bytes)等

对齐规则

  • 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
  • 结构体本身,对齐值必须为编译器默认对齐长度或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
  • 结合以上两点,可得若编译器默认对齐长度超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的

也就是说

  • 对于具体类型来说,对齐值=min(编译器默认对齐值(一般是8字节),类型大小Sizeof长度)。也就是在默认设置的对齐值和类型的内存占用大小之间,取最小值为该类型的对齐值。
  • struct在每个字段都内存对齐之后,其本身也要进行对齐,对齐值=min(默认对齐值(一般是8字节),字段最大类型长度)。这条也很好理解,struct的所有字段中,最大的那个类型的长度以及默认对齐值之间,取最小的那个。
go 复制代码
type test struct {
    a bool  // 1字节
    b int32 // 4字节
    c int8  // 1字节
    d int64 // 4字节
    e byte  // 1字节
}

成员对齐

  • 第一个成员 a

    • 类型为 bool
    • 对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 b

    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
  • 第三个成员 c

    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c...
  • 第四个成员 d

    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 其偏移量必须为 8 的整数倍。确定偏移量为 16,因此 9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五个成员 e

    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e...

整体对齐

在每个成员变量进行对齐后,整个结构体本身也要进行字节对齐,因为发现它可能并不是 2^n,显然不符合对齐的规则。根据结构体本身的对齐规则,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。结构体进行对齐

成员变量 类型 偏移量 自身占用
a bool 0 1
字节对齐 - 1 3
b int32 4 4
c int8 8 1
字节对齐 - 9 7
d int64 16 8
e byte 24 1
字节对齐 - 25 7
总占用大小 - - 32

可以看到上面构造的结构体 因为内存对齐产生了很多无用的空洞。在日常的编码过程中,我们可以通过合理地调整结构体的字段顺序,从而优化结构体的大小

相关实践

  • 在一些需要防止CacheLine 伪共享的时候,会进行特殊的字段对齐。例如sync.Pool中显式地添加pedding字段
go 复制代码
type poolLocal struct {
        poolLocalInternal
        // Prevents false sharing on widespread platforms with// 128 mod (cache line size) = 0 .
        pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
  • 结构体中需要频繁访问的字段可以放于结构体中的第一个

在访问结构体的第一个字段时,我们可以直接使用结构体的指针来访问第一个字段(结构体变量的内存地址就是其第一个字段的内存地址)。

如果要访问结构体的其他字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为访问第一个字段的机器代码更紧凑,速度更快。

下面的代码是标准库sync.Once中的使用示例,通过将常用字段放置在结构体的第一个位置上减少CPU要执行的指令数量,从而达到更快的访问效果。

stackoverflow.com/questions/5...

go 复制代码
// src/sync/once.go

// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
type Once struct {
    // done indicates whether the action has been performed.
    // It is first in the struct because it is used in the hot path.
  // The hot path is inlined at every call site. 
  // Placing done first allows more compact instructions on some architectures (amd64/386), 
  // and fewer instructions (to calculate offset) on other architectures. 
    done uint32
    m    Mutex
}

Unsafe 经典应用

实现结构体高效互转

go 复制代码
type Student struct {
    isBoy bool  // 1字节
    age   int32 // 4字节
    score int64 // 8字节

}

type Person struct {
    isBoy bool  // 1字节
    age   int32 // 4字节
    score int64 // 8字节
}

func StudentToPerson(stu *Student) *Person {
    return (*Person)(unsafe.Pointer(stu))
}

func TestStudentToPerson(t *testing.T) {
    stu := &Student{
        age:   2,
        score: 3,
        isBoy: true,
    }
    person := StudentToPerson(stu)
    fmt.Printf("stu: %v\nperson: %v", stu, person)
}

输出

可以看到能够输出结果符合预期

css 复制代码
=== RUN   TestStudentToPerson
stu: &{isBoy:true age:2 score:3}
person: &{isBoy:true age:2 score:3}

如果稍微调整一下结构体Person的内存布局呢?

go 复制代码
type Student struct {
    isBoy bool  // 1字节
    age   int32 // 4字节
    score int64 // 8字节
}
// fmt.Printf("stu: %v\n",unsafe.Sizeof(Student{}))  16

type Person struct {
    age   int32 // 4字节
    score int64 // 8字节
    isBoy bool // 1字节
}

// fmt.Printf("stu: %v\n",unsafe.Sizeof(Person{})) 24

结果输出会是怎样的呢?

go 复制代码
=== RUN   TestStudentToPerson
stu: &{isBoy:true age:2 score:3}
person: &{age:1 score:3 isBoy:true}

我们惊讶地发现程序并没有报错,但是结果已经不符合预期了(age字段的赋值错误)

当知道这两者内存布局的差异便能够解释这个现象了~

相关推荐
煎鱼eddycjy14 小时前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy14 小时前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲1 天前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星2 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架3 天前
golang高频面试真题
面试·go
郝同学的测开笔记3 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声3 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星5 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星6 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_7 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin