这一章节我们来分析一下 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 中哪些类型变量是值类型,哪些类型变量是引用类型。

在这里插入图片描述

另外需要特别注意的是 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 数据所以会影响到原变量。对于指针,无论值类型的指针还是引用类型的指针都会改变原变量。