Go 指针

Go中的指针

翻译自

https://go101.org/article/pointer.html

尽管Go吸收了很多其他语言的特性,但Go总体来说是一个C家族语言。其中一个证据就是Go也支持指针。Go指针和C指针在许多方面非常相似,但其中也有一些不同。本文将会列举Go指针的概念和细节。

Memory Address 内存地址

内存地址指的是整个系统管理(通常由操作系统管理)的内存空间中的偏移量(byte的个数)。

通常,内存地址被存储成一个无符号(整型)字。字长在32位机器上是4字节,在64位机器上是8字节。所以理论最大寻址范围,在32位机器上是2^32,也就是4GB。在64位机器上是2^64,也就是16EB(1EB=1024PB,1PB=1024TB,1TB=1024GB)。

内存地址通常用Hex表达模式书写,如0x1234CDEF

Value Address 值地址

值的地址代表这个值在内存段中的起始地址

什么是指针?

指针是Go的一种类型。指针用来存储内存地址。跟C语言不同,为了安全的原因,Go指针上有一些限制。

Go指针类型和值

在Go中,未定义的指针类型可以被写成*TT可以是任意类型。T*T的基础类型。

我们也可以自定义指针类型,通常由于未定义指针类型拥有更好的可读性,不推荐使用自定义指针类型。

如果一个定义的指针类型的底层类型(underlying type)是*T,那么该指针的基础类型是T。这个基础类型相同的指针也是同一种类型。样例

// 未定义的指针类型,基础类型是int
*int
// 未定义的指针类型,基础类型是*int
**int 

// Ptr是一个定义指针类型,基础类型是int
type Ptr *int 
// PP是一个定义指针类型,基础类型是Ptr
type PP *Ptr

指针的零值是nil,不存储任何地址。

基础类型为T的指针仅能存储T类型值的地址

如何获得指针值、什么是可寻址值

