为什么说golang参数传递皆是值传递?

什么是值传递 (Pass-by-Value)?

值传递的核心思想是:当一个变量作为参数传递给函数时,函数接收到的是该变量的一个副本 (copy),而不是变量本身。 函数内部对这个副本的任何操作,都不会影响到函数外部的原始变量。

值类型 (Value Types) 的传递

Go 中的基本类型(如 int, float, string, bool)、结构体 (struct) 和数组 (array) 都属于值类型。

当你将这些类型的变量传递给函数时,Go 会创建这些变量的一个完整副本,并将副本交给函数。

示例:

package main

import "fmt"

// 修改 int 副本
func modifyInt(val int) {
	val = 100 // 这里修改的是传入参数 a 的副本
	fmt.Println("Inside modifyInt:", val) // 输出 100
}

// 修改 struct 副本
func modifyStruct(p struct{ x int }) {
	p.x = 200 // 这里修改的是传入参数 pt 的副本的 x 字段
	fmt.Println("Inside modifyStruct:", p.x) // 输出 200
}

func main() {
	// int 示例
	a := 10
	fmt.Println("Before modifyInt:", a) // 输出 10
	modifyInt(a)
	fmt.Println("After modifyInt:", a) // 输出 10,原始 a 未改变

	fmt.Println("---")

	// struct 示例
	type Point struct {
		x int
	}
	pt := Point{x: 20}
	fmt.Println("Before modifyStruct:", pt.x) // 输出 20
	modifyStruct(pt)
	fmt.Println("After modifyStruct:", pt.x) // 输出 20,原始 pt 未改变
}

从输出可以看出,modifyIntmodifyStruct 函数内部对参数的修改,完全没有影响到 main 函数中的原始变量 apt

看似引用传递的类型 (Reference Types - Sort Of)

现在我们来看看指针 (pointer)、切片 (slice)、映射 (map)、通道 (channel) 和函数 (func)。这些类型经常被误认为是“引用传递”,但它们本质上仍然是值传递。传递的是它们内部结构(通常包含一个指向底层数据的指针)的副本。

1. 指针 (*T)

传递指针时,传递的是指针地址的副本。这个副本指向与原始指针相同的内存地址。因此,函数可以通过这个地址副本修改原始指针指向的数据,但不能修改原始指针变量本身(比如让它指向一个新的地址)。

示例:

package main

import "fmt"

func modifyPointer(ptr *int) {
	*ptr = 100 // 通过地址副本,修改了原始变量 b 的值
	fmt.Println("Inside modifyPointer (value pointed to):", *ptr) // 输出 100
	
	// 尝试修改指针本身(让副本指向新的地址)
	// x := 200
	// ptr = &x // 这只会改变 ptr 这个副本,不会改变 main 函数中的 bptr
}

func main() {
	b := 10
	bptr := &b // bptr 存储 b 的地址
	fmt.Println("Before modifyPointer:", b) // 输出 10
	modifyPointer(bptr)
	fmt.Println("After modifyPointer:", b) // 输出 100,原始 b 被修改
}

2. 切片 (slice)

切片本身是一个小结构体,包含三个字段:

  • 指向底层数组的指针 (ptr)
  • 切片的长度 (len)
  • 切片的容量 (cap)

当你传递一个切片时,是把这个结构体进行了值拷贝。这意味着:

  • 函数内外的切片副本共享同一个底层数组(因为它们内部的指针副本指向同一个数组)。
  • 如果在函数内部修改切片的元素 (s[i] = newValue),这个修改会反映到原始切片上,因为它们操作的是同一个底层数组。
  • 如果在函数内部通过 append 操作导致切片扩容(底层数组被重新分配),函数内的切片副本会指向新的底层数组,而外部的原始切片仍然指向旧数组。这时,函数内对新数组的修改不会影响外部。
  • 如果在函数内对切片变量本身重新赋值 (s = anotherSlice),只会改变函数内副本的指向,不影响外部。

示例:

package main

import "fmt"

func modifySlice(s []int) {
	if len(s) > 0 {
		s[0] = 99 // 修改共享底层数组的元素
	}
	fmt.Println("Inside modifySlice (first element):", s[0]) // 输出 99

    // 尝试 append (如果未扩容,会影响外部;如果扩容,则不影响)
	// s = append(s, 4) 
    // fmt.Println("Inside modifySlice (after append):", s) 
}

func main() {
	sl := []int{1, 2, 3}
	fmt.Println("Before modifySlice:", sl) // 输出 [1 2 3]
	modifySlice(sl)
	fmt.Println("After modifySlice:", sl) // 输出 [99 2 3],原始切片元素被修改
}

3. 映射 (map) 和 通道 (channel)

映射和通道与切片类似,它们内部也包含一个指向底层数据结构的指针。

传递 mapchannel 时,传递的是它们内部指针的副本。因此,在函数内部对 mapchannel 进行的操作(如添加键值对、发送/接收数据)会影响到原始的 mapchannel,因为它们都指向同一个底层实现。同样,在函数内对 mapchannel 变量重新赋值也不会影响外部。

示例 (Map):

package main

import "fmt"

func modifyMap(m map[string]int) {
	m["b"] = 200 // 操作共享的底层数据结构
	fmt.Println("Inside modifyMap:", m) // 输出 map[a:1 b:200]
	
	// 尝试重新赋值 (只会改变副本 m)
	// m = make(map[string]int)
	// m["c"] = 300
}

func main() {
	mp := map[string]int{"a": 1}
	fmt.Println("Before modifyMap:", mp) // 输出 map[a:1]
	modifyMap(mp)
	fmt.Println("After modifyMap:", mp) // 输出 map[a:1 b:200],原始 map 被修改
}

结论

Go 语言的参数传递机制始终是值传递

  • 对于值类型int, string, struct, array 等),传递的是值的完整副本,函数内部的修改不影响外部。
  • 对于指针、切片、映射、通道、函数等类型,传递的也是它们内部表示(通常是一个包含指针的结构)的副本。由于这个副本通常指向与原始变量相同的底层数据结构,所以在函数内部通过这个副本对底层数据的修改会影响到外部的原始变量,这造成了“类似引用传递”的效果。

记住:一切皆是值传递,只是传递的值有时是指针(或者包含指针的结构)的副本而已。

打 赏