Golang的unsafe包是一个很特殊的包。 为什么这样说呢? 本文将详细解释。

来自go语言官方文档的警告

unsafe包的文档是这么说的:

导入unsafe的软件包可能不可移植,并且不受Go 1兼容性指南的保护。

Go 1 兼容性指南这么说:

导入unsafe软件包可能取决于Go实现的内部属性。 我们保留对可能导致程序崩溃的实现进行更改的权利。

当然包名称暗示unsafe包是不安全的。 但这个包有多危险呢? 让我们先看看unsafe包的作用。

Unsafe包的作用

直到现在(Go1.7),unsafe包含以下资源:

  • 三个函数:

    • func Alignof(variable ArbitraryType)uintptr
    • func Offsetof(selector ArbitraryType)uintptr
    • func Sizeof(variable ArbitraryType)uintptr
  • 和一种类型:

    • 类型Pointer * ArbitraryType

这里,ArbitraryType不是一个真正的类型,它只是一个占位符。

与Golang中的大多数函数不同,上述三个函数的调用将始终在编译时求值,而不是运行时。 这意味着它们的返回结果可以分配给常量。

(BTW,unsafe包中的函数中非唯一调用将在编译时求值。当传递给len和cap的参数是一个数组值时,内置函数和cap函数的调用也可以在编译时被求值。)

除了这三个函数和一个类型外,指针在unsafe包也为编译器服务。

出于安全原因,Golang不允许以下之间的直接转换:

  • 两个不同指针类型的值,例如 int64和 float64。

  • 指针类型和uintptr的值。

但是借助unsafe.Pointer,我们可以打破Go类型和内存安全性,并使上面的转换成为可能。这怎么可能发生?让我们阅读unsafe包文档中列出的规则:

  • 任何类型的指针值都可以转换为unsafe.Pointer。
  • unsafe.Pointer可以转换为任何类型的指针值。
  • uintptr可以转换为unsafe.Pointer。
  • unsafe.Pointer可以转换为uintptr。

这些规则与Go规范一致:

底层类型uintptr的任何指针或值都可以转换为指针类型,反之亦然。

规则表明unsafe.Pointer类似于c语言中的void 。当然,void 在C语言里是危险的!

在上述规则下,对于两种不同类型T1和T2,可以使 T1值与unsafe.Pointer值一致,然后将unsafe.Pointer值转换为 T2值(或uintptr值)。通过这种方式可以绕过Go类型系统和内存安全性。当然,滥用这种方式是很危险的。

举个例子:

package main

import (
"fmt"
"unsafe"
)
func main() {
var n int64 = 5
var pn = &n
var pf = (*float64)(unsafe.Pointer(pn))
// now, pn and pf are pointing at the same memory address
fmt.Println(*pf) // 2.5e-323
*pf = 3.14159
fmt.Println(n) // 4614256650576692846
}

  

在这个例子中的转换可能是无意义的,但它是安全和合法的(为什么它是安全的?)。

因此,资源在unsafe包中的作用是为Go编译器服务,unsafe.Pointer类型的作用是绕过Go类型系统和内存安全。

再来一点 unsafe.Pointer 和 uintptr

这里有一些关于unsafe.Pointer和uintptr的事实:

  • uintptr是一个整数类型。

    • 即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收。
  • unsafe.Pointer是一个指针类型。
    • 但是unsafe.Pointer值不能被取消引用。
    • 如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收。
    • unsafe.Pointer是一个通用的指针类型,就像* int等。

由于uintptr是一个整数类型,uintptr值可以进行算术运算。 所以通过使用uintptr和unsafe.Pointer,我们可以绕过限制,* T值不能在Golang中计算偏移量:

package main

import (
"fmt"
"unsafe"
) func main() {
a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1])
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
*(*int)(p3) = 6
fmt.Println("a =", a) // a = [0 1 2 6] // ... type Person struct {
name string
age int
gender bool
} who := Person{"John", 30, true}
pp := unsafe.Pointer(&who)
pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
*pname = "Alice"
*page = 28
*pgender = false
fmt.Println(who) // {Alice 28 false}
}

  

unsafe包有多危险

关于unsafe包,Ian,Go团队的核心成员之一,已经确认:

  • 在unsafe包中的函数的签名将不会在以后的Go版本中更改,

  • 并且unsafe.Pointer类型将在以后的Go版本中始终存在。

所以,unsafe包中的三个函数看起来不危险。 go team leader甚至想把它们放在别的地方。 unsafe包中这几个函数唯一不安全的是它们调用结果可能在后来的版本中返回不同的值。 很难说这种不安全是一种危险。

看起来所有的unsafe包的危险都与使用unsafe.Pointer有关。 unsafe包docs列出了一些使用unsafe.Pointer合法或非法的情况。 这里只列出部分非法使用案例:

package main

