没错,Go 语言的函数参数没有引用传递方式

Go语言函数参数的值传递解析

这篇文章想浅浅地讲解 Go 语言函数参数传递的值拷贝。

一句话观点

Go语言中所有传递都是值传递,严格来说并不存在引用传递的概念。传递指针只是传递指针的值,并不是引用传递,只不过通过指针可以间接修改变量的值,从而达到类似引用传递的效果。

值传递

值传递就是将参数的副本传递给函数,因此在函数内部修改参数的值,不会影响到原始变量的值。

  
func modifyValue(person Person) {  
    person.Name = "Alice"  
}

func Test4(t *testing.T) {  
    person1 := Person{Name: "Bob"}  
    modifyValue(person1)  
    fmt.Println("值传递:", person1.Name) // 输出: 值传递: Bob
}

在这个例子中,modifyValue 函数接收 person 的副本,修改它的字段值并不会影响原来的 person

引用传递

要实现引用传递,可以通过传递指针来实现。

func modifyReference(person *Person) {  
    person.Name = "Alice"  
}

func Test4(t *testing.T) {  
	person2 := &Person{Name: "Bob"}  
	modifyReference(person2)  
	fmt.Println("引用传递:", person2.Name) // 输出: 引用传递: Alice
}

在这个例子中,modifyReference 函数接收的是 person 指针类型的副本,修改副本指向的值,会影响到原来的。

值传递例子

下面再看一个值传递的例子。恰好最近在读《Go 语言精进之路》这本书,书里有一段讲解的方法的本质问题,案例是下面这段代码:

type field struct{ name string }  
  
func (p *field) print() {  
    fmt.Println(p.name)  
}  
func main() {  
    data1 := []*field{{"one"}, {"two"}, {"three"}}  
    for _, v := range data1 {  
       go v.print()  
    }  
    data2 := []field{{"four"}, {"five"}, {"six"}}  
    for _, v := range data2 {  
       go v.print()  
    }  
    time.Sleep(3 * time.Second)  
}

我对上面的代码进行简单改造,改为单测方法,打印输出内存地址,改造的代码如下:

  
func (p *field) print() {  
    fmt.Printf("%p %s \n", p, p.name)  
}  
  
func Test1(t *testing.T) {  
    data1 := []*field{{"one"}, {"two"}, {"three"}}  
    for _, v := range data1 {  
       go (*field).print(v)  
    }  
    
    data2 := []field{{"four"}, {"five"}, {"six"}}  
    for _, v := range data2 {  
       go (*field).print(&v)  
    }  
    time.Sleep(3 * time.Second)  
}

运行单测,看下结果:

0xc000026590 six 
0xc000026590 six 
0xc000026590 six 
0xc000026540 two 
0xc000026530 one 
0xc000026550 three 

如果你奇怪为什么第二段代码输出的全是 six,那么可以接着看下面的原因分析。

先要知道的是, for range 循环中会重复使用同一个变量 v

在第一段,data1 是一个指针数组([]*field),v 每次迭代时是指向数组中不同元素的指针。由于 v 是指针,每个元素的地址自然是不同的,因此打印出的地址不同,每个元素都可以打印出来。

在第二段,data2 是一个结构体数组([]field),而 v 是数组元素的副本。当你取 &v 时,每次获取的都是这个循环中的同一个变量的地址,所以 &v 的地址是相同的。但每次迭代时,这个地址指向的值会被更新为 data2 中当前元素的副本,在最后执行的时候,&v 执行的值就是 six

如果想让第二段输出所有元素,可以每次迭代拷贝一个 v 副本,复制给变量 s,如下:

for _, v := range data2 {  
    s := v  
    go (*field).print(&s)  
}

执行单测,输出结果如下:

0xc0000265d0 six 
0xc000026590 four 
0xc0000265b0 five 
0xc000026540 two 
0xc000026550 three 
0xc000026530 one 

在这个例子中,每次迭代都会创建新的变量 s,内存地址是不同的,所以可以全部输出。

Over!

