函数存在于各种编程语言中,是可重用的,用于执行指定任务的代码块。C#中函数(方法)的参数传递默认的是值传递,还有引用传递和输出传递,其中后两种需要在参数类型前面对应加上refout限制符;除了主要的值传递与引用传递外,C#数据类型还分为值类型与引用类型。通过排列组合,C#在函数的定义到使用就有如下几种情况:

  • 值传递值类型

  • 引用传递值类型

  • 值传递引用类型

  • 引用传递引用类型

实际上out输出传递也是通过引用传递参数,实质与ref一致,不赘述。

那么go语言呢?

1.开门见山—只有值传递

go语言也有 值类型 引用类型,但是参数传递只有按值传参。因为所谓的传引用(指针):

  • 如果实参是值类型:传递给函数的参数是一个指针,而指针代表的是实参的内存地址;
  • 如果实参是引用类型:像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型,默认使用引用类型引用的指针进行传递,而不是引用类型变量本身的指针,到了函数内部,也是使用指针的副本,所以依旧是值传递。

这个跟C#还是有区别的。C#的引用传递,传引用,那就真正的引用传递:

static void Main()
{
    int n = 5;
    Test(ref x);
}
private static void Test(ref int x)
{
    x=10;
}
  • n与x的地址都是一样。

引用传递:参数是原变量的指针,引用传递参数和原变量的内存地址相同。但是go不是这样的。请往下看。

2.循序渐进—展示

对于值类型的传递,博主不做任何验证,主要以引用类型slice切片为例,对go语言函数的参数传递进行测试。

2.1 切片

func main() {
	var sliceTest []int
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 0x0 只声明
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018

	sliceTest = []int{1, 2, 3, 4}								// 赋值
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018
	fmt.Printf("the addr of slice var itself:%v\n", sliceTest)  // [1 2 3 4]
	fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
    
    // test1
	test1(sliceTest) 
	fmt.Println(sliceTest) 										//[999 2 3 4]
}

// 普通切片
func test1(slice []int) {
	fmt.Printf("-------start test func1----------\n")
	fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120060
	fmt.Printf("the value of func var:%p\n", slice)        //值传递,底层数组地址([0]的地址) 【0x1400013c020】
	slice[0] = 999
	fmt.Println(slice)									   //[999 2 3 4]
	fmt.Printf("-------end test func1----------\n")
}

上述函数,整个参数传递,是传递的切片的指针(底层数组,在切片中索引为0的元素的指针),而函数的参数只是对引用类型的指针进行了拷贝,传递参数和原变量的内存地址不同,没错,这就是值传递。

2.2 切片的指针

func main() {
	var sliceTest []int
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 0x0 只声明
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018

	sliceTest = []int{1, 2, 3, 4}								// 赋值
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Printf("the addr of slice var itself:%v\n", sliceTest)  // [1 2 3 4]
	fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
    
    // test1
	test1(sliceTest) 
	fmt.Println(sliceTest) 										//[999 2 3 4]
    
    // test2
	test2(&sliceTest)
	fmt.Println(sliceTest)										//[888 2 3 4]
}

// 切片指针
func test2(slice *[]int) {
	fmt.Printf("-------start test func2----------\n")
	fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000134020
	fmt.Printf("the value of func var:%p\n", slice)        //值传递,传递的是main中的切片变量地址,[0x14000120018]
	fmt.Printf("the value of pointer:%p\n", *slice)        //指针运算符获得切片变量的值 依然是切片的底层数组地址 【0x1400013c020】

	(*slice)[0] = 888
	fmt.Println(*slice)									   //[888 2 3 4]

	fmt.Printf("-------end test func2----------\n")

}

上述函数,整个参数传递,是传递的切片变量的指针,函数的参数只是对切片变量的指针进行了拷贝,传递参数和原变量的内存地址仍然不同,没错,这依然是值传递。

2.3 返回切片—内部修改元素

func main() {
	var sliceTest []int
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 0x0 只声明
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018

	sliceTest = []int{1, 2, 3, 4}								// 赋值
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Printf("the addr of slice var itself:%v\n", sliceTest)  // [1 2 3 4]
	fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
    
    // test1
	test1(sliceTest) 
	fmt.Println(sliceTest) 										//[999 2 3 4]
    
    // test2
	test2(&sliceTest)
	fmt.Println(sliceTest)
    
    // test3
	sliceTest = test3(sliceTest)
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Println(sliceTest)										// [777 2 3 4]
}

// 普通切片 返回切片
func test3(slice []int) []int {
	fmt.Printf("-------start test func3----------\n")
	fmt.Printf("the addr of func var itself:%p\n", &slice) //这是在函数中内部的变量,新的变量,所以是重新分配的
	fmt.Printf("the value of func var:%p\n", slice)        //值却依然是切片的底层数组地址 【0x1400013c020】
	slice[0] = 777
	fmt.Println(slice)									   //[777 2 3 4]
	fmt.Printf("-------end test func3----------\n")
	return slice
}

