一讲到指针,你可能会忌惮于C语言中复杂的指针,把你搞得晕头转向,那么刚一进来这个主题,就要跟大家说明白,Go语言中的指针跟C/C++中的指针完全不同!Go语言中的指针不能进行偏移和运算,是安全指针。
所以请大家放宽心,Go语言中的指针非常简单,大家只需要记住两个互补操作符号:&(取地址)和*(根据地址取值),就搞定了!
一、基本概念
首先,咱们先搞清楚三个概念:指针地址、指针类型和指针取值。
- 指针地址:每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。指针地址就是一个指针变量在内存中的地址。
- 指针类型:被做指针取值操作的变量的类型,就是指针的类型。如下代码示例:
ptr := &v // v的类型为T
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
//操作示例
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b) //type of b:*int
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c) //type of c:int
fmt.Printf("value of c:%v\n", c) //value of c:10
二、空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
var p *string
fmt.Println(p)
fmt.Printf("p的值是%v\n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
三、特别的指针:unsafe.Pointer
unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。unsafe.Pointer指向可寻址的(addressable)值的指针。unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。
这里牵扯一个“可寻址的”的概念。那Go语言中的哪些值是不可寻址的呢?
不可变的值(常量、字面量、字符串、基于字符串的索引或切片、函数以及方法的字面量等)、 临时结果(算术结果、表达式结果)以及不安全的(字典中的元素)。不可寻址的值,无法使用取址操作符&获取它们的指针(无法编译通过)。
一个普通的*T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针。
package math
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
unsafe.Pointer是普通的*T类型指针的指针值和uintptr值(我们在 数字number 一节有讲到)之间的桥梁。通过unsafe.Pointer可以操纵可寻址的值。但首先声明这么使用是危险的,可能造成安全隐患,非必要时不能使用。
dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))
//unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位
namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))
四、如何高效的完成字符串和字节数组之间的转换?
今天有一个朋友发给我一段代码,问我为什么这么写。可以下先看下下面的代码:
func Str2bytes(s string) []byte {
x := (*[2]uintptr)(unsafe.Pointer(&s))
h := [3]uintptr{x[0], x[1], x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func Bytes2str(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
这里面其实就涉及一个“效率”上的问题了。如果我们直接用强制转换的方式的话,是通过底层数据copy实现的,这种操作在并发量达到十万百万级别的时候会拖慢程序的处理速度,这时候,通过unsafe.Pointer(指针转换)和unitptr(指针运算)实现转换,效率就可以得到很高的提升(避免了复制操作)。我们可以知道字符串和字符数组的结构差异如下:
//string 的底层数组结构如下
struct string {
unit8 *str
int len
}
// []byte 的底层结构如下
struct uint8 {
unit8 *array
int len
int cap
}
从他们的底层结构来看,string 可看做 [2]uintptr,而 []byte 则是 [3]uintptr,这便于我们编写代码,无需额外定义结构类型。因此,str2bytes 只需构建 [3]uintptr{ptr, len, len},而 bytes2str 更简单,直接转换指针类型,忽略掉 cap 即可。
所以,上面的这两个转换函数,能够达到更加高效的转换效率。 网上有人对比过两种方式的执行效率:优化的方法5亿次耗时1.6秒,而不用unsafe.Pointer和uintptr转换300次耗时久达到了1.1秒。

Go语言的指针是不是很简单?你都Get到了吗?