golang 函数参数传递--指针,引用和值(二)

这一章节我们来分析一下 golang 值,指针,引用的区别。在大学我们学习 C 语言对值和指针已经有足够了解了,但是引用这个概念是在更高级的语言中引入的,比如 java,引用和指针很像,但是它和指针有上面区别呢?为什么需要应用?。接下来我们通过一些示例一一了解他们。

可以理解为变量存储的内容,或者说变量所代表的存储空间的内容。值在函数中传递时会 copy 一个副本,也就是说传入函数后这个值和原来的变量就没有关系了,修改这个值不会影响原来变量的值,我们来看一个示例:

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a int) {
    a = a + 1
}

输出结果:

a=1

指针

在大学我们 C 语言的时候接触过指针的概念,golang 的指针和 C 语言的指针时一个含义,其内容是一段存储空间的地址。指针本身也是一个值,这不过这个值是一个存储空间的地址。然后函数参数传递的是一个指针,通过这个指针可以修改原来变量的值。我们把上面示例稍微修改一下在看看结果:

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(&a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a *int) {
    *a = *a + 1
}

输出结果:

a=2

另外需要特性强调的是,golang 的指针是一个阉割版的指针,golang 的指针是不支持运算的,指针本身的值是无法改变。golang 这么做是为了防止出现野指针(指针指向了非法的空间)。

下面代码在编译时就会报错

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(&a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a *int) {
    a = a + 1
}

报错信息

/main.go:12:10: cannot convert 1 (untyped int constant) to *int

其实 golang 的指针也并非是完全不能进行加减乘除运算的,但是需要先用 unsafe.pointer 把指针先转为整数,但是这目前不是本章的重点,后面我会写一篇专门的文档。

引用

这是我们这一章节重点要说的,应该有很多人对引用的理解始终不是那么的透彻,大家第一次接触引用的概念应该是在大学学习 java 的时候,其行为和指针很像但是无论老师和课本都强调它不是指针,那么引用到底和指针有哪些区别呢。其实引用的引入是为了处理一些复杂数据类型的,如 golang 中的 slice,map,chan 等,为什么说这些数据类型复杂呢?我们就以 slice(切片)举例说明。

先看下面程序:

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    fmt.Printf("value of a=%v; lenght of a=%d; capacity of a=%d\n", a, len(a), cap(a))
    b := a[1:3]
    fmt.Printf("value of b=%v; lenght of b=%d; capacity of b=%d\n", b, len(b), cap(b))
}

输出结果:

value of a=[1 2 3]; lenght of a=3; capacity of a=3
value of b=[2 3]; lenght of b=2; capacity of b=2

我先来解释一下为什么 slice 是一种复杂类型,然后再回来分析上面的结果。所谓复杂类型,就是这种类型的变量自带一下“内禀属性”或者叫“内建属性”有或者可以叫“元数据”,这些属性的并不需要人工赋予,是语言自动添加的,slice 有三个内禀属性:Data,Len,Cap,在 reflect 中其对应的底层的结构体如下:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  • Data 是一个指针,指向了一段存储空间,这段存储空间用于存储 slice 数据的
  • Len 表示当前 slice 的长度
  • Cap 表示底层存储空间的容量

这样我们就很好理解上面的输出了:slice a 和 slice b 共用底层存储空间,所以它们的 Cap 属性是一样的。

现在我们在回来看引用,之所以会用引用是因为引用所代表的变量背后的数据结构是复杂的,有很多属性是语言自动处理的(语言不希望我们来改变这些数据),在这种情况下使用指针明显是不合适的。

下面这张图详细解释了 golang 中哪些类型变量是值类型,哪些类型变量是引用类型。

go-types.png

另外需要特别注意的是 slice 和 map 分别对应的有两个内建函数 append 和 delete,append 用于向 slice 追加数据但是当 slice 底层存储空间不足时 append 会把原 slice 的底层数据 copy 一份放入新的地址空间追加的数据放入新的地址空间,也就是说使用 append 不会改变原来的数据,我们来看一个例子:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a []int) {
    a = append(a, 3)
}

输出结果:

a=[1 2]

我们可以看到函数调用前后 a 的值没有变化。但是通过下面的方式改变函数里面 slice 的值,会影响到原来的变量:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a []int) {
    a[1] = 3
}

输出结果:

a=[1 3]

我接下来在看看 delete 是如何处理 map 的,直接看例子:

package main

import (
    "fmt"
)

func main() {
    a := map[int]string{1: "a", 2: "b"}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a map[int]string) {
    delete(a, 1)
}

输出结果:

a=map[2:b]

可以看到函数调用前后 map a 的值改变了,我们在来看一个向 map 中添加元素的例子:

package main

import (
    "fmt"
)

func main() {
    a := map[int]string{1: "a", 2: "b"}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a map[int]string) {
    a[3] = "c"
}

输出结果:

a=map[1:a 2:b 3:c]

同样的,map a 的值被改变了。

那么引用的指针呢,会有什么现象?我们在看一个函数参数是引用类型变量的指针的例子:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(&a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a *[]int) {
    *a = append(*a, 3)
}

输出结果:

a=[1 2 3]

可以看到 slice a 在函数调用前后有变化。

总结

通过上面的例子大家能感受到 golang 数据类型的复杂,那么最后我就用一段话对上面这些现象及其原理做一下概括性解释。

golang 数据类型分为两大类:值类型和应用类型,如果函数参数是值类型,在函数调用时会 copy 一份数据,函数中改变数据不会改变原变量;如果函数参数时应用类型,在函数调用时只会 copy 引用本身并不会 copy 引用所代表的底层数据,但是在用 append 函数处理 slice 时,由于append 函数內部会 copy slice,所以通过 append 更改 slice 时不会影响原值,处理 map 的 delete 并不会 copy 数据所以会影响到原变量。对于指针,无论值类型的指针还是引用类型的指针都会改变原变量。

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

推荐阅读更多精彩内容