<think>嗯,用户问的是Go语言中哪些类型是引用类型。我得先回忆一下Go的类型系统。记得Go有值类型和引用类型的区别,对吧?引用类型的话,通常是指那些变量存储的是数据的引用,而不是数据本身。比如切片、映射、通道这些应该算是引用类型。 首先,切片(slice),没错,切片底层其实是一个结构体,包含指向数组的指针、长度和容量。所以当传递切片的时候,传递的是这个结构体的副本,里面的指针还是指向同一个数组,所以修改会影响到底层数组。这就是引用类型的行为。 然后是映射(map),对的,映射也是引用类型。创建映射的时候返回的是一个指向hmap结构的指针,所以当赋值给另一个变量时,两个变量共享同一个映射,修改其中一个会影响另一个。不过要注意,不能直接用未初始化的映射,得用make或者字面量初始化。 接下来是通道(channel),通道也是引用类型。创建通道后,变量实际上是指向hchan结构体的指针。传递通道时,传递的是指针的副本,所以多个变量指向同一个通道,发送和接收数据都是通过这个通道进行的。 还有函数类型(func)应该也算引用类型,不过可能用户更关心的是数据结构类型。函数作为参数传递时,传递的是函数的引用,但可能用户不太会关注这个。另外接口类型(interface)内部包含类型和值的指针,所以行为上类似引用类型,但严格来说是否算引用类型可能有不同说法,可能需要确认一下。 另外指针类型(*T)本身是值类型,因为指针变量存储的是地址的值,传递指针时复制的是地址值,但通过指针可以修改指向的数据。不过指针类型本身不算引用类型,但用户可能会混淆。需要明确区分。 总结一下,Go中的引用类型主要是切片、映射、通道,还有函数和接口可能也被包括在内。但根据官方文档或常见说法,可能更明确的是前三种。需要确保准确,避免误导用户。另外要提醒用户,虽然这些类型是引用类型,但他们的变量本身在传递时是按值传递的,但因为内部有指针,所以表现像引用类型。这点很重要,避免用户误解。 可能用户在学习Go的时候遇到了变量传递的问题,想了解哪些类型在传递时不会被复制数据本身。所以需要详细解释每个类型的行为,并举例说明。比如修改切片元素会影响所有引用该切片的变量,而映射的赋值也会共享底层数据。 还要注意错误处理,比如未初始化的映射是nil,不能直接使用,需要在例子中说明正确用法。同时,可能用户会混淆指针和引用类型,需要明确两者的区别。 最后,结构体是否算引用类型?应该不算,结构体是值类型,但如果结构体包含引用类型的字段,比如切片,那么结构体复制时,切片字段的引用会被共享。这点也可以提一下,帮助用户全面理解。 总之,回答要结构清晰,分点列出每个引用类型,解释其行为,并给出示例。同时澄清可能的误区,比如指针和引用类型的区别,确保用户正确理解。</think>在 Go 语言中,**引用类型**的变量存储的是对底层数据结构的引用(类似于指针),而不是直接持有数据本身。这类类型的特点是: 1. **赋值和传参时传递的是引用**,多个变量可能共享同一份底层数据 2. **修改操作会影响所有引用该数据的变量** 以下是 Go 中的引用类型及其特性分析: --- ### 一、切片(Slice) ```go s1 := []int{1, 2, 3} s2 := s1 // s2 与 s1 共享底层数组 s2[0] = 99 // 修改会影响 s1 fmt.Println(s1) // 输出 [99 2 3] ``` - 底层结构包含:指向数组的指针 + 长度 + 容量 - 修改元素会影响所有共享该切片的变量 - 但**扩容操作**(如 `append` 导致容量不足)会创建新数组,此时与原切片解绑 --- ### 二、映射(Map) ```go m1 := make(map[string]int) m1["a"] = 1 m2 := m1 // m2 与 m1 共享底层哈希表 m2["a"] = 100 // 修改会影响 m1 fmt.Println(m1) // 输出 map[a:100] ``` - 底层实现为哈希表的指针 - 必须使用 `make` 初始化后才能操作 - 并发读写需要加锁(或使用 `sync.Map`) --- ### 三、通道(Channel) ```go ch := make(chan int, 3) chCopy := ch // chCopy 与 ch 指向同一通道 go func() { chCopy <- 42 // 向通道发送数据 }() fmt.Println(<-ch) // 从原通道接收,输出 42 ``` - 用于协程间通信 - 关闭通道会影响所有引用该通道的变量 - 零值为 `nil`,直接操作会导致阻塞 --- ### 四、函数(Function) ```go func foo() {} f1 := foo f2 := f1 // f1 和 f2 指向同一个函数 ``` - 函数变量存储的是函数入口地址的引用 - 函数闭包捕获的变量也遵循引用语义 --- ### 五、接口(Interface) ```go var w io.Writer = os.Stdout wCopy := w // 共享接口的 (type, value) 二元组 w.Write([]byte("hello")) // 两者都输出到标准输出 ``` - 接口值存储类型信息和值的指针 - 空接口 `interface{}`(Go 1.18+ 用 `any`)同理 --- ### ⚠️ 易混淆点:指针类型 虽然指针(`*T`)也存储内存地址,但它是**值类型**: ```go a := 10 p1 := &a p2 := p1 // p2 是 p1 的副本(值拷贝),但都指向同一地址 *p2 = 20 // 通过指针修改会影响 a fmt.Println(a) // 输出 20 ``` - 指针变量本身是值传递(复制的是地址值) - 与引用类型的本质区别在于类型系统层面的处理方式 --- ### 最佳实践建议 1. **修改共享数据时使用互斥锁**(`sync.Mutex`) 2. 对切片进行**防御性复制**(`copy()` 函数)以避免意外修改 3. 通过 `make` 初始化时合理设置容量(对切片/通道) 4. 使用 `defer` 确保关闭资源(如文件、通道) 需要特别注意:Go引用类型在参数传递时仍然是**值传递**(复制引用本身),这与 Java/Python 等语言中真正的引用传递有本质区别。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LeiCodeX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值