大家好呀,今天网管想在这篇文章里好好跟大家聊一下 Go 语言指针这个话题,相较于 C 而言,Go 语言在设计时为了使用安全给指针在类型和运算上增加了限制,这让Go程序员既可以享受指针带来的便利,又避免了指针的危险性。除了常规的指针外,Go 语言在 unsafe 包里其实还通过 unsafe.Pointer 提供了通用指针,通过这个通用指针以及 unsafe 包的其他几个功能又让使用者能够绕过 Go 语言的类型系统直接操作内存进行例如:指针类型转换,读写结构体私有成员这样操作。网管觉得正是因为功能强大同时伴随着操作不慎读写了错误的内存地址即会造成的严重后果所以 Go 语言的设计者才会把这些功能放在 unsafe 包里。其实也没有想得那么不安全,掌握好了使用得当还是能带来很大的便利的,在一些偏向底层的源码中 unsafe 包使用的频率还是不低的。对于励志成为高阶 Gopher 的各位,这也是一项必不可少需要掌握的技能啦。接下来网管就带大家从基本的指针使用方法和限制开始看看怎么用 unsafe 包跨过这些限制直接读写内存。

基础知识

指针保存着一个值的内存地址,类型 *T代表指向T 类型值的指针。其零值为nil

&操作符为它的操作数生成一个指针。

i := 42
p = &i

*操作符则会取出指针指向地址的值,这个操作也叫做“解引用”。

fmt.Println(*p) // 通过指针p读取存储的值
*p = 21         // 通过指针p设置p执行的内存地址存储的值

为什么需要指针类型呢?参考一个从go101网站上看到的例子 :

package mainimport "fmt"func double(x int) { x += x
}func main() { var a = 3 double(a)fmt.Println(a) // 3
}

在 double 函数里将 a 翻倍,但是例子中的函数却做不到。因为 Go 语言的函数传参都是值传递。double 函数里的 x 只是实参 a 的一个拷贝,在函数内部对 x 的操作不能反馈到实参 a。

把参数换成一个指针就可以解决这个问题了。

package main
import "fmt"func double(x *int) { *x += *xx = nil
}func main() {var a = 3double(&a)fmt.Println(a) // 6p := &adouble(p)fmt.Println(a, p == nil) // 12 false
}

上面的程序乍一看你可能对下面这一行代码有些疑惑

x = nil

稍微思考一下上面说的Go语言里面参数都是值传递,你就会知道这一行代码根本不影响外面的变量 a。因为参数都是值传递,所以函数内的 x 也只是对 &a 的一个拷贝。

*x += *x

这一句把 x 指向的值(也就是 &a 指向的值,即变量 a)变为原来的 2 倍。但是对 x 本身(一个指针)的操作却不会影响外层的 a,所以在double函数内部的 x=nil 不会影响外面。

指针的限制

相较于 C 语言指针的灵活,Go 语言里指针多了不少限制,不过这让我们:既可以享受指针带来的便利,又避免了指针的危险性。下面就简单说一下 Go 对指针操作的一些限制

限制一:指针不能参与运算

来看一个简单的例子:

package mainimport "fmt"func main() {a := 5p := afmt.Println(p)p = &a + 3
}

上面的代码将不能通过编译,会报编译错误:

invalid operation: &a + 3 (mismatched types *int and int)

也就是说 Go 不允许对指针进行数学运算。

限制二:不同类型的指针不允许相互转换。

下面的程序同样也不能编译成功:

package mainfunc main() {var a int = 100var f *float64f = *float64(&a)
}

限制三:不同类型的指针不能比较和相互赋值

这条限制同上面的限制二,因为指针之间不能做类型转换,所以也没法使用==或者!=进行比较了,同样不同类型的指针变量相互之间不能赋值。比如下面这样,也是会报编译错误。

package mainfunc main() {var a int = 100var f *float64f = &a
}

Go语言的指针是类型安全的,但它有很多限制,所以 Go 还有提供了可以进行类型转换的通用指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然如果掌握不好就使用,也更容易让程序崩溃。

unsafe 包

unsafe 包用于编译阶段可以绕过 Go 语言的类型系统,直接操作内存。例如,利用 unsafe 包操作一个结构体的未导出成员。unsafe 包让我可以直接读写内存的能力。

unsafe包只有两个类型,三个函数,但是功能很强大。

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

ArbitraryTypeint的一个别名,在 Go 中ArbitraryType有特殊的意义。代表一个任意Go表达式类型。Pointerint指针类型的一个别名,在 Go 中可以把任意指针类型转换成unsafe.Pointer类型。