import (
"fmt"
"unsafe"
) // case A: conversions between unsafe.Pointer and uintptr
// don't appear in the same expression
func illegalUseA() {
fmt.Println("===================== illegalUseA") pa := new([4]int) // split the legal use
// p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
// into two expressions (illegal use):
ptr := uintptr(unsafe.Pointer(pa))
p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
// "go vet" will make a warning for the above line:
// possible misuse of unsafe.Pointer // the unsafe package docs, https://golang.org/pkg/unsafe/#Pointer,
// thinks above splitting is illegal.
// but the current Go compiler and runtime (1.7.3) can't detect
// this illegal use.
// however, to make your program run well for later Go versions,
// it is best to comply with the unsafe package docs. *(*int)(p1) = 123
fmt.Println("*(*int)(p1) :", *(*int)(p1)) //
} // case B: pointers are pointing at unknown addresses
func illegalUseB() {
fmt.Println("===================== illegalUseB") a := [4]int{0, 1, 2, 3}
p := unsafe.Pointer(&a)
p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
// now p is pointing at the end of the memory occupied by value a.
// up to now, although p is invalid, it is no problem.
// but it is illegal if we modify the value pointed by p
*(*int)(p) = 123
fmt.Println("*(*int)(p) :", *(*int)(p)) // 123 or not 123
// the current Go compiler/runtime (1.7.3) and "go vet"
// will not detect the illegal use here. // however, the current Go runtime (1.7.3) will
// detect the illegal use and panic for the below code.
p = unsafe.Pointer(&a)
for i := 0; i <= len(a); i++ {
*(*int)(p) = 123 // Go runtime (1.7.3) never panic here in the tests fmt.Println(i, ":", *(*int)(p))
// panic at the above line for the last iteration, when i==4.
// runtime error: invalid memory address or nil pointer dereference p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
}
} func main() {
illegalUseA()
illegalUseB()
}

  

编译器很难检测Go程序中非法的unsafe.Pointer使用。 运行“go vet”可以帮助找到一些潜在的错误,但不是所有的都能找到。 同样是Go运行时,也不能检测所有的非法使用。 非法unsafe.Pointer使用可能会使程序崩溃或表现得怪异(有时是正常的,有时是异常的)。 这就是为什么使用不安全的包是危险的。

转换T1 为 T2

对于将 T1转换为unsafe.Pointer,然后转换为 T2,unsafe包docs说:

如果T2比T1大,并且两者共享等效内存布局,则该转换允许将一种类型的数据重新解释为另一类型的数据。

这种“等效内存布局”的定义是有一些模糊的。 看起来go团队故意如此。 这使得使用unsafe包更危险。

由于Go团队不愿意在这里做出准确的定义,本文也不尝试这样做。 这里,列出了已确认的合法用例的一小部分,

合法用例1:在[]T和[]MyT之间转换

在这个例子里,我们用int作为T:

type MyInt int

在Golang中,[] int和[] MyInt是两种不同的类型,它们的底层类型是自身。 因此,[] int的值不能转换为[] MyInt,反之亦然。 但是在unsafe.Pointer的帮助下,转换是可能的:

package main

import (
"fmt"
"unsafe"
) func main() {
type MyInt int a := []MyInt{0, 1, 2}
// b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
b := *(*[]int)(unsafe.Pointer(&a)) b[0]= 3 fmt.Println("a =", a) // a = [3 1 2]
fmt.Println("b =", b) // b = [3 1 2] a[2] = 9 fmt.Println("a =", a) // a = [3 1 9]
fmt.Println("b =", b) // b = [3 1 9]
}

  

合法用例2: 调用sync/atomic包中指针相关的函数

sync / atomic包中的以下函数的大多数参数和结果类型都是unsafe.Pointer或*unsafe.Pointer:

  • func CompareAndSwapPointer(addr * unsafe.Pointer,old,new unsafe.Pointer)(swapped bool)
  • func LoadPointer(addr * unsafe.Pointer)(val unsafe.Pointer)
  • func StorePointer(addr * unsafe.Pointer,val unsafe.Pointer)
  • func SwapPointer(addr * unsafe.Pointer,new unsafe.Pointer)(old unsafe.Pointer)

要使用这些功能,必须导入unsafe包。 注意: unsafe.Pointer是一般类型,因此 unsafe.Pointer的值可以转换为unsafe.Pointer,反之亦然。

package main

import (
"fmt"
"log"
"time"
"unsafe"
"sync/atomic"
"sync"
"math/rand"
) var data *string // get data atomically
func Data() string {
p := (*string)(atomic.LoadPointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
))
if p == nil {
return ""
} else {
return *p
}
} // set data atomically
func SetData(d string) {
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
unsafe.Pointer(&d),
)
} func main() {
var wg sync.WaitGroup
wg.Add(200) for range [100]struct{}{} {
go func() {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000) log.Println(Data())
wg.Done()
}()
} for i := range [100]struct{}{} {
go func(i int) {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
s := fmt.Sprint("#", i)
log.Println("====", s) SetData(s)
wg.Done()
}(i)
} wg.Wait() fmt.Println("final data = ", *data)
}

  

