[转]Go里面的unsafe包详解
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包详解的更多相关文章
- Android Service生命周期 Service里面的onStartCommand()方法详解
在Demo上,Start一个Service之后,执行顺序:onCreate - > onStartCommand 然后关闭应用,会重新执行上面两步. 但是把代码拷贝到游戏工程发现,关闭游戏后,只 ...
- django的views里面的request对象详解大全
简介 HTTP 应用的信息是通过 请求报文 和 响应报文 传递的,关于更多的相关知识,可以阅读<HTTP权威指南>获得. 其中 请求报文 由客户端发送,其中包含和许多的信息,而 djang ...
- python里面的xlrd模块详解(一)
那我就一下面积个问题对xlrd模块进行学习一下: 1.什么是xlrd模块? 2.为什么使用xlrd模块? 3.怎样使用xlrd模块? 1.什么是xlrd模块? python操作excel主要用到xlr ...
- vue 里面的watch 选项详解
早就想好好写写这个watch了,一直顾不上,因为想深刻的记录一下,其实这些东西只要是好好看看官网的说明,都在里面呢. 虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器.这就是为什么 V ...
- python里面的xlrd模块详解
那我就一下面积个问题对xlrd模块进行学习一下: 1.什么是xlrd模块? 2.为什么使用xlrd模块? 3.怎样使用xlrd模块? 1.什么是xlrd模块? ♦python操作excel主要用到xl ...
- Spring 3.x jar 包详解 与 依赖关系(转)
以下的内容我会持续更新(当然是我有新发现的时候); 以下内容是我在网上搜索.整理.修改的而成的内容.由于很多内容都是转载了,无法追溯到源头,因此无法一一对原作者进行道谢. 这几天,我查阅大量的官方的文 ...
- 常见 jar包详解
常见 jar包详解 jar包 用途 axis.jar SOAP引擎包 commons-discovery-0.2.jar 用来发现.查找和实现可插入式接口,提供一些一般类实例化.单件的生命周期 ...
- Spring各个jar包详解
Spring各jar包详解 spring.jar 是包含有完整发布模块的单个jar 包.但是不包括mock.jar,aspects.jar, spring-portlet.jar, and sprin ...
- 最全面的Android Webview详解
转自:最全面的Android Webview详解 前言 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝.京东.聚划算等等,如下图 那么这种该如何实现呢?其实这是 ...
随机推荐
- python之单例模式、栈、队列和有序字典
一.单例模式 import time import threading class Singleton(object): lock = threading.RLock() # 定义一把锁 __inst ...
- 2018年第九届蓝桥杯题目(C/C++B组)汇总
第一题 标题:第几天 2000年的1月1日,是那一年的第1天. 那么,2000年的5月4日,是那一年的第几天? 注意:需要提交的是一个整数,不要填写任何多余内容. 解题思路: 1. 判断2月有几天, ...
- 【kafka】Java连接出现Connection refused: no further information的解决方法
在Linux机器(ip:10.102.16.203)安装完kafka(参考:kafka的安装及使用),在windows上使用Java接口访问服务时(参考:Java实现Kafka的生产者.消费者),报异 ...
- 数据分析三剑客之numpy
Numpy 简介 数据分析三剑客:Numpy,Pandas,Matplotlib NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算, ...
- IAR使用跳转功能时不正常的情况
@2019-04-12 [小记] [使用环境]IAR-Arm8.30.1 [验证] 均为实测 1. 出现如下图这种情况应该是工程所在路径太深导致 2. 如果不弹出上图警告,但还是不跳转应该是工程编译信 ...
- SPOJ DIVCNT2
SPOJ DIVCNT2 题目大意: 求\(S2(n)=\sum_{i=1}^{n}\sigma_0{(i^2)}\) . 题解 我们可以先考虑括号里只有一个\(i\)的情况,这样,我们把\(i\)分 ...
- babel7-按需加载polyfill
babel7 babel7发布了. 在升级到 Babel 7 时需要注意几个重大变化: 移除对 Node.js 6 之前版本的支持: 使用带有作用域的 @babel 命名空间,以防止与官方 Babel ...
- Java 8 特性 —— 默认方法和静态方法
Java 8 新增了接口的默认方法.简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法.我们只需在方法名前面加个 default 关键字即可实现默认方法. 为什么要有这个特性?之前的 ...
- Nacos环境搭建
先去下载↓↓↓↓ https://github.com/alibaba/nacos/releases 单机版 单机版主要为了测试,没啥意思,你下载一个zip包,然后解压,进入bin目录,双击 star ...
- thinkphp 5.0 在appache下隐藏index.php入口代码
一.在appache的配置文件httpd.conf中开启rewrite_module 二.启用.htaccess的配置 启用.htaccess,需要修改httpd.conf,启用AllowOverri ...