golang 中slice 、map、chan作为函数参数分析

本文探讨Go语言中slice、map、chan作为函数参数时的行为。虽然Go语言默认值传递,但slice在函数内部的修改会影响其原始值,除非使用append导致扩容,此时会创建新的slice。map作为参数时,内部修改会影响到外部。而chan作为参数,由于其本质是引用类型,函数内的操作会直接影响到外部chan。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写这篇文章之前考虑一个问题:

  • go里面都是值传递,不存在引用传递?
    https://cloud.tencent.com/developer/article/1416563

先来总结一下slice、map、chan的特性:
slice:

func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

其实makeslice64返回是的[]int

  1. slice本身是一个结构体,而不是一个指针,其底层实现是指向数组的指针。
  2. 三要素:type(指针)、len、cap

map:

func makemap(t *maptype, hint int, h *hmap) *hmap
  1. makemap返回的一个指针
  2. go map底层实现是hashmap,并采用链地址方法解决hash冲突的
  3. map的扩容机制:2倍扩容,渐进式扩容。

slice作为参数

先看看例子:

package main

import (
	"fmt"
	"reflect"
)

func modify1(slice []int)  {
	for i:=0;i<len(slice);i++{
		slice[i] = 0
	}

	fmt.Println("Inside  modify1 after append: ", len(slice))
	fmt.Println("Inside  modify1 after append: ", cap(slice))
	fmt.Println("Inside  modify1 after append: ", slice)
}

func modify2(slice []int)  {
	length := len(slice)
	for i:=0;i<length;i++{
		slice = append(slice, 1)
	}

	fmt.Println("Inside  modify2 after append: ", len(slice))
	fmt.Println("Inside  modify2 after append: ", cap(slice))
	fmt.Println("Inside  modify2 after append: ", slice)
}

func main(){

	s1 := make([]int,10,10)
	for i:=0;i<10;i++{
		s1[i] = i
	}
	fmt.Println("makeslcie return type: ", reflect.TypeOf(s1))

	fmt.Println("before modify slice: ", s1)

	modify1(s1)
	fmt.Println("after modify1 slice: ", s1)

    for i:=0;i<10;i++{
		s1[i] = i
	}
	modify2(s1)
	fmt.Println("after modify2 slice: ", len(s1))
	fmt.Println("after modify2 slice: ", cap(s1))
	fmt.Println("after modify2 slice: ", s1)

}

运行看看输出:

makeslcie return type:  []int
before modify slice:  [0 1 2 3 4 5 6 7 8 9]
Inside  modify1 after append:  10
Inside  modify1 after append:  10
Inside  modify1 after append:  [0 0 0 0 0 0 0 0 0 0]
after modify1 slice:  [0 0 0 0 0 0 0 0 0 0]
Inside  modify2 after append:  20
Inside  modify2 after append:  20
Inside  modify2 after append:  [0 1 2 3 4 5 6 7 8 9 1 1 1 1 1 1 1 1 1 1]
after modify2 slice:  10
after modify2 slice:  10
after modify2 slice:  [0 1 2 3 4 5 6 7 8 9]

modify1看,slice作为参数,在函数内部能修改slice,表面看确实能在函数内部修改slice

modify2看,在函数modify2内部用appen操作扩容了slice,len和cap都变成了20,但是再看看后面的输出,modify2并没有修改slice,外部的slice依然没变 len和cap都没变化。

这是怎么回事,函数内部修改slice并没有影响外部slice。 其实go里面都是值传递,makeslice返回的是[]int,传入函数内部会对其拷贝一份,slice内部实现是指向数组的指针的,拷贝的副本部分底层实现也是指向同一内存地址的指针数组。所以内部修改slice的值是能修改的,但是append的并没有修改传入的slice的数组,而是返回一个新的slice的,这要去看看slice的实现和其append的扩容机制。实际上当函数内部不扩容slice,如果修改slice也是修改其指向的底层数组。如果发生扩容会发生数据拷贝,并不会修改其指向的array数组。

如果想在函数内部修改可以传递数组指针就可以了,类似下面这样

func modify2(slice *[]int)