有两种方式可用于获取非nil的指针值

  • go内置的new函数,可以分配任何类型的内存。new(T)在内存中分配一个T值的空间,然后返回T值的地址。分配的值是T类型的零值。返回的地址就是一个T类型的指针
  • 我们还可以直接获取可寻址值的地址。对于一个可寻址类型T的值t,我们可以使用&t来获取t的地址,`&操作符用来获取值地址。

通常上来说,可寻址值意味着该值存放在内存中的某处。现在,我们只需要知道任何变量都是可寻址的,同时常数、函数调用和显示转换的结果是不可寻址的。变量声明的时候,Go运行时将会为这个变量分配一片内存,这片内存的开始地址就是这个变量的地址。

Pointer Derefernece 指针解引用

对于一个基础类型为T的指针类型p,你如何获取指针中存储的值(或者说,被指针指向的值)?只需要使用表达式*p*被称为解引用操作符。指针的解引用是取地址的逆运算。*p的返回值是T类型,也就是p的基础类型。

nil指针进行解引用会导致运行时异常。

这个程序展示了地址获取和解引用的例子:

package main

import "fmt"

func main() {
    // p0指向int的零值
    p0 := new(int)
    // hex表达的地址
    fmt.Println(p0)
    // 0
    fmt.Println(*p0)

    // x是p0指向值的拷贝
    x := *p0
    // 都取得x的地址
    // x, *p1, *p2 的值相同
    p1, p2 := &x, &x
    // true
    fmt.Println(p1 == p2)
    // false
    fmt.Println(p0 == p1)
    // p3 和 p0 也存储相同的地址
    p3 := &*p0
    fmt.Println(p0 == p3)
    *p0, *p1 = 123, 789
    // 789 789 123
    fmt.Println(*p2, x, *p3)

    // int, int
    fmt.Printf("%T, %T \n", *p0, x)
    // *int, *int
    fmt.Printf("%T, %T \n", p0, p1)
}

下图揭示了上面程序存储的值间关系

image-20210712093648121

为什么我们需要指针

让我们先来看一个样例程序

package main

import "fmt"

func double(x int) {
    x += x
}

func main() {
    var a = 3
    double(a)
    fmt.Println(a) // 3
}

上例中的double函数预期对输入值进行双倍处理。但是它失败了。为什么?因为所有值的分配,包括函数参数的传递,都是值拷贝。double函数操作的x只是a变量的拷贝,而不是a变量。

修复上例的一种方式是让double函数返回一个新值。但这并不是所有场景都适用。下例展示了另一种使用指针的方案

package main

import "fmt"

func double(x *int) {
    *x += *x
    // 这行只为了解释用途
    x = nil 
}

func main() {
    var a = 3
    double(&a)
    fmt.Println(a) // 6
    p := &a
    double(p)
    fmt.Println(a, p == nil) // 12 false
}

我们可以发现,通过将参数改为指针类型,传递的指针参数&a和它的拷贝x都指向相同的值,所以在*x上进行的修改,在a上也体现了出来。同时,因为参数传递都是值拷贝,上面将x赋值为nil,在p上也不生效。

简而言之,指针提供了操作值的间接方式。大部分语言没有指针的概念。然而,指针的概念只是隐藏在了语言的其他概念中。

返回局部变量指针在Go是安全的

和C语言不通,Go支持垃圾回收,所以返回局部变量的指针在Go中是绝对安全的

func newInt() *int {
    a := 3
    return &a
}

Go指针的限制

为了安全原因,相比C语言来说,Go在指针上做了一些限制。通过这些限制,Go保持了指针带来的收益,并且避免了危险的指针使用。

Go指针不支持算术操作

在Go中,指针不能进行算术运算。对于指针pp++p-2都是非法的。

如果指针p指向一个数值,编译器会将*p++识别为一个合法的语句,并解析为(*p)++。换句话说,*p操作的优先级高于++--操作符。样例

package main

import "fmt"

func main() {
    a := int64(5)
    p := &a

    // 下面的语句无法编译
    /*
    p++
    p = (&a) + 8
    */

    *p++
    fmt.Println(*p, a)   // 6 6
    fmt.Println(p == &a) // true

    *&a++
    *&*&a++
    **&p++
    *&*p++
    fmt.Println(*p, a) // 10 10
}

指针值不能转换为任意的指针类型

在Go中,T1的指针值可以被隐式或是显示地转换为T2,需要满足如下两个条件

  • T1T2的底层类型相同(忽略结构体Tag)。特别地,如果T1T2都是未定义类型并且它们的底层类型相同(考虑结构体Tag),可以进行隐式转换。
  • T1T2都是未定义指针类型,并且它们的基础类型的底层类型相同(忽略结构体Tag)

举个例子,有如下类型

type MyInt int64
type Ta *int64
type Tb *MyInt

有如下事实

  • *int64类型的值可以被隐式转换为Ta类型,反过来也是可以的。因为它们的底层类型都是*int64
  • *MyInt类型的值可以被隐式转换为Tb类型,反过来也行。因为它们的底层类型都是*MyInt
  • *MyInt类型的值可以被限制转换为*int64,反之亦然。因为它们的基础类型的底层类型都是int64
  • 即使是显示转换,Ta类型的值也不能直接转换为Tb。因为Ta和底层类型和Tb不同,并且都是定义指针类型。不过可以进行连续几次转换,将Ta类型的pa间接转化为Tb类型。 先将pa转化为*int64类型(因为基础类型的底层类型都是int64),再将*int64类型转换为*MyInt类型,再将*MyInt类型转化为*Tb类型。Tb((*MyInt)((*int64)(pa)))

上面的值通过任何安全的手段,都不能转化为类型*uint64

任意两个指针的值不能比较

在Go中,指针可以通过==!=符号比较。如果满足如下任意一个条件,那么两个Go指针的值可以进行比较

  • 两个Go指针类型一致
  • 指针值可以隐式转换为另一类型
  • 两个指针中的一个且仅一个用无类型 nil 标识符表示。
package main

func main() {
    type MyInt int64
    type Ta    *int64
    type Tb    *MyInt

    // 4个不同类型的指针零值
    var pa0 Ta
    var pa1 *int64
    var pb0 Tb
    var pb1 *MyInt

    // 下面这6行都可以正常编译
    // 比较结果都为true
    // 指针可以隐式转换
    _ = pa0 == pa1
    // 指针类型一致
    _ = pb0 == pb1
    _ = pa0 == nil
    _ = pa1 == nil
    _ = pb0 == nil
    _ = pb1 == nil

    // 这三行都不能正常编译
    /*
    _ = pa0 == pb0
    _ = pa1 == pb1
    _ = pa0 == Tb(nil)
    */
}

指针值不能赋值给其他指针类型

指针值互相赋值的条件和比较的条件一样

有手段打破Go对指针的限制

unsafe 标准包提供的机制(特别是 unsafe.Pointer 类型)可以用来打破 Go 中对指针的限制。 unsafe.Pointer 类型类似于 C 中的 void*。一般不推荐使用unsafe

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容