Go 语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

但是像 stringslicemap 这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。

不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时 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
}

可以看到,函数传参外面和函数里面的参数的地址不相同,分别为 0xc00005a1500xc00005a180 ,所以在函数内修改参数值并不会影响函数外面的原始参数。

所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值

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
}

从输出结果可以看到,它们的内存地址一模一样,所以才可以修改原始数据。而且在打印指针的时候,直接使用的是变量 srcMapmodifyMap,并没有用到取地址符 &,这是因为它们本来就是指针,所以就没有必要再使用 & 取地址了。

注意:这里的 map 可以理解为引用类型,但是它本质上是个指针,只是可以叫作引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。

4. 函数传参为 channel

channel 也可以理解为引用类型,而它本质上也是个指针。

通过下面的源代码可以看到,所创建的 chan 其实是个 *hchan,所以它在参数传递中也和 map 一样。

func makechan(t *chantype, size int64) *hchan {
    //省略无关代码
}

严格来说,Go 语言没有引用类型,但是我们可以把 mapchan 称为引用类型,这样便于理解。除了 mapchan 之外,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 函数中修改的参数 stumain 函数中的变量 stu 不是同一个,这也是我们在 modify 函数中修改参数 stu,但是在 main 函数中打印后发现并没有修改的原因。

导致这种结果的原因是 Go 语言中的函数传参都是值传递。 值传递指的是传递原来数据的一份拷贝,而不是原来的数据本身。

modify 函数来说,在调用 modify 函数传递变量 stu 的时候,Go 语言会拷贝一个 stu 放在一个新的内存中,这样新的 p 的内存地址就和原来不一样了,但是里面的 nameage 是一样的,还是 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
}

所以指针类型的参数是永远可以修改原数据的,因为在参数传递时,传递的是内存地址。

注意:值传递的是指针,也是内存地址。通过内存地址可以找到原数据的那块内存,所以修改它也就等于修改了原数据。

定义的普通变量 stustudent 类型的。在 Go 语言中,student 是一个值类型,而 &stu 获取的指针是 *student 类型的,即指针类型。

总结:在 Go 语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 mapchan 等),那么就可以在函数中修改原始数据。

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 成员在传递过程中保持 一致,表示指针在函数参数值传递中传递的只是指针值,不会复制指针指向的部分。