参考资料:https://www.cnblogs.com/junneyang/p/6074786.html

map作为参数

func makemap(t *maptype, hint int, h *hmap) *hmap

在函数内部可以修改map

package main

import "fmt"

func modify1(m map[string]string){

	for key,_ := range m{
		m[key] = "chen"
	}

	m["chen"] = "xun"
	//fmt.Println("修改之后的map:", m)
}


func main(){

	m := map[string]string{ // :=创建
		"name": "小明",
		"age":  "18",
	}

	fmt.Println("修改之前的map:", len(m),  m)
	modify1(m)
	fmt.Println("修改之前的map:", len(m),  m)

}

chan作为参数

func makechan(t *chantype, size int) *hchan 

也就是make() chan的返回值为一个hchan类型的指针,因此当我们的业务代码在函数内对channel操作的同时,也会影响到函数外的数值。

package main

import "fmt"

func test_chan2(ch chan string){
	fmt.Printf("inner: %v, %v\n",ch, len(ch))
	ch<-"b"
	fmt.Printf("inner: %v, %v\n",ch, len(ch))
}

func main() {
	ch := make(chan string, 10)
	ch<- "a"

	fmt.Printf("outer: %v, %v\n",ch, len(ch))
	test_chan2(ch)
	fmt.Printf("outer: %v, %v\n",ch, len(ch))
}

参考资料:https://zhuanlan.zhihu.com/p/54988753

### Goroutine 参数传递机制 在 Go 语言函数参数的传递方式本质上是按值传递。这意味着当我们将变量作为参数传递给函数时,实际上是将该变量的一个副本传入函数内部[^1]。然而,在讨论 Goroutine 的参数传递时,还需要考虑指针类型的特殊行为。 #### 值传递 如果 Goroutine 使用的是普通的值类型(如 `int` 或自定义结构体),那么每次调用 Goroutine 都会创建一个新的拷贝。这种情况下,Goroutine 对其接收到的值所做的任何修改都不会影响原始变量[^2]。 ```go func modifyValue(v int) { v += 10 } func main() { value := 5 go modifyValue(value) fmt.Println(value) // 输出仍然是 5 } ``` 上述代码展示了即使 Goroutine 修改了参数 `v`,也不会改变外部变量 `value` 的值。 #### 引用传递(通过指针) 尽管 Go 不支持传统意义上的引用传递,但它允许通过指针实现类似的效果。当我们把一个指针作为参数传递给 Goroutine 时,实际上是在传递指向内存地址的值。因此,多个 Goroutine 可以共享同一块内存区域并对其进行操作[^3]。 ```go func modifyPointer(ptr *int) { *ptr += 10 } func main() { value := 5 ptr := &value go modifyPointer(ptr) time.Sleep(1 * time.Second) // 确保 Goroutine 执行完成 fmt.Println(*ptr) // 输出可能是 15 } ``` 在这个例子,`modifyPointer` 函数能够更改由 `*ptr` 指向的实际整数值。需要注意的是,为了防止竞争条件 (race condition),应当谨慎处理多线程环境下的并发访问问题。 #### Channel 类型的行为 Channels 是一种特殊的通信工具,它们本身也是引用类型。无论何时我们传递 channel 给另一个 Goroutine ,都是将其引用复制过去而不是整个对象。这样做的好处是可以让不同的 Goroutines 共享同一个 channel 实例来进行消息交换。 ```go ch := make(chan int) // 将 channel 作为一个参数传递给新的 goroutine go func(c chan<- int){ c <- 42 }(ch) result := <- ch fmt.Println(result) // 应打印出 42 ``` 这里可以看到虽然 channel 被当作常规参数对待,但由于它是引用类型,两个独立运行的 Goroutines 还能经由此渠道互相通讯。 ### 总结 Go 编程语言里的所有参数均采用按值方式进行传递。对于基本数据类型来说这意味著完全隔离;而对于复杂的数据结构比如 slice, mapchannels 则因为属于引用类别而表现出近似于 C/C++ 的“按引用”特性。理解这些差异有助于开发者更有效地设计高效且安全的并发程序[^1].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值