Go
语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
但是像 string
、slice
、map
这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。
不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时 Go
编译器会介入:对于类型为接口类型的形参,Go
编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go
编译器会将零个或多个实参按一定形式转换为对应的变长形参。
1. 函数传参为数组
package main
import "fmt"
func main() {
srcArray := [3]string{"a", "b", "c"}
fmt.Printf("srcArray address is %p\n", &srcArray) // srcArray address is 0xc00005a150
modify(srcArray)
fmt.Printf("srcArray is %v\n", srcArray) // srcArray is [a b c]
}
func modify(modifyArr [3]string) [3]string {
fmt.Printf("modifyArr address is %p\n", &modifyArr) // modifyArr address is 0xc00005a180
modifyArr[1] = "x"
fmt.Printf("modifyArr is %v\n", modifyArr) // modifyArr is [a x c]
return modifyArr
}
可以看到,函数传参外面和函数里面的参数的地址不相同,分别为 0xc00005a150
和 0xc00005a180
,所以在函数内修改参数值并不会影响函数外面的原始参数。
所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值
2. 函数传参为切片
package main
import "fmt"
func main() {
srcSlice := []string{"a", "b", "c"}
fmt.Printf("srcSlice address is %p\n", srcSlice) // srcSlice address is 0xc00005a150
modify(srcSlice)
fmt.Printf("srcSlice is %v\n", srcSlice) // modifySlice is [a x c]
}
func modify(modifySlice []string) []string {
fmt.Printf("modifySlice address is %p\n", modifySlice) // modifySlice address is 0xc00005a150
modifySlice[1] = "x"
fmt.Printf("modifySlice is %v\n", modifySlice) // srcSlice is [a x c]
return modifySlice
}
可以看到,函数传参外面和函数里面的参数的地址相同,都为 0xc00005a150
,所以在函数内修改参数值会影响到原始参数值。
因为这里 srcSlice
本身就是指针地址,所以不需要再用 &
取地址,如果再加上 &
则为指向指针的指针。
对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。以切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,而它的底层数组并不会被拷贝。
3. 函数传参为字典map
在 Go
语言中,任何创建 map
的代码(不管是字面量还是 make
函数)最终调用的都是 runtime.makemap
函数。
小提示:用字面量或者
make
函数的方式创建map
,并转换成makemap
函数的调用,这个转换是Go
语言编译器自动帮我们做的。
从下面的代码可以看到,makemap
函数返回的是一个 *hmap
类型,也就是说返回的是一个指针,所以我们创建的 map
其实就是一个 *hmap
。
src/runtime/map.go
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
//省略无关代码
}
这也是通过 map
类型的参数可以修改原始数据的原因,因为它本质上就是个指针。
package main
import "fmt"
func main() {
srcMap := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Printf("srcMap address is %p\n", srcMap) // srcMap address is 0xc00005a150
modify(srcMap)
fmt.Printf("srcMap is %#v\n", srcMap) // srcMap is map[string]int{"a":1, "b":2, "c":100}
}
func modify(modifyMap map[string]int) map[string]int {
fmt.Printf("modifyMap address is %p\n", modifyMap) // modifyMap address is 0xc00005a150
modifyMap["c"] = 100
fmt.Printf("modifyMap is %#v\n", modifyMap) // modifyMap is map[string]int{"a":1, "b":2, "c":100}
return modifyMap
}
从输出结果可以看到,它们的内存地址一模一样,所以才可以修改原始数据。而且在打印指针的时候,直接使用的是变量 srcMap
和 modifyMap
,并没有用到取地址符 &
,这是因为它们本来就是指针,所以就没有必要再使用 &
取地址了。
注意:这里的
map
可以理解为引用类型,但是它本质上是个指针,只是可以叫作引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。
4. 函数传参为 channel
channel
也可以理解为引用类型,而它本质上也是个指针。
通过下面的源代码可以看到,所创建的 chan
其实是个 *hchan
,所以它在参数传递中也和 map
一样。
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}
严格来说,
Go
语言没有引用类型,但是我们可以把map
、chan
称为引用类型,这样便于理解。除了map
、chan
之外,Go
语言中的函数、接口、slice
切片都可以称为引用类型。指针类型也可以理解为是一种引用类型。
5. 函数传参为 struct
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
s := Student{name: "wohu", age: 20}
fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060
modify(s)
fmt.Printf("s is %v\n", s) // s is {wohu 20}
}
func modify(stu Student) Student {
fmt.Printf("stu address is %p\n", &stu) // stu address is 0xc00000c080
stu.age = 30
fmt.Printf("stu is %v\n", stu) // stu is {wohu 30}
return stu
}
发现它们的内存地址都不一样,这就意味着,在 modify
函数中修改的参数 stu
和 main
函数中的变量 stu
不是同一个,这也是我们在 modify
函数中修改参数 stu
,但是在 main
函数中打印后发现并没有修改的原因。
导致这种结果的原因是 Go
语言中的函数传参都是值传递。 值传递指的是传递原来数据的一份拷贝,而不是原来的数据本身。
以 modify
函数来说,在调用 modify
函数传递变量 stu
的时候,Go
语言会拷贝一个 stu
放在一个新的内存中,这样新的 p
的内存地址就和原来不一样了,但是里面的 name
和 age
是一样的,还是 wohu和 20。这就是副本的意思,变量里的数据一样,但是存放的内存地址不一样。
除了 struct
外,还有浮点型、整型、字符串、布尔、数组,这些都是值类型。
指针类型的变量保存的值就是数据对应的内存地址,所以在函数参数传递是传值的原则下,拷贝的值也是内存地址。现在对以上示例稍做修改,修改后的代码如下:
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
s := Student{name: "wohu", age: 20}
fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060
modify(&s)
fmt.Printf("s is %v\n", s) // s is {wohu 30}
}
func modify(stu *Student) *Student {
fmt.Printf("stu address is %p\n", stu) // stu address is 0xc00000c060
stu.age = 30
fmt.Printf("stu is %v\n", *stu) // stu is &{wohu 30}
return stu
}
所以指针类型的参数是永远可以修改原数据的,因为在参数传递时,传递的是内存地址。
注意:值传递的是指针,也是内存地址。通过内存地址可以找到原数据的那块内存,所以修改它也就等于修改了原数据。
定义的普通变量 stu
是 student
类型的。在 Go
语言中,student
是一个值类型,而 &stu
获取的指针是 *student
类型的,即指针类型。
总结:在 Go
语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 map
、chan
等),那么就可以在函数中修改原始数据。
6. 其它示例
直接上代码
package main
import "fmt"
func main() {
// 示例1。
array1 := [3]string{"a", "b", "c"}
fmt.Printf("The array: %v\n", array1)
array2 := modifyArray(array1)
fmt.Printf("The modified array: %v\n", array2)
fmt.Printf("The original array: %v\n", array1)
fmt.Println()
// 示例2。
slice1 := []string{"x", "y", "z"}
fmt.Printf("The slice: %v\n", slice1)
slice2 := modifySlice(slice1)
fmt.Printf("The modified slice: %v\n", slice2)
fmt.Printf("The original slice: %v\n", slice1)
fmt.Println()
// 示例3。
complexArray1 := [3][]string{
[]string{"d", "e", "f"},
[]string{"g", "h", "i"},
[]string{"j", "k", "l"},
}
fmt.Printf("The complex array: %v\n", complexArray1)
complexArray2 := modifyComplexArray(complexArray1)
fmt.Printf("The modified complex array: %v\n", complexArray2)
fmt.Printf("The original complex array: %v\n", complexArray1)
}
// 示例1。
func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}
// 示例2。
func modifySlice(a []string) []string {
a[1] = "i"
return a
}
// 示例3。
func modifyComplexArray(a [3][]string) [3][]string {
a[1][1] = "s"
a[2] = []string{"o", "p", "q"}
return a
}
- 如果是进行一层修改,即数组的某个完整元素进行修改(指针变化),那么原有数组不变;
- 如果是进行二层修改,即数组中某个元素切片内的某个元素再进行修改(指针未改变),那么原有数据也会跟着改变,传参可以理解是浅copy,参数本身的指针是不同,但是元素指针相同,对元素指针所指向目的的操作会影响传参过程中的原始数据;
7. 函数传参为地址
当变量被当做参数传入调用函数时,是值传递,也称变量的一个拷贝传递。如果传递过来的值是指针,就相当于把变量的地址作为参数传递到函数内,那么在函数内对这个指针所指向的内容进行修改,将会改变这个变量的值。如下边示例代码:
package main
import (
"fmt"
)
func demo(str *string) {
*str = "world"
}
func main() {
var str = "hello"
demo(&str)
fmt.Println("str value is:", str)
}
输出结果:
str value is: world
从上边的输出信息可知,str
变量地址当做参数传入函数后,在函数中对地址所指向内容进行了修改,导致了变量 str
值发生了变化。这个过程能否说明函数调用传递的是指针,而不是变量的拷贝呢?下边通过另一个例子来进行说明:
package main
import (
"fmt"
)
var world = "hello wolrd"
func demo(str *string) {
str = &world
fmt.Println("str in demo func is:", *str)
}
func main() {
var str = "hello"
demo(&str)
fmt.Println("str in main func is:", str)
}
输出结果:
str in demo func is: hello wolrd
str in main func is: hello
上边示例中,str
变量地址被作为参数传入到了函数 demo
中,在函数中对参数进行重新赋值,将 world
变量地址赋值给了参数,函数调用结束后,重新打印变量 str
值,发现值没有被修改。
所以,在函数调用中,变量被拷贝了一份传入函数,函数调用结束后,拷贝的值被丢弃。
如果拷贝的是变量的地址,那么在函数内,其实是通过修改这个地址所指向内存中内容,从而达到修改变量值的目的,但是函数内并不能修改这个变量的地址,也就是 str
变量虽然将地址当做参数传入到 demo
函数中,demo
函数中虽然对这个地址进行了修改,但是在函数调用结束后,拷贝传递进去并被修改的参数被丢弃,str
变量地址未发生变化。
8. 综合示例
package main
import "fmt"
// 用于测试值传递效果的结构体,结构体是拥有多个字段的复杂结构。
type Data struct {
complax []int // complax 为整型切片类型,切片是一种动态类型,内部以指针存在。
instance InnerData // instance 成员以 InnerData 类型作为 Data 的成员 。
ptr *InnerData // 将 ptr 声明为 InnerData 的指针类型
}
// 代表各种结构体字段
type InnerData struct {
a int
}
// 值传递测试函数,该函数的参数和返回值都是 Data 类型。
// 在调用中, Data 的内存会被复制后传入函数,当函数返回时,又会将返回值复制一次,
// 赋给函数返回值的接收变量。
func passByValue(inFunc Data) Data {
// 输出参数的成员情况
fmt.Printf("inFunc value: %+v\n", inFunc)
// 打印inFunc的指针
fmt.Printf("inFunc ptr: %p\n", &inFunc)
// 将传入的变量作为返回值返回,返回的过程将发生值复制。
return inFunc
}
func main() {
// 准备传入函数的结构
in := Data{
complax: []int{1, 2, 3},
instance: InnerData{
5,
},
ptr: &InnerData{1},
}
// 输入结构的成员情况
fmt.Printf("in value: %+v\n", in)
// 输入结构的指针地址
fmt.Printf("in ptr: %p\n", &in)
// 传入结构体,返回同类型的结构体
out := passByValue(in)
// 输出结构的成员情况
fmt.Printf("out value: %+v\n", out)
// 输出结构的指针地址
fmt.Printf("out ptr: %p\n", &out)
}
输出结果:
in value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
in ptr: 0xc000078150
inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
inFunc ptr: 0xc0000781e0
out value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
out ptr: 0xc0000781b0
从运行结果中发现:
- 所有的
Data
结构的指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是将Data
结构传入函数内部,还是通过函数返回值传回Data
都会发生复制行为 。 - 所有的
Data
结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递。 -
Data
结构的ptr
成员在传递过程中保持 一致,表示指针在函数参数值传递中传递的只是指针值,不会复制指针指向的部分。