前言
最近在公司内部给近几个月入职的新人做了一个技术分享,因为团队发展几个业务线的研发都增加了不少新人,大部分人之前都是
PHP
开发,所以就跟大家一起交流了下从PHPer
到Gopher
这个过程中思维和习惯上要做的调整。文章是我这次分享的演讲稿修改整理而成的,在阅读的时候尽量先不看文章里几个例子的答案,先给出自己认为的答案再看后面的解释,这样效果会好一点,我在现场也是带着大家一起思考这些问题来慢慢推演结论的。关于付费:只是想这个文章现阶段只在我自己的公众号里小范围传播:D,后续并没做付费文章的计划。
学习一门新编程语言时,我们总会下意识地用自己熟悉语言类比着去理解新语言,甚至用原来语言的思维套路写新语言的程序。比如PHP
里数组的长度是可以动态增长的,Go
里面的切片和它差不多也能自动增加长度。比如PHP
里我们可以用引用参数让函数修改外部的变量的数据,那在Go
我们也可以用指针类型的参数达到同样的目的,所以他们在使用上应该都差不多吧,只不过是换了种编程语言来表达。
大家在刚从PHP
转到用Go
语言写程序时一定要警惕这种想法,从零开始了解Go语言的基础,才能用Go语言写好程序。我们这次分享会探讨两个问题:
Go
语言里有引用类型吗Go
函数的参数能够通过引用传递吗
我先不给出这两个问题的答案,咱们用例子推演出这两个问题的结果。
之后我们会说几个PHP
程序员在刚开始用Go
写程序时几个需要改变的编码习惯和要注意的地方。我们这次分享不涉及什么高深的技术,都是一些需要注意的细节,相信新同学们在今天的分享会后会更有信心用Go
语言写好程序。
我们先从上面提到的切片和指针两个数据类型切入,探讨上面提到的两个问题。
重新认识Go里的引用类型
切片是引用类型吗
数组需要预先声明长度,有些不灵活,因此在Go
代码中不经常见到它们。但是切片却无处不在。切片是一段数组的描述符,编译期间的切片是 slice
类型的,但是在运行时切片由如下的 SliceHeader
结构体表示,其中 Data
字段是指向底层数组的指针(可以理解成底层数组中存储切片索引0
位置上的元素的内存地址),Len
表示切片的长度,而 Cap
表示切片的容量(最大长度)
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
切片与数组的联系可以用下面这张图表示
很多地方提起切片都会说它是引用类型,但是在上面的SliceHeader
结构体类型中我们看到切片的属性里只有Data
是指向底层数组的指针,而长度和容量却不是,这在让我们在平时使用切片时如果稍不注意,尤其是带着在其他语言使用引用类型的思维定式来使用切片时程序不但不会按照预期的运行还会出现一些诡异的现象,我们通过三个例子来看一下。
func main() {
var s []int
for i := 1; i <= 3; i++ {
s = append(s, i)
}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
for i, j := 0, len(s) - 1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
程序最终的输出结果是:
[3 2 1]
上面的代码段首先构建了一个切片s
,一开始s
的值是[1, 2, 3]
。然后在reverse
函数里对切片进行了反转。在main
函数里打印s
会发现在reverse
函数外也能看到reverse
对切片s
的操作结果。这符合我们对引用类型的理解。
现在我们将反转函数的内部稍微修改一下,在反转切片前往里面先追加一个元素。
func reverse(s []int) {
s = append(s, 999)
for i, j := 0, len(s) - 1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
这次程序的输出变成了:
[999 3 2]
1
不见了,导致1
不见的原因是当调用append
时,将创建一个新切片。新切片具有新的 “长度” 属性,该属性不是指针,但Data
属性仍指向同一个底层数组。因此,我们函数内的代码最终会反转切片所引用的底层数组(切片里边是不存储任何数据的),但是函数外原始切片的长度属性还是之前的长度值3
,这就是造成了上面 1 被丢掉的原因。
还有比这个更诡异的情况,我们再把上面的反转函数进一步改造一下,多添加几个元素:
func reverse(s []int) {
s = append(s, 999, 1000, 1001)
for i, j := 0, len(s)-1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
这次程序的输出变成了:
[1, 2, 3]
在反转函数内对切片的更改在函数外又看不见了,这隐隐约约让我们感觉,切片并不像其他语言的引用类型那样是按照地址传递的。
如前所述,当我们调用append
时,会创建一个新的切片。在第二个例子中,反转函数里的新切片仍指向同一底层数组,因为数组有足够的容量来添加新元素,因此在函数内对底层数组的更改也能在函数外体现,但是这个例子中,在reverse
函数里向切片添加了三个元素,而我们的切片没有足够的容量。于是系统分配了一个新数组,让切片指向该数组。这时函数内外的切片指向的不同的底层数组,所以在函数内对切片做的任何更改都不会再影响我们的初始切片。
上面两个例子的切片对应的底层数组的变化如下:
从上面几个切片的例子来看切片好像不是什么引用类型,根据切片头结构里的Data
指针推测,指针有可能也不是引用类型,指针参数也是通过值传递给函数内部的。
指针是引用类型吗
我们还是通过一个例子来验证上面的猜测。