指针Pointer

复习日志(1)

* 本页面主要介绍Go语言引用数据类型指针Pointer的相关内容。

一讲到指针,你可能会忌惮于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到了吗?


* 本页内容参考以下数据源:

  • 《Go程序设计语言》
  • 《Go语言核心36讲》 15 关于指针的有限操作

凯冰科技 · 代码改变世界,技术改变生活
Next Page→