golang unsafe.Pointer与uintptr
原文地址:https://blog.fanscore.cn/p/33/
先说结论
- uintptr 是一个地址数值,它不是指针,与地址上的对象没有引用关系,垃圾回收器不会因为有一个uintptr类型的值指向某对象而不回收该对象。
- unsafe.Pointer是一个指针,类似于C的void *,它与地址上的对象存在引用关系,垃圾回收器会因为有一个unsafe.Pointer类型的值指向某对象而不回收该对象。
- 任何指针都可以转为unsafe.Pointer
- unsafe.Pointer可以转为任何指针
- uintptr可以转换为unsafe.Pointer
- unsafe.Pointer可以转换为uintptr
- 指针不能直接转换为uintptr
为什么需要uintptr这个类型呢?
理论上说指针不过是一个数值,即一个uint,但实际上在go中unsafe.Pointer是不能通过强制类型转换为一个uint的,只能将unsafe.Pointer强制类型转换为一个uintptr。
var v1 float64 = 1.1
var v2 *float64 = &v1
_ = int(v2) // 这里编译报错:cannot convert unsafe.Pointer(v2) (type unsafe.Pointer) to type uint
但是可以将一个unsafe.Pointer强制类型转换为一个uintptr:
var v1 float64 = 1.1
var v2 *float64 = &v1
var v3 uintptr = uintptr(unsafe.Pointer(v2))
v4 := uint(v3)
fmt.Println(v3, v4) // v3和v4打印出来的值是相同的
可以理解为uintptr是专门用来指针操作的uint。
另外需要指出的是指针不能直接转为uintptr,即
var a float64
uintptr(&a) 这里会报错,不允许将*float64转为uintptr
一个
通过上面的描述如果你还是一头雾水的话,不妨看下下面这个实际案例:
package foo
type Person struct {
	Name string
	age  int
}
上面的代码中我们在foo包中定义了一个结构体Person,只导出了Name字段,而没有导出age字段,就是说在另外的包中我们只能直接操作Person.Name而不能直接操作Person.age,但是利用unsafe包可以绕过这个限制使我们能够操作Person.age。
package main
func main() {
	p := &foo.Person{
		Name: "张三",
	}
	fmt.Println(p)
	// *Person是不能直接转换为*string的,所以这里先将*Person转为unsafe.Pointer,再将unsafe.Pointer转为*string
	pName := (*string)(unsafe.Pointer(p))
	*pName = "李四"
	// 正常手段是不能操作Person.age的这里先通过uintptr(unsafe.Pointer(pName))得到Person.Name的地址
	// 通过unsafe.Sizeof(p.Name)得到Person.Name占用的字节数
	// Person.Name的地址 + Person.Name占用的字节数就得到了Person.age的地址,然后将地址转为int指针。
	pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
	// 将p的age字段修改为12
	*pAge = 12
	fmt.Println(p)
}
打印结果为:
$ go run main.go
&{张三 0}
&{李四 12}
需要注意的是下面这段代码比较长:
pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
但是尽量不要分成两段代码,像这样:
temp := uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))
pAge := (*int)(unsafe.Pointer(temp)
原因是在第二行语句时,已经没有指针指向p了,这时p可能会回收掉了,这时得到的地址temp就是个野指针了,不知道指向谁了,是比较危险的。
另外一个原因是在当前Go(golang版本:1.14)的内存管理机制中不会迁移内存,但是不保证以后的版本内存管理机制中有迁移内存的操作,一旦发生了内存迁移指针地址发生变更,上面的分段代码就有可能出现严重问题。
关于Go的内存管理可以参看这篇文章:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/,读完这篇文章相信你就能理解上面的内存迁移问题。
除了上面两点外还有一个原因是在Go 1.3上,当栈需要增长时栈可能会发生移动,对于下面的代码:
var obj int
fmt.Println(uintptr(unsafe.Pointer(&obj)))
bigFunc() // bigFunc()增大了栈
fmt.Println(uintptr(unsafe.Pointer(&obj)))
完全有可能打印出来两个地址。
通过上面的例子应该明白了为什么这个包名为unsafe,因为使用起来确实有风险,所以尽量不要使用这个包。
我之所以研究unsafe.Pointer完全是因为我要在多线程的环境中采用原子操作避免竞争问题,所以我用到了atomic.LoadPointer(addr *unsafe.Pointer)。不过我后面发现了atomic包提供了一个atomic.Value结构体,这个结构体提供的方法使我避免显式使用了unsafe.Pointer。所以你也正在使用atomic.LoadPointer()不妨看看atomic.Value是不是可以解决你的问题,这是我一点提醒。
参考资料
- Go语言实战笔记(二十七)| Go unsafe Pointer
- garbage collection - Does Go guarantee constant addresses? - Stack Overflow
golang unsafe.Pointer与uintptr的更多相关文章
- 你不知道的Go unsafe.Pointer uintptr原理和玩法
		unsafe.Pointer 这个类型比较重要,它是实现定位和读写的内存的基础,Go runtime大量使用它.官方文档对该类型有四个重要描述: (1)任何类型的指针都可以被转化为Pointer (2 ... 
- Go之unsafe.Pointer && uintptr 类型
		Go语言是个强类型语言.Go语言要求所有统一表达式的不同的类型之间必须做显示的类型转换.而作为Go语言鼻祖的C语言是可以直接做隐式的类型转换的. 也就是说Go对类型要求严格,不同类型不能进行赋值操作. ... 
- Go语言阅读小笔记,来自知呼达达关于unsafe.Pointer的分享.
		第一式 - 获得Slice和String的内存数据 func stringPointer(s string) unsafe.Pointer { p := (*reflect.StringHeader) ... 
- 使用unsafe.Pointer将结构体转为[]byte
		package main import ( "fmt" "unsafe" ) type TestStructTobytes struct { data int6 ... 
- golang: 利用unsafe操作未导出变量
		unsafe.Pointer其实就是类似C的void *,在golang中是用于各种指针相互转换的桥梁.uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它 ... 
- golang内存分配
		golang内存分配 new一个对象的时候,入口函数是malloc.go中的newobject函数 func newobject(typ *_type) unsafe.Pointer { flags ... 
- Golang下通过syscall调用win32的dll(calling Windows DLLs from Go )
		很多同学比如我虽然很喜欢golang,但是还是需要调用很多遗留项目或者其他优秀的开源项目,这时怎么办呢?我们想到的方法是用package里的syscall结合cgo 注意此处有坑: 在我调试时显示no ... 
- Golang 的内存管理(上篇)
		Golang 的内存管理基于 tcmalloc,可以说起点挺高的.但是 Golang 在实现的时候还做了很多优化,我们下面通过源码来看一下 Golang 的内存管理实现.下面的源码分析基于 go1.8 ... 
- [转]Go里面的unsafe包详解
		Golang的unsafe包是一个很特殊的包. 为什么这样说呢? 本文将详细解释. 来自go语言官方文档的警告 unsafe包的文档是这么说的: 导入unsafe的软件包可能不可移植,并且不受Go 1 ... 
随机推荐
- SQL Server 批量插入数据方案 SqlBulkCopy 的简单封装,让批量插入更方便
			一.Sql Server插入方案介绍 关于 SqlServer 批量插入的方式,有三种比较常用的插入方式,Insert.BatchInsert.SqlBulkCopy,下面我们对比以下三种方案的速度 ... 
- 对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考
			一:背景 1. 讲故事 这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀! 园子里最近挺热闹的,精致码农大佬分享了三篇文章: 为什么要小心使用 Task.Run [http ... 
- Alpha冲刺——序言篇(任务与计划)
			Alpha冲刺--序言篇(任务与计划) 1.整个项目预期的任务量 需求规格说明书 架构设计,原型设计,原型改进(给目标用户展现原型,并进一步理解需求) 编码规范完成.平台环境搭建完成.初步架构搭建 队 ... 
- box-sizing什么时候用?常用的值都有什么?
			一般在做自适应的网页设计的时候用,用这个属性网页结构才不会被破坏. 常用的值: 1. content-box:宽度和高度分别应用到元素的内容框,在宽度和高度之外绘制元素的内边距和边框. 2. bo ... 
- mysql创建表分区
			MySQL创建表分区 create table erp_bill_index( id int primary key auto_increment, addtime datetime ); inser ... 
- k8s遇见的问题
			open /etc/docker/certs.d/registry.access.redhat.com/redhat-ca.crt: no such file or directory 解决方案 ... 
- shell--数据库备份脚本
			#!/bin/bash #数据库的完全备份 #把日期显示为170605(这个是当前的时间)的格式 date=$(date +%y%m%d) #计算下这个备份的数据库文件的大小 size=$(du -s ... 
- css处理文字不换行、换行截断、溢出省略号
			1.使文字不换行 white-space: nowrap; 值 描述 normal 默认.空白会被浏览器忽略. pre 空白会被浏览器保留.其行为方式类似 HTML 中的 <pre> 标签 ... 
- Sharding-JDBC分库分表简单示例
			1. 简介 Sharding是一个简单的分库分表中间件,它不需要依赖于其他的服务,即可快速应用在实际项目的分库分表策略中. 2. 初始化数据库(db0.db1.db2) 1 #创建数据库db0 2 C ... 
- Mysql性能优化专栏
			1. 最大数据量 Mysql没有对单表的数据量大小做限制,单表的大小取决于操作系统对文件大小的限制. <阿里巴巴Java开发手册>中建议当单表的数据量大小超过500万行或者大于2GB时需 ... 