结论

  • unsafe包用于Go编译器,而不是Go运行时。
  • 使用unsafe作为程序包名称只是让你在使用此包是更加小心。
  • 使用unsafe.Pointer并不总是一个坏主意,有时我们必须使用它。
  • Golang的类型系统是为了安全和效率而设计的。 但是在Go类型系统中,安全性比效率更重要。 通常Go是高效的,但有时安全真的会导致Go程序效率低下。 unsafe包用于有经验的程序员通过安全地绕过Go类型系统的安全性来消除这些低效。
  • unsafe包可能被滥用并且是危险的。

原文:https://gocn.vip/question/371

[转]Go里面的unsafe包详解的更多相关文章

  1. Android Service生命周期 Service里面的onStartCommand()方法详解

    在Demo上,Start一个Service之后,执行顺序:onCreate - > onStartCommand 然后关闭应用,会重新执行上面两步. 但是把代码拷贝到游戏工程发现,关闭游戏后,只 ...

  2. django的views里面的request对象详解大全

    简介 HTTP 应用的信息是通过 请求报文 和 响应报文 传递的,关于更多的相关知识,可以阅读<HTTP权威指南>获得. 其中 请求报文 由客户端发送,其中包含和许多的信息,而 djang ...

  3. python里面的xlrd模块详解(一)

    那我就一下面积个问题对xlrd模块进行学习一下: 1.什么是xlrd模块? 2.为什么使用xlrd模块? 3.怎样使用xlrd模块? 1.什么是xlrd模块? python操作excel主要用到xlr ...

  4. vue 里面的watch 选项详解

    早就想好好写写这个watch了,一直顾不上,因为想深刻的记录一下,其实这些东西只要是好好看看官网的说明,都在里面呢. 虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器.这就是为什么 V ...

  5. python里面的xlrd模块详解

    那我就一下面积个问题对xlrd模块进行学习一下: 1.什么是xlrd模块? 2.为什么使用xlrd模块? 3.怎样使用xlrd模块? 1.什么是xlrd模块? ♦python操作excel主要用到xl ...

  6. Spring 3.x jar 包详解 与 依赖关系(转)

    以下的内容我会持续更新(当然是我有新发现的时候); 以下内容是我在网上搜索.整理.修改的而成的内容.由于很多内容都是转载了,无法追溯到源头,因此无法一一对原作者进行道谢. 这几天,我查阅大量的官方的文 ...

  7. 常见 jar包详解

        常见 jar包详解 jar包 用途 axis.jar SOAP引擎包 commons-discovery-0.2.jar 用来发现.查找和实现可插入式接口,提供一些一般类实例化.单件的生命周期 ...

  8. Spring各个jar包详解

    Spring各jar包详解 spring.jar 是包含有完整发布模块的单个jar 包.但是不包括mock.jar,aspects.jar, spring-portlet.jar, and sprin ...

  9. 最全面的Android Webview详解

    转自:最全面的Android Webview详解 前言 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝.京东.聚划算等等,如下图  那么这种该如何实现呢?其实这是 ...

随机推荐

  1. 机器学习---最小二乘线性回归模型的5个基本假设(Machine Learning Least Squares Linear Regression Assumptions)

    在之前的文章<机器学习---线性回归(Machine Learning Linear Regression)>中说到,使用最小二乘回归模型需要满足一些假设条件.但是这些假设条件却往往是人们 ...

  2. Vue针对性笔记

    Github原文阅读 MVVM(Model-View-ViewModel)模型 MVVM分为Model.View.ViewModel三部分. Model代表数据模型,定义数据和业务逻辑,访问数据层 V ...

  3. 详解最大似然估计(MLE)、最大后验概率估计(MAP),以及贝叶斯公式的理解

    转载声明:本文为转载文章,发表于nebulaf91的csdn博客.欢迎转载,但请务必保留本信息,注明文章出处. 原文作者: nebulaf91 原文原始地址:http://blog.csdn.net/ ...

  4. JDK环境部署

    JDK环境部署 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 说起JDK想必大家并不陌生,作为运维的小伙伴,谁不层接触过Java程序员呢?而且在搭建服务上时也必须得接触他,比如to ...

  5. I/O模型系列之三:IO通信模型BIO NIO AIO

    一.传统的BIO 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请 ...

  6. Angular记录(6)

    文档资料 箭头函数--MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_fun ...

  7. IDEA+Maven配置MyBatis的初体验(坑点总结)

    起因 在老师的推荐下,我跟着这篇文章https://www.jb51.net/article/70923.htm尝试MyBatis的初次调试运行.途中困难重重,由于教程中的开发环境是eclipse,项 ...

  8. ES6.3.2 副本失败处理

    ES6.3.2 副本失败处理 副本的失败处理对理解ES的数据副本模型很有帮助.在ES6.3.2 index操作源码流程的总结中提到:ES的写操作会先写主分片,然后主分片再将操作同步到副本分片.本文给出 ...

  9. hibernate自定义校验Valid

    步骤: 1.定义注解: import javax.validation.Constraint; import javax.validation.Payload; import java.lang.an ...

  10. 深入理解display属性——前端布局常用重要属性

    小注:display属性在网页布局中非常常见,但经常用到的仅仅是block.inline-block.inline和none等寥寥几个属性值,下面本人将详细介绍display属性的各个方面 定义 di ...