三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。

  • Sizeof接受任意类型的值(表达式),返回其占用的字节数,这和c语言里面不同,c语言里面sizeof函数的参数是类型,而这里是一个值,比如一个变量。

  • Offsetof:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)。

  • Alignof返回变量对齐字节数量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的值当作参数,具体细节咱们到以后聊内存对齐的文章里再说。

注意以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行

unsafe.Pointer

unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:

  1. 任何类型的指针都可以被转化为Pointer

  2. Pointer可以被转化为任何类型的指针

  3. uintptr可以被转化为Pointer

  4. Pointer可以被转化为uintptr

unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址

什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错。

a := 1
b := unsafe.Pointer(a) //报错
b := unsafe.Pointer(&a) // 正确

和普通指针一样,unsafe.Pointer 指针也是可以比较的,并且支持和 nil 比较判断是否为空指针。

unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型

// uintptr、unsafe.Pointer和普通指针之间的转换关系
uintptr <==> unsafe.Pointer <==> *T

uintptr

uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptrtypedef unsigned long long int  uint64;
typedef uint64          uintptr;

一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到uintptr类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是随便将一个 uintptr 转为 unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。

还有一点要注意的是,uintptr 并没有指针的语义,意思就是存储 uintptr 值的内存地址在Go发生GC时会被回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。

聊了这么多概念性的话题,接下来网管带大家一起看看怎么使用 unsafe.Pointer 进行指针转换以及结合 uintptr 读写结构体的私有成员。

应用示例

使用unsafe.Pointer进行指针类型转换

import ("fmt""reflect""unsafe"
)func main() {v1 := uint(12)v2 := int(13)fmt.Println(reflect.TypeOf(v1)) //uintfmt.Println(reflect.TypeOf(v2)) //intfmt.Println(reflect.TypeOf(&v1)) //*uintfmt.Println(reflect.TypeOf(&v2)) //*intp := &v1p = (*uint)(unsafe.Pointer(&v2)) //使用unsafe.Pointer进行类型的转换fmt.Println(reflect.TypeOf(p)) // *unitfmt.Println(*p) //13
}

使用unsafe.Pointer 读写结构体的私有成员

通过 Offsetof 方法可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。

package mainimport ("fmt""unsafe"
)func main() {var x struct {a intb intc []int}// unsafe.Offsetof 函数的参数必须是一个字段,  比如 x.b,  方法会返回 b 字段相对于 x 起始地址的偏移量, 包括可能的空洞。// 指针运算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。// 和 pb := &x.b 等价pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))*pb = 42fmt.Println(x.b) // "42"
}

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性

如果改为下面这种用法是有风险的:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

随着程序执行的进行,goroutine 会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个 unsafe.Pointer 是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr 类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp也就不再是现在的 &x.b 的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃。

string 和 []byte 零拷贝转换

这是一个非常经典的例子。实现字符串和 bytes 切片之间的零拷贝转换。

string和[]byte 在运行时的类型表示为reflect.StringHeaderreflect.SliceHeader

type SliceHeader struct {Data uintptrLen  intCap  int
}type StringHeader struct {Data uintptrLen  int
}

只需要共享底层 []byte 数组就可以实现零拷贝转换。

代码比较简单,不作详细解释。通过构造reflect.StringHeaderreflect.SliceHeader,来完成 string[]byte 之间的转换。

import ("fmt""reflect""unsafe"
)func main() {s := "Hello World"b := string2bytes(s)fmt.Println(b)s = bytes2string(b)fmt.Println(s)}func string2bytes(s string) []byte {stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))bh := reflect.SliceHeader{Data: stringHeader.Data,Len: stringHeader.Len,Cap: stringHeader.Len,}return *(*[]byte)(unsafe.Pointer(&bh))
}func bytes2string(b []byte) string {sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))sh := reflect.StringHeader{Data: sliceHeader.Data,Len:  sliceHeader.Len,}return *(*string)(unsafe.Pointer(&sh))
}

总结

Go 的源码中也在大量使用 unsafe 包,通过 unsafe 包绕过 Go 指针的限制,达到直接操作内存的目的,使用它有一定的风险性,但是在一些场景下,可以提升代码的效率。

参考资料

  • 深度解密Go语言之unsafe

  • unsafe.Pointer 和 uintptr :https://www.cnblogs.com/echojson/p/10743530.html

看到这里了,如果喜欢我的文章就帮我点个赞吧,我会每周通过技术文章分享我的所学所见和第一手实践经验,感谢你的支持。微信搜索关注公众号「网管叨bi叨」每周教会你一个进阶知识,还有专门写给开发工程师的Kubernetes入门教程

