golang的参数传递
一、总体概述
在 Go 语言中,所有的参数传递都是值传递。也就是说,当我们将一个变量作为参数传递给函数或方法时,实际上是将该变量的副本传递过去。
然而,某些类型的变量在复制时会复制其引用(指针),这使得在函数中对这些变量的修改会影响到原始变量。这种行为类似于引用传递,但实际上仍然是值传递。
二、值类型和引用类型
1. 值类型
定义:值类型的变量直接包含其值,赋值或传参时会复制整个值。
包括:
基本类型:
int
、float
、bool
、string
、complex
等。结构体(
struct
)数组(
array
)
2. 引用类型
定义:引用类型的变量存储的是对底层数据的引用,赋值或传参时复制的是引用(指针)。
包括:
切片(
slice
)映射(
map
)通道(
channel
)接口(
interface
)函数(
func
)指针(
pointer
)
三、详细说明
1. 值类型
(1)基本类型
特性:赋值和传参时,复制整个值,对副本的修改不会影响原始变量。
示例:
func modifyValue(x int) {
x = 100
}
func main() {
a := 10
modifyValue(a)
fmt.Println(a) // 输出:10
}
解释:
modifyValue
函数接收的是a
的副本,对x
的修改不影响a
。
(2)数组(array)
特性:数组是定长的,赋值和传参时会复制整个数组。
示例:
func modifyArray(arr [3]int) {
arr[0] = 100
}
func main() {
a := [3]int{1, 2, 3}
modifyArray(a)
fmt.Println(a) // 输出:[1 2 3]
}
解释:
modifyArray
中的arr
是a
的副本,对其修改不影响a
。
(3)结构体(struct)
特性:结构体也是值类型,赋值和传参时会复制整个结构体。
示例:
type Person struct {
Name string
Age int
}
func modifyPerson(p Person) {
p.Name = "Bob"
}
func main() {
person := Person{Name: "Alice", Age: 30}
modifyPerson(person)
fmt.Println(person.Name) // 输出:Alice
}
解释:
modifyPerson
中的p
是person
的副本,对其修改不影响原始person
。
2. 引用类型
(1)切片(slice)
特性:切片是一个结构体,包含指向底层数组的指针、长度和容量。赋值和传参时复制的是切片结构体,但其内部指针仍指向同一个底层数组。
示例:
func modifySlice(s []int) {
s[0] = 100
}
func main() {
a := []int{1, 2, 3}
modifySlice(a)
fmt.Println(a) // 输出:[100 2 3]
}
解释:尽管
s
是a
的副本,但它们共享同一个底层数组,因此对元素的修改会影响原始切片。
(2)映射(map)
特性:映射是引用类型,底层实现为指针。赋值和传参时复制的是指针,对映射的修改会影响原始映射。
示例:
g
解释:
modifyMap
中对m
的修改会影响myMap
,因为它们指向同一个映射。
(3)通道(channel)
特性:通道是引用类型,底层实现为指针。赋值和传参时复制的是指针。
示例:
func sendData(ch chan int) {
ch <- 100
}
func main() {
ch := make(chan int, 1)
sendData(ch)
fmt.Println(<-ch) // 输出:100
}
解释:
sendData
中的ch
和main
中的ch
指向同一个通道。
(4)接口(interface)
特性:接口本身是一个包含类型和值的组合。赋值和传参时复制的是接口本身,但接口内部可能包含指向引用类型的值。
示例:
type Speaker interface {
Speak()
}
type Person struct {
Name string
}
func (p *Person) Speak() {
fmt.Println("Hello, my name is", p.Name)
}
func modifyInterface(s Speaker) {
if p, ok := s.(*Person); ok {
p.Name = "Bob"
}
}
func main() {
person := &Person{Name: "Alice"}
modifyInterface(person)
person.Speak() // 输出:Hello, my name is Bob
}
解释:
modifyInterface
中的s
和main
中的person
指向同一个Person
实例。
(5)指针(pointer)
特性:指针存储的是变量的内存地址。赋值和传参时复制的是指针的值(地址)。
示例:
gfunc modifyPointer(p *int) {
*p = 100
}
func main() {
a := 10
modifyPointer(&a)
fmt.Println(a) // 输出:100
}
解释:
modifyPointer
中的p
指向a
的地址,对其解引用修改会影响a
。
四、总结
值传递:
基本类型、数组、结构体等值类型变量,赋值和传参时会复制整个值,对副本的修改不影响原始变量。
引用行为的值传递:
切片、映射、通道、接口、指针、函数等引用类型变量,赋值和传参时复制的是引用(指针)的副本。
由于这些引用类型内部包含指针,指向底层数据结构,因此对它们的操作可能影响原始数据。
注意事项:
切片的扩容:当切片发生扩容时,可能会创建新的底层数组,此时新切片与原始切片不再共享底层数组。
映射的并发安全:映射在并发访问时需要加锁保护,否则可能发生竞态条件。
结构体中的引用类型字段:即使结构体是值类型,如果其中包含引用类型的字段,复制时这些字段仍指向相同的底层数据。
五、实用建议
理解类型特性:在编写代码时,清楚地了解每种类型的传递方式和行为,避免意外修改原始数据。
必要时使用指针:对于需要在函数中修改原始值类型变量的情况,可以使用指针传递。
避免不必要的副本:对于大型结构体或数组,频繁的复制会带来性能开销,可以考虑使用指针或切片。
并发编程注意事项:引用类型在并发环境下需要注意数据的一致性和安全性。
六、总结一句话
在 Go 语言中,所有参数传递都是值传递,但对于包含指向底层数据的引用类型变量(如切片、映射、通道等),由于复制的是指向同一底层数据的引用,所以对它们的操作可能会影响到原始数据,这种行为看起来类似于引用传递。