Go语言中slice在函数传递中的问题

Go语言中的slice并非像C++的引用传递,而是通过值传递一个包含底层数组指针的结构体。尽管在某些操作下,如修改元素,看起来像是引用传递,但一旦涉及到如`append`这样的操作,由于可能改变底层数组,就会破坏这种假象。Go只有值传递,没有引用传递。通道(channel)的传递同样遵循值传递原则。

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

问题描述

最初想这个问题是因为官方称 slice, map, 函数, 结构体为引用类型…
当时就在想 引用类型 是指C++中 变量引用 一样的意思么,如果一样, 那不就是slice作为函数参数传递时就是像C++中的引用传递一样么, 和Python 也是一样的传递概念么。结果,经过试验,搜索相关的信息, 我发现, 官方的说什么引用类型简直就是坑知道C++的人, 而且我想吐槽下 Go 的设计者在设计这门语言时是不是满脑子想着我一定要与C/C++与众不同!!!!ε=(´ο`*)))唉, 学Go给我一种奇怪的感觉,很别扭,但是要用,还是得学。下面说明下Go slice在函数中传递的问题。

Go语言中slice作为函数参数传递的真相
// 这么一个函数
package main
var slice_out = []int{1,3}

func test_func(slice_in []int) {
	// 此处代码忽略, 随便是啥 
	// 我们讨论slice传入函数时做了什么 !!!
	// 明白了这个我们就全明白了
}

func main() {
	test_func(slice_out)
}

分析一下, 如上一个流程, 我们将全局变量slice_b 传入函数test_func执行到底发生了什么。首先我们得知道slice的真实结构:


// slice数据结构定义
type slice struct {
	array unsafe.Pointer // 指向底层数组的指针
	len int		// slice里面有效元素的个数
	cap int		// slice最大容量 在某些场景可以理解为底层数组的长度大小, 但某些场景不能这么理解.这里不做讨论
}

开始说明, slice传入test_func中时做了什么事情,
第一步,复制了一个slice_out 的结构体赋值给slice_in 变量,
也就是说 slice_inslice_out不是同一个结构体, 它们只不过是结构体元素的值相同的不同结构体,
也就是slice_in.arrayslice_out.array 这两个指针变量的值同一个底层数组的地址!!!
是不是瞬间明白了,因为指针值一致 slice_in[i] = 123 实际上是通过slice_in.array这个指针执行类似于*(slice_in.array+i) = 123的操作去修改了 slice_in.array 这个指针指向的底层数组元素。又因为slice_in 与 slice_out 的array指针指向同一数组,所以可以影响到外面 slice_out的值,所以看起来像是C++的引用传递。但是有个操作就打破了这种假象, 那就是 append函数

// 这里说明下append函数干了什么
var slice_out = []int{1,3}

slice_out = append(slice_out, 1)

解释下上面的步骤发生了什么
首先, 最初slice_out结构体的元素 len = cap = 2, array 指向的是一个长度为2的数组
append函数在执行时会判断当前指向的数组容量(也即是cap的值), 如果数组容量已经满了,那么我们想往里面再append元素就需要进行扩容(也就是申请一个更大的数组,并将slice_out.array这个指针变量的值改为新生成的数组的地址, 同时, len和cap两个元素的值都会更新), 这就是为什么我们要用=符号为slice_out重新赋值(Python程序员是不是感觉非常别扭,一定是这样。。。顺便从C/C++的角度吐槽下, 我靠, 每次append都得对slice_out的结构体内部的值重新赋值,这也太不迅速了不是).
这里我们分析下append是怎么打破上面看起来像引用的假象的

// 假如函数内部是进行的append操作
func test_func(slice_in []int) {
	slice_in = append(slice_in, 1)
}

假如我们函数内部有如上的操作, 你就会发现 slice_out 并未随着slice_in的变化而变化, 至于原因, 就是因为Go中就只有值传递, 没有C++中的引用传递! ! !
一定要记住这一点, 知道这一点, 疑惑瞬间解开
再吐槽下, Go说slice是引用类型这个称呼建议改一改,不然真很容易让人误解

总结slice作为形参的函数设计场景
// 非并发场景
// 第一个场景:
//      我们不需要append操作, 但需要更改slice中的值

func test_func(slice_in []int) {} // 直接值传递即可

// 第二个场景:
// 		我想在函数里对slice为所欲为
// ε=(´ο`*)))唉
func test_func(slice_out_ptr *[]int) {} // 请传递slice的指针并且对指针指向的slice为所欲为

// 或者来慢一点的或者说效率低一些的
func test_func(slice_in []int) []int {
	// 这里是对slice_in为所欲为的代码
	
	// 返回 slice_in 换掉 slice_out内部的值!!! 
	return slice_in
}
// 函数外部则这么干
slice_out = test_func(slice_out)	
// 多赋值过程 且 slice 拷贝过程需要拷贝 24bytes 而指针只要 8Bytes还不用赋值

补充

记住Go只有值传递哈!!!!这是重点,就如同Python里面万物皆对象!!!
C/C++ 嗯~~~,我啥操作都有,想秀你就来

继续补充

经试验 Go 语言中 channel 传递 数据时也是值传递,从底层看, Go语言的编译器会将 chan <- data 之类的代码在内部替换成函数调用的代码,先前我们说过Go语言只有值传递,所以此处也能推出其channel的传递也和函数传参一致为值传递方式 。测试代码如下:

package main

import "fmt"

type Data struct {
	name int
	data *realData
}

type realData struct {
	sex int
	age int
}

var (
	data_ch = make(chan Data, 1)
	rcv_data_ch <-chan Data = data_ch
	send_data_ch chan<- Data = data_ch
	end_sig_chan = make(chan bool, 1)
	change_chan = make(chan bool)
)


func send() {
	fmt.Println("Goroutine send Start !!!")
	ptr_s_data := new(Data)
	ptr_s_data.name = 1
	ptr_s_data.data = &realData{ 1, 2,}
	
	fmt.Printf("send data 初始数据值: %p \n", ptr_s_data)
	fmt.Printf("send data 初始数据内地址值: %p \n", ptr_s_data.data)
	send_data_ch <- *ptr_s_data
	
	_ = <- change_chan
	fmt.Println("send data name 值: ", ptr_s_data.name)
	change_chan <- true
	fmt.Println("Goroutine send end !!!")
}

func rcv() {
	fmt.Println("Goroutine rcv Start !!!")
	ptr_r_data := <- rcv_data_ch
	fmt.Printf("rcv data: %p \n", &ptr_r_data)
	fmt.Printf("rcv data 初始数据内地址值: %p \n", ptr_r_data.data)
	fmt.Println("rcv data name 值: ", ptr_r_data.name)
	
	fmt.Println("开始改变数据内容")
	ptr_r_data.name = 2
	fmt.Println("rcv data name 值: ", ptr_r_data.name)

	change_chan <- true
	_ = <- change_chan

	end_sig_chan <- true
	
	fmt.Println("Goroutine rcv end !!!")
}

func main() {
	fmt.Println("Program Start !!!")
	go send()
	go rcv()
	
	_ = <- end_sig_chan
	
	fmt.Println("Program send end !!!")
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值