- END -

关注公众号,每周分享给你一个进阶知识

Go指针的使用限制和突破之路相关推荐

  1. 西门子SMART200程序 PID的控制写法,突破8路,PID直接做成子程序

    西门子SMART200程序 PID的控制写法,突破8路,PID直接做成子程序,无密码,直接调用. YID:6810655441338032工控技术窝

  2. 西门子SMART200程序 PID的控制写法,突破8路

    西门子SMART200程序 PID的控制写法,突破8路,PID直接做成子程序,无密码,直接调用. YID:6810655441338032工控技术窝

  3. 【挨踢人物传】Vage:高级DBA的突破之路(第13期)

    原文首发原创与51CTO技术论坛:http://bbs.51cto.com/thread-1085647-1.html [编者有话]      本期的嘉宾吕海波是Oracle技术方面的资深专家,也是我 ...

  4. Vage:高级DBA的突破之路

    http://bbs.51cto.com/thread-1085647-1.html Vage:高级DBA的突破之路 有一个笑话,创世第一天,上帝创造了驴.上帝对驴说:"今天我创造了你!作为 ...

  5. 【cs自我突破之路】Tools->Git

    [cs自我突破之路]主标题->副标题 "cs自我突破之路" 本学习路线和笔记是以GitHub大神PKUFlyingPig的cs-self-learning内容为核心方向,从个 ...

  6. TokenInsight对话首席——褪去浮华归本真!2020公链自主创新突破之路

    邀行业首席,谈市场现状,见趋势未来!第27期<对话首席>线上直播,于12月13日(周五)下午3点举办. 本期<对话首席>特邀Quarkchain 创始人/CEO-Qi Zhou ...

  7. c语言野指针导致问题,C语言进阶之路(三)----野指针的产生原因及解决办法

    1.会产生野指针的做法 #include //这就是一种错误的写法 int main(){ int *p = NULL; p = (int *)malloc(); //释放P所指向的内存空间,但指针变 ...

  8. 把效能带到游戏里!仙峰红海蜕变突破之路

    手游行业的蓝海与红海 传奇是2001左右的游戏产品,2014年当时国内很多大型公司在传统端游的海洋里干的热火朝天,难以抽身.随着移动互联网的爆发期到来,仙峰(全称:苏州仙峰网络科技股份有限公司)敏锐地 ...

  9. 21 年总结:文章的盘点和写作背景

    今天已经到了二月底,才来写上年的总结... 妥妥的拖延症晚期患者.其实春节前后这两周都在赶项目加班,包括春节假期有两天也在处理的工作的事儿,时间少,加上干活期间学到的东西想优先记下,所以就把总结类的文 ...

最新文章

  1. Docker 仓库管理
  2. ios android安全性分析,Android与iOS的对比分析及Android安全技术的设计与实现
  3. remote: 此仓库的限制大小为: 2048 MB, 您的使用已经超出限额
  4. Python requests 抓取网页状态返回418(亲测)
  5. 修改Eclipse自动换行长度
  6. 在ultraedit查找每行第二个单词_新手收藏!亚马逊关键字查找
  7. 解决宿主机Ping不通虚拟机的问题
  8. vue2 递归组件--树形
  9. WCF学习笔记(一):WCF Service Application和WCF Service Library的区别
  10. 编写测试用例的实用小技巧
  11. 77GHz汽车防撞雷达信号处理设计与实现
  12. html表格上下居中 w3c,HTML中怎么把表格居中
  13. 该虚拟机似乎正在使用中。如果该虚拟机未在使用,请按“获取所有权(T)”按钮获取它的所有权
  14. [设计模式]行为模式-模板方法(C++描述)
  15. 《AngularJS深度剖析与最佳实践》一1.3 创建项目
  16. Geoserver发布切片地图组完整教程
  17. RK3288RK3399 GMAC以太网调试
  18. oracle ebcdic 转换,使用sqlldr导入EBCDIC格式数据并新增Oracle字符集
  19. Google缩进40%搜索结果,微信公众号/今日头条开放搜索引擎
  20. Java项目:JSP网上在线酒类商城系统网站

热门文章

  1. 算法笔记 --- 排列组合
  2. Java图形界面设计——substance皮肤
  3. Windows下svn服务器安装
  4. win7 lnk 图标丢失——图片缓存问题
  5. 时间同步服务 chrony
  6. 2013流行Python项目汇总
  7. App的selenium,Appium爬App!
  8. Linux 用户组权限讲解
  9. 协程 vs 线程 demo
  10. 修改centos系统默认编辑器