ot;outer: %v, %p\n",m, m)
test_map2(m)
fmt.Printf("outer: %v, %p\n",m, m)
}
由于在函数test_map2()外仅仅对map变量m进行了声明而未初始化,在函数test_map2()中才对map进行了初始化和赋值操纵,这时候,我们看到对于map的更改便无法反馈到函数外了。
//output
outer: map[], 0x0
inner: map[], 0x0
inner: map[a:11], 0x442260
outer: map[], 0x0
跟风的Channel
在介绍完map类型作为参数传递时的行为后,我们再来看看golang的特殊类型:channel的行为。还是通过一段代码来来入手:
//demo4
package main
import "fmt"
func test_chan2(ch chan string){
fmt.Printf("inner: %v, %v\n",ch, len(ch))
ch<-"b"
fmt.Printf("inner: %v, %v\n",ch, len(ch))
}
func main() {
ch := make(chan string, 10)
ch<- "a"
fmt.Printf("outer: %v, %v\n",ch, len(ch))
test_chan2(ch)
fmt.Printf("outer: %v, %v\n",ch, len(ch))
}
结果如下,我们看到,在函数内往channel中塞入数值,在函数外可以看到channel的size发生了变化:
//output
outer: 0x436100, 1
inner: 0x436100, 1
inner: 0x436100, 2
outer: 0x436100, 2
在golang中,对于channel有着与map类似的结果,其make()函数实现源代码如下:
func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
也就是make() chan的返回值为一个hchan类型的指针,因此当我们的业务代码在函数内对channel操作的同时,也会影响到函数外的数值。
与众不同的Slice
对于golang中slice的行为,可以总结一句话:与众不同。首先,我们来看下golang中对于slice的make实现代码:
func makeslice(et *_type, len, cap int) slice {
...
我们发现,与map和channel不同的是,sclie的make函数返回的是一个内建结构体类型slice的对象,而并非一个指针类型,其中内建slice的数据结构如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
也就是说,如果采用slice在golang中传递参数,在函数内对slice的操作是不应该影响到函数外的。那么,对于下面的这段示例代码,运行的结果又是什么呢?
//demo5
package main
import "fmt"
func main() {
sl := []string{
"a",
"b",
"c",
}
fmt.Printf("%v, %p\n",sl, sl)
test_slice(sl)
fmt.Printf("%v, %p\n",sl, sl)
}
func test_slice(sl []string){
fmt.Printf("%v, %p\n",sl, sl)
sl[0] = "aa"
//sl = append(sl, "d")
fmt.Printf("%v, %p\n",sl, sl)
}
通过运行结果,我们看到,在函数内部对slice中的第一个元素的数值修改成功的返回到了test_slice()函数外层!与此同时,通过打印地址,我们发现也显示了是同一个地址。到了这儿,似乎又一个奇怪的现象出现了:makeslice()返回的是值类型,但是当该数值作为参数传递时,在函数内外的地址却未发生变化,俨然一副指针类型。
//output
[a b c], 0x442260
[a b c], 0x442260
[aa b c], 0x442260
[aa b c], 0x442260
这时候,我们还是回归源码,回顾一下上面列出的golang内部slice结构体的特点。没错,细心地读者可能已经发现,内部slice中的第一个元素用来存放数据的结构是个指针类型,一个指向了真正的存放数据的指针!因此,虽然指针拷贝了,但是指针所指向的地址却未更改,而我们在函数内部修改了指针所指向的地方的内容,从而实现了对元素修改的目的了。
让我们再进阶一下上面的示例,将注释的那行代码打开:
sl = append(sl, "d")
再重新运行上面的代码,得到的结果又有了新的变化:
//output
[a b c], 0x442280
[a b c], 0x442280
[aa b c d], 0x442280
[aa b c], 0x442280
函数内我们修改了slice中一个已有元素,同时向slice中append了另一个元素,结果在函数外部:
其实这就是由于slice的结构引起的了。我们都知道slice类型在make()的时候有个len和cap的可选参数,在上面的内部slice结构中第二和第三个成员变量就是代表着这俩个参数的含义。我们已知原因,数据部分由于是指针类型,这就决定了在函数内部对slice数据的修改是可以生效的,因为值传递进去的是指向数据的指针。而同一时刻,表示长度的len和容量的cap均为int类型,那么在传递到函数内部的就仅仅只是一个副本,因此在函数内部通过append修改了len的数值,但却影响不到函数外部slice的len变量,从而,append的影响便无法在函数外部看到了。
解释到这儿,基本说清了golang中map、channel和slice在函数传递时的行为和原因了,但是,喜欢提问的读者可能一直觉得有哪儿是怪怪的,这个时候我们来完整的整理一下已经的关于slice的信息和行为:
- makeslice()出来的一定是个结构体对象,而不是指针;
- 函数内外打印的slice地址一致;
- 函数体内对slice中元素的修改在函数外部生效了;
- 函数体内对slice进行append操作在外部没有生效;
没错了,对于问题1、3和4我们应该都已经解释清楚了,但是,关于第2点为什么函数内外对于这三个内建类型变量的地址打印却是一致的?我们已经更加确定了golang中的参数传递的确是值类型,那么,造成这一现象的唯一可能就是出在打印函数fmt.Printf()中有些小操作了。因为我们是通过%p来打印地址信息的,