上述函数,整个参数传递,依然是值传递,就不赘述这个问题。 我们引入了新的操作:在函数内部对切片按索引进行了修改操作。 这样就影响原切片。

2.4 函数内部操作切片—append

func main() {
	var sliceTest []int
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 0x0 只声明
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018

	sliceTest = []int{1, 2, 3, 4}								// 赋值
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Printf("the addr of slice var itself:%v\n", sliceTest)  // [1 2 3 4]
	fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
    
    // test1
	test1(sliceTest) 
	fmt.Println(sliceTest) 										//[999 2 3 4]
    
    // test2
	test2(&sliceTest)
	fmt.Println(sliceTest)
    
    // test3
	sliceTest = test3(sliceTest)
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Println(sliceTest)										// [777 2 3 4]
    
    // test4
	test4(sliceTest)
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    //array 地址  0x1400013c020
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 0x14000120018
	fmt.Println(sliceTest)                                      // [666 2 3 4]
}

func test4(slice []int) {
	fmt.Printf("-------start test func4----------\n")
	fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120180
	fmt.Printf("the value of func var:%p\n", slice)        // 值却依然是切片的底层数组地址 【0x1400013c020】
	slice[0] = 666
	slice = append(slice, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
	slice[0] = 555
	fmt.Println(slice)                                     // [555 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
	fmt.Printf("the addr of func var itself:%p\n", &slice) /// 这是在函数中内部的变量 不变,0x14000120180
	fmt.Printf("the value of func var:%p\n", slice)        //  0x1400013e000--变了
	fmt.Printf("-------end test func4----------\n")
}

上述函数,整个参数传递,依然是值传递,同样不赘述。 我们引入了新的操作:在函数内部对切片进行append。 切片还是像之前博文【Golang】来几道题以加强切片知识那样,切片append后的长度超越了容量,这时底层数组将会更换,所以append之前的操作会影响原切片,之后便不再影响。

2.5 返回切片—内部append

func main() {
	var sliceTest []int
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 0x0 只声明
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 0x14000120018

	sliceTest = []int{1, 2, 3, 4}								// 赋值
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Printf("the addr of slice var itself:%v\n", sliceTest)  // [1 2 3 4]
	fmt.Printf("the addr of slice var itself:%v\n", &sliceTest) // &[1 2 3 4]
    
    // test1
	test1(sliceTest) 
	fmt.Println(sliceTest) 										//[999 2 3 4]
    
    // test2
	test2(&sliceTest)
	fmt.Println(sliceTest)
    
    // test3
	sliceTest = test3(sliceTest)
	fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) 【0x1400013c020】
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Println(sliceTest)										// [777 2 3 4]
    

	// test5
	sliceTest = test5(sliceTest)
    fmt.Printf("the addr of array to slice:%p\n", sliceTest)    // 底层数组地址([0]的地址) ---{0x1400013e0b0}--
	fmt.Printf("the addr of slice var itself:%p\n", &sliceTest) // 切片变量 [0x14000120018]
	fmt.Println(sliceTest)                                      // [444 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
}

func test5(slice []int) []int {
	fmt.Printf("-------start test func5----------\n")
	fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量,新的变量,所以是重新分配的 0x14000120210
	fmt.Printf("the value of func var:%p\n", slice)        // 【0x1400013c020】
	slice[0] = 444
	slice = append(slice, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
	fmt.Println(slice)                                     // [444 2 3 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17]
	fmt.Printf("the addr of func var itself:%p\n", &slice) // 这是在函数中内部的变量 不变,0x14000120210 
    fmt.Printf("the value of func var:%p\n", slice)        // {0x1400013e0b0}--变了
	fmt.Printf("-------end test func5----------\n")
	return slice
}

上述函数,整个参数传递,依然是值传递,同样不赘述。 我们同样引入了新的操作:在函数内部对切片进行append。 只是,这次我们函数返回了append后的切片,就是已经更换底层数组的切片。

3.结论

  • Go语言中所有的传参都是值传递(传值)。

    • 函数内拷贝的内容是非引用类型(intstringstruct等这些),这样就在函数中就无法修改原内容数据;
    • 函数内拷贝的内容是引用类型(指针、mapslicechan等这些),这样就可以修改原内容数据。
  • 是否可以修改原内容数据,和传值、传引用没有必然的关系。

    • C#中,传引用肯定是可以修改原内容数据的,但是值传递,传递是引用类型,也可以办到,比如类的实例;
    • 同样,Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。
  • 参数是切片的函数,一定要当心函数内部的操作对切片影响的程度。

    • 切片的底层数组的更换;
    • 如果对经过函数执行后的切片,仍有后续操作;
    • 总之,建议统一采用函数返回切片的方式;

结语:了解底层原理,才能让我们找到规避问题的方案。