今天,尝试谈下 Go 中的引用。git
之因此要谈它,一方面是以前的我也有些概念混乱,想梳理下,另外一方面是由于不少人对引用都有疑问。我常常会看到与引用有关的问题。程序员
好比,什么是引用?引用和指针有什么区别?Go 中有引用类型吗?什么是值传递?址传递?引用传递?github
在开始谈论以前,我已经感受到这一定是一个很是头疼的话题。这或许就是学了那么多语言,但没有深刻总结,从而致使的思惟混乱。golang
个人理解是,要完全搞懂引用,得从类型和传递两个角度分别进行思考。编程
从类型角度,类型可分为值类型和引用类型,通常而言,咱们说到引用,强调的都是类型。c#
从传递角度,有值传递、址传递和引用传递,传递是在函数调用时才会提到的概念,用于代表实参与形参的关系。segmentfault
引用类型和引用传递的关系,我尝试用一句话归纳,引用类型不必定是引用传递,但引用传递的必定是引用类型。数组
这几句话,是我在使用各类语言的以后总结出来的,但愿无误吧,毕竟不能误导他人。bash
谈到引用,就不得不提指针,而指针与引用是编程学习中老生常谈的话题了。有些编程语言为了下降程序员的使用门槛,只有引用。而有些语言则是指针引用皆存在,如 C++ 和 Go。
指针,即地址的意思。
在程序运行的时候,操做系统会为每一个变量分配一块内存放变量内容,而这块内存有一个编号,即内存地址,也就是变量的地址。如今 CPU 通常都是 64 位,于是,这个地址的长度通常也就是 8 个字节。
引用,某块内存的别名。
通常状况,都会这么解释引用。换句话说,引用代指某个内存地址,这句话真的是很是简洁,同时也很是好理解。但在 Go 中,这句话看起来并不全面,具体后面解释。
除了指针和引用,还有另一个更普遍的概念,值。谈变量传递时,常会提到值传递、址传递和引用传递。从广义上看,对大部分的语言而言,指针和引用都属于值。而从狭义角度来讲,则可分为值、址和引用。
至关绕人是否是?
我已经感受到本身头发在掉了。其实,要想完全搞清楚这些概念,仍是得从本质出发。
先来搞明白值与指针区别。
上一节在介绍指针的时候,提到了要注意变量的地址和内容的不一样。为何要说这句话呢?
假设,咱们定义一个 int 类型的变量 a,以下:
var a int = 1
复制代码
变量 a 的内容为 1,而变量内容是存在某个地址之中的。如何获取变量地址呢?Go 中获取变量地址的方法与 C/C++ 相同。代码以下:
var p = &a
复制代码
经过 & 获取 a 的地址。同时,这里还定义了一个新的变量 p 用于保存变量 a 的地址。p 的类型为 int 指针,也就是变量 p 中的内容是变量 a 的地址。
以下代码输出它们的地址:
var a = 1
var p = &a
fmt.Printf("%p\n", p)
fmt.Printf("%p\n", &p)
复制代码
我这里的输出结果是,变量 a 和 p 的地址分别为 0xc000092000 和 0xc00008c010。此时的内存的分布以下:
变量 p 的内容是 a 的地址,于是能够说指针便是其余变量的内容,也是某个变量的地址。为何啰啰嗦嗦的说这些,由于在学习 C 语言,会单独强调址的概念,但在 Go 中,指针相对弱化,也是归于值类型之中。
前面说过,引用是某块内存的别名。从字面理解,彷佛表达的是引用类型变量中的内容是指针,这么理解彷佛也没错。既然如此,我天然而然地想到,怎么将引用与指针关联起来。
在 C/C++ 中,引用实际上是编译器实现的一个语法糖,通过汇编后,将会把引用操做转化为了指针操做。这真的是别名啊,有种 define 预处理的感受,只不过是汇编级别的。分享一篇 C++中“引用”的底层实现 的文章,有兴趣仔细读读,我只是看了个大概。
而其余一些语言中,引用的本质实际上是 struct 中包含指针,好比 Python。下面的 C 结构是 Python 中列表类型的底层结构。
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
复制代码
变量真正存放数据的地方在 **ob_item
中。结构中的其余两个成员起辅助做用。
如今看来,引用的实现主要有两种。一是 C++ 的思路,引用其实一种便于使用指针的语法糖,和咱们想象中的别名含义一致。二是相似 Python 中的实现,底层结构中包含指向实际内容的指针。
固然,或许还有其余的实现方式,但核心应该是不变的。
谈到引用传递,就不得不提值传递,值传递的通常定义以下。
函数调用时,实参经过拷贝将自身内容传递给形参,形参其实是实参值的一个拷贝,此时,针对函数中形参的任何操做,仅仅是针对实参的副本,不影响原始值的内容。
值传递中有一个特殊形式,若是传递参数的类型是指针,咱们就会称之为址传递,C 语言中就有值传递和址传递两种说法。深究起来,C 中的址传递也属于值传递,由于对指针类型而言,变量的值是指针,即传递的值也是指针。而 C 语言之因此强调址传递,我认为主要 C 这门底层语言对指针较为重视。
什么是引用传递?
参考值传递的定义,实参地址在函数调用被传递给形参,针对形参的操做,影响到了实参,则能够认为是引用传递。
在我用过的语言中,支持引用传递的语言有 PHP 和 C++。
Go 的引用类型有 slice、map 和 chan,实现机制采用的是前面提到的第二种方式,即结构体含指针成员。它们均可以使用内置函数 make 进行初始化。
本来我是想把这几种引用类型的底层结构都贴出来,但发现这会干扰本文主题的理解。咱们只看 slice 的结构,以下:
// slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
复制代码
slice 的结构最简单,包含三个成员,分别是切片的底层数组地址、切片长度和容量大小。是否感受与前面提到的 Python 列表的底层结构很是相似?
若是想了解 map 和 chan 的结构,可自行阅读 go 的源码,runtime/slice.go、runtime/map.go 和 runtime/chan.go。
若是不想研究源码,推荐阅读饶大的 Go 深度解密系列文章,包括 深度解密Go语言之Slice、深度解密Go语言之map、深度解密Go语言之channel,这几篇文章由于写的都很是细且很是长,可能读起来会比较考验你的耐心。
按官方说法,Go 中只有值传递。原文以下:
In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.
重点是下面这句话。
After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.
有点迷糊?最初我也迷糊,Go 不是有指针和引用类型吗。但读了一些文章,思考了许久,才完全想明白。下面,我将尝试为官方的说法找个合理的解释。
为何说 Go 中没有址传递
其实,这个问题前面已经解释的很清楚了,指针只是值的一种特殊形式,C 语言是门很是底层的语言,常会涉及一些地址操做,会强调指针的特殊地位。但于 Go 而言,指针已经弱化了不少,Go 团队可能也以为没有必要再单独强调指针的地位。
为何说 Go 中没有引用传递?
有人可能会说,Go 中明明有引用传递,按照引用传递的定义,能够很是容易就拿出一个例子反驳我。
package main
import "fmt"
func update(s []int) {
s[1] = 10
}
func main() {
a := []int{0, 1, 2, 3, 4}
fmt.Println(a)
update(a)
fmt.Println(a)
}
复制代码
输出结果以下:
[0 1 2 3 4]
[0 10 2 3 4]
复制代码
针对形参 s 的操做确实改变了实参 a 的值,彷佛的确是引用传递。但我想说的是,针对形参的操做并不是指的是针对形参中某个元素的操做。
看个 C++ 中引用的例子。
void update(int& s) {
s = 10;
printf("s address: %p\n", &s);
}
int main() {
int a = 1;
std::cout << a << std::endl;
printf("a address: %p\n", &a);
update(a);
std::cout << a << std::endl;
}
复制代码
执行结果以下:
1
a address: 0x7fff5b98f21c
s address: 0x7fff5b98f21c
10
复制代码
针对 s 的操做确实改变了 a 的值。在 Go 中尝试一样的代码,以下:
func update(s []int) {
s[1] = 10
fmt.Printf("%p\n", &s)
}
func main() {
a := []int{0, 1, 2, 3, 4}
fmt.Println(a)
fmt.Printf("%p\n", &a)
update(a)
fmt.Println(a)
}
复制代码
输出以下:
[0 1 2 3 4]
0xc00000c060
0xc000098000
[0 10 2 3 4]
复制代码
很是遗憾,针对形参的赋值操做并无改变实参的值。基于此,得出结论是 slice 的传递并不是引用传递。我比较喜欢的这种解释方式,适合我我的的记忆理解,不知道是否有不妥的地方。
除此以外,介绍另一种识别是不是引用传递的方式。
经过比较形参和实参地址确认,若是二者地址相同,则是引用传递,不一样则非引用传递。但由于 C++ 和 Go 引用的实现机制不一样,理解起来会比较困难。咱们也能够选择只记结论。
这种方式的验证很是简单,咱们在上面的 C++ 和 Go 的例子中已经输出了形参和实参的地址,比较下便可得出结论。
本文主要从引用的类型和传递两个角度出发,深刻浅出的分析了 Go 中的引用。
首先,引用类型和引用传递并无绝对的关系,不知道有多少人认为引用类型必然是引用传递。接着,咱们讨论了不一样语言引用的实现机制,涉及到 C++、Python 和 Go。
文章的最后,解释了一个常见的疑惑,为何说 Go 只有值传递。在此基础上,文中提出了两种方式,帮助识别一门语言是否支持引用传递。
golang中哪些引用类型的指针在声明时不用加&号,哪些在函数定义的形参和返回值类型中不用*号标注
Golang中的make(T, args)为何返回T而不是*T?
The Go Programming Language Specification
欢迎关注个人公众号。