说说不知道的Golang中参数传递

本文深入探讨了Go语言中函数参数传递的机制,特别是map、channel和slice这三种内置数据结构在函数调用过程中的行为。揭示了尽管Go语言采用值传递,但由于这三种数据结构的特殊性,能在函数内部修改其内容。

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

本文由云+社区发表

导言

几乎每一个C++开发人员,都被面试过有关于函数参数是值传递还是引用传递的问题,其实不止于C++,任何一个语言中,我们都需要关心函数在参数传递时的行为。在golang中存在着map、channel和slice这三种内建数据类型,它们极大的方便着我们的日常coding。然而,当这三种数据结构作为参数传递的时的行为是如何呢?本文将从这三个内建结构展开,来介绍golang中参数传递的一些细节问题。

背景

首先,我们直接的来看一个简短的示例,下面几段代码的输出是什么呢?

//demo1
package main

import "fmt"

func test_string(s string){
	fmt.Printf("inner: %v, %v\n",s, &s)
	s = "b"
	fmt.Printf("inner: %v, %v\n",s, &s)
}

func main() {
	s := "a"
	fmt.Printf("outer: %v, %v\n",s, &s)
	test_string(s)
	fmt.Printf("outer: %v, %v\n",s, &s)
}

上文的代码段,尝试在函数test_string()内部修改一个字符串的数值,通过运行结果,我们可以清楚的看到函数test_string()中入参的指针地址发生了变化,且函数外部变量未被内部的修改所影响。因此,很直接的一个结论呼之欲出:golang中函数的参数传递采用的是:值传递

//output
outer: a, 0x40e128
inner: a, 0x40e140
inner: b, 0x40e140
outer: a, 0x40e128

那么是不是到这儿就回答完,本文就结束了呢?当然不是,请再请看看下面的例子:当我们使用的参数不再是string,而改为map类型传入时,输出结果又是什么呢?

//demo2
package main

import "fmt"

func test_map(m map[string]string){
	fmt.Printf("inner: %v, %p\n",m, m)
	m["a"]="11"
	fmt.Printf("inner: %v, %p\n",m, m)
}

func main() {

	m := map[string]string{
		"a":"1",
		"b":"2",
		"c":"3",
	}
	
	fmt.Printf("outer: %v, %p\n",m, m)
	test_map(m)
	fmt.Printf("outer: %v, %p\n",m, m)
}

根据我们前文得出的结论,按照值传递的特性,我们毫无疑问的猜想:函数外两次输出的结果应该是相同的,同时地址应该不同。然而,事实却正是相反:

//output
outer: map[a:1 b:2 c:3], 0x442260
inner: map[a:1 b:2 c:3], 0x442260
inner: map[a:11 b:2 c:3], 0x442260
outer: map[b:2 c:3 a:11], 0x442260

没错,在函数test_map()中对map的修改再函数外部生效了,而且函数内外打印的map变量地址竟然一样。做技术开发的人都知道,在源代码世界中,如果地址一样,那就必然是同一个东西,也就是说:这俨然成为了一个引用传递的特性了。

两个示例代码的结果竟然截然相反,如果上述的内容让你产生了疑惑,并且你希望彻底的了解这过程中发生了什么。那么请阅读完下面的内容,跟随作者一起从源码透过现象看本质。本文接下来的内容,将对golang中的map、channel和slice三种内建数据结构在作为函数参数传递时的行为进行分析,从而完整的解析golang中函数传递的行为。

迷惑人心的Map

Golang中的map,实际上就是一个hashtable,在这儿我们不需要了解其详细的实现结构。回顾一下上文的例子我们首先通过make()函数(运算符:=是make()的语法糖,相同的作用)初始化了一个map变量,然后将变量传递到test_map()中操作。

众所周知,在任何语言中,传递指针类型的参数才可以实现在函数内部直接修改内容,如果传递的是值本身的,会有一次拷贝发生(此时函数内外,该变量的地址会发生变化,通过第一个示例可以看出),因此,在函数内部的修改对原外部变量是无效的。但是,demo2示例中的变量却完全没有拷贝发生的迹象,那么,我们是否可以大胆的猜测,通过make()函数创建出来的map变量会不会实际上是一个指针类型呢?这时候,我们便需要来看一下源代码了:

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
	if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
		hint = 0
	}
	...

上面是golang中的make()函数在map中通过makemap()函数来实现的代码段,可以看到,与我们猜测一致的是:makemap()返回的是一个hmap类型的指针*hmap。也就是说:test_map(map)实际上等同于test_map(*hmap)。因此,在golang中,当map作为形参时,虽然是值传递,但是由于make()返回的是一个指针类型,所以我们可以在函数哪修改map的数值并影响到函数外。

我们也可以通过一个不是很恰当的反例来证明这点:

//demo3
package main

import "fmt"

func test_map2(m map[string]string){
	fmt.Printf("inner: %v, %p\n",m, m)
	m = make(map[string]string, 0)
	m["a"]="11"
	fmt.Printf("inner: %v, %p\n",m, m)
}

func main() {
	var m map[string]string//未初始化
	
	fmt.Printf("outer: %v, %p\n",m, m)
	test_map2(m)
	fmt.Printf("outer: %v, %p\n",m, m)
}

由于在函数test_map2()外仅仅对map变量m进行了声明而未初始化,在函数test_map2()中才对map进行了初始化和赋值操纵,这时候,我们看到对于map的更改便无法反馈到函数外了。

//output
outer: map[], 0x0
inner: map[], 0x0
inner: map[a:11], 0x442260
outer: map[], 0x0

跟风的Channel

在介绍完map类型作为参数传递时的行为后,我们再来看看golang的特殊类型:channel的行为。还是通过一段代码来来入手:

//demo4
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))
}

结果如下,我们看到,在函数内往channel中塞入数值,在函数外可以看到channel的size发生了变化:

//output
outer: 0x436100, 1
inner: 0x436100, 1
inner: 0x436100, 2
outer: 0x436100, 2

在golang中,对于channel有着与map类似的结果,其make()函数实现源代码如下:

func makechan(t *chantype, size int) *hchan {
	elem := t.elem
  ...

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

与众不同的Slice

对于golang中slice的行为,可以总结一句话:与众不同。首先,我们来看下golang中对于slice的make实现代码

func makeslice(et *_type, len, cap int) slice {
  ...

我们发现,与map和channel不同的是,sclie的make函数返回的是一个内建结构体类型slice的对象,而并非一个指针类型,其中内建slice的数据结构如下:

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

也就是说,如果采用slice在golang中传递参数,在函数内对slice的操作是不应该影响到函数外的。那么,对于下面的这段示例代码,运行的结果又是什么呢?

//demo5
package main

import "fmt"

func main() {
	
	sl := []string{
		"a",
		"b",
		"c",
	}
	
	fmt.Printf("%v, %p\n",sl, sl)
	test_slice(sl)
	fmt.Printf("%v, %p\n",sl, sl)
}


func test_slice(sl []string){
	fmt.Printf("%v, %p\n",sl, sl)
	sl[0] = "aa"
	//sl = append(sl, "d")
	fmt.Printf("%v, %p\n",sl, sl)
}

通过运行结果,我们看到,在函数内部对slice中的第一个元素的数值修改成功的返回到了test_slice()函数外层!与此同时,通过打印地址,我们发现也显示了是同一个地址。到了这儿,似乎又一个奇怪的现象出现了:makeslice()返回的是值类型,但是当该数值作为参数传递时,在函数内外的地址却未发生变化,俨然一副指针类型。

//output
[a b c], 0x442260
[a b c], 0x442260
[aa b c], 0x442260
[aa b c], 0x442260

这时候,我们还是回归源码,回顾一下上面列出的golang内部slice结构体的特点。没错,细心地读者可能已经发现,内部slice中的第一个元素用来存放数据的结构是个指针类型,一个指向了真正的存放数据的指针!因此,虽然指针拷贝了,但是指针所指向的地址却未更改,而我们在函数内部修改了指针所指向的地方的内容,从而实现了对元素修改的目的了。

让我们再进阶一下上面的示例,将注释的那行代码打开:

sl = append(sl, "d")

再重新运行上面的代码,得到的结果又有了新的变化:

//output
[a b c], 0x442280
[a b c], 0x442280
[aa b c d], 0x442280
[aa b c], 0x442280

函数内我们修改了slice中一个已有元素,同时向slice中append了另一个元素,结果在函数外部:

  • 修改的元素生效了;
  • append的元素却消失了。

其实这就是由于slice的结构引起的了。我们都知道slice类型在make()的时候有个len和cap的可选参数,在上面的内部slice结构中第二和第三个成员变量就是代表着这俩个参数的含义。我们已知原因,数据部分由于是指针类型,这就决定了在函数内部对slice数据的修改是可以生效的,因为值传递进去的是指向数据的指针。而同一时刻,表示长度的len和容量的cap均为int类型,那么在传递到函数内部的就仅仅只是一个副本,因此在函数内部通过append修改了len的数值,但却影响不到函数外部slice的len变量,从而,append的影响便无法在函数外部看到了。

解释到这儿,基本说清了golang中map、channel和slice在函数传递时的行为和原因了,但是,喜欢提问的读者可能一直觉得有哪儿是怪怪的,这个时候我们来完整的整理一下已经的关于slice的信息和行为:

  1. makeslice()出来的一定是个结构体对象,而不是指针;
  2. 函数内外打印的slice地址一致;
  3. 函数体内对slice中元素的修改在函数外部生效了;
  4. 函数体内对slice进行append操作在外部没有生效;

没错了,对于问题1、3和4我们应该都已经解释清楚了,但是,关于第2点为什么函数内外对于这三个内建类型变量的地址打印却是一致的?我们已经更加确定了golang中的参数传递的确是值类型,那么,造成这一现象的唯一可能就是出在打印函数fmt.Printf()中有些小操作了。因为我们是通过%p来打印地址信息的,为此,我们需要关注的是fmt包中fmtPointer():

func (p *pp) fmtPointer(value reflect.Value, verb rune) {
	var u uintptr
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
		u = value.Pointer()
	default:
		p.badVerb(verb)
		return
	}
	...
}

我们发现在fmtPointer()中,对于map、channel和slice,都被当成了指针来处理,通过Pointer()函数获取对应的值的指针。我们知道channel和map是因为make函数返回的就已经是指针了,无可厚非,但是对于slice这个非指针,在value.Pointer()是如何处理的呢?

// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0.  If the slice is empty but non-nil the return value is non-zero.
func (v Value) Pointer() uintptr {
	// TODO: deprecate
	k := v.kind()
	switch k {
	case Chan, Map, Ptr, UnsafePointer:
		return uintptr(v.pointer())
	case Func:
		...
	case Slice:
		return (*SliceHeader)(v.ptr).Data
	}
	...
}

果不其然,在Pointer()函数中,对于Slice类型的数据,返回的一直是指向第一个元素的地址,所以我们通过fmt.Printf()中%p来打印Slice的地址,其实打印的结果是内部存储数组元素的首地址,这也就解释了问题2中为什么地址会一致的原因了。

总结

通过上述的一系列总结,我们可以很高兴的确定的是:在golang中的传参一定是值传递了!

然而golang隐藏了一些实现细节,在处理map,channel和slice等这些内置结构的数据时,其实处理的是一个指针类型的数据,也是因此,在函数内部可以修改(部分修改)数据的内容。

但是,这些修改得以实现的原因,是因为数据本身是个指针类型,而不是因为golang采用了引用传递,注意二者的区别哦~

此文已由作者授权腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

转载于:https://my.oschina.net/qcloudcommunity/blog/3002157

<think>首先,用户的问题是:“我想了解Golang参数传递的方式,特别是是否采用值传递。请问golang参数传递机制值传递还是指针传递”。用户关注的是Golang参数传递机制,特别是它是值传递还是指针传递。从系统指令中,我有一些约束:-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识,例如:[^1]。-所有行内数学表达式用$...$,独立公式用$$...$$格式(但在这个主题上,可能没有数学表达式)。-参考提供的引用,用户给了我几个引用:-[^1]:讨论Golang的值传递和指针传递,特别提到切片中包含指针,实际上是值传递中的指针传递。-[^2]:解释了值传递和引用传递的区别,并说Golang只有值传递,即使是传递指针,也是传递指针的拷贝。-[^3]:类似地,描述了值传递和引用传递的定义。-[^4]:展示了通过指针传递修改值的示例。引用[^1]、[^2]、[^3]和[^4]都指向Golang参数传递机制。关键点是:-Golang只有值传递。-这意味着函数参数总是被复制。-对于基本类型如int、float,传递的是值。-对于指针类型,传递的是指针的拷贝(值),但指针指向的内存是共享的。-对于切片、map、channel等,它们内部有指针,所以传递时复制的是描述符(descriptor),但数据在堆上是共享的。这与我作为AI的知识一致:在Go语言中,函数参数传递始终是值传递。即使是指针,也是传递指针的值(一个地址),而是引用传递。我的回答需要结构清晰:-先回答问题:Golang使用值传递。-解释原因和机制。-给出示例。-自然地添加引用标识。-生成相关问题。结构化回答:1.**引言**:直接回应问题。2.**解释机制**:-描述值传递是什么。-对比指针传递和引用传递(但Golang没有引用传递)。-解释Golang中的实现:-基本类型:传递值。-指针类型:传递指针的值,可以修改指向的数据。-引用类型:如切片、map,它们类似于指针,传递时复制描述符。-强调只有值传递。3.**引用支持**:自然地插入引用。-例如,引用[^1]说:“golang函数只有值传递,即使是传递指针,也是将指针拷贝一份”。-在相关段落后添加[^1]、[^2]等。4.**示例**:添加代码示例来展示。-如一个传递int值,另一个传递指针。5.**总结**:重申关键点。6.**生成相关问题**:在末尾添加,作为系统指令的一部分。生成相关问题:-系统指令说“生成相关问题”。-在用户可见层末尾添加。-基于主题:-例如:-Golang中的切片传递机制是怎样的?-为什么在Golang中传递指针更高效?-如何避免在函数中意外修改数据?确保回答真实可靠:我将基于标准Golang知识。最终回答结构:-回答主体。-“§§相关问题§§”部分列出问题。-引用标识在回答中。使用中文。</think> ### Golang参数传递机制:值传递还是指针传递? 在Golang中,函数参数传递的方式**始终是值传递**。这意味着无论参数是什么类型(包括基本类型、指针或引用类型如切片或map),函数调用时都会创建一个参数值的拷贝,并将这个拷贝传递到函数内部。这与传统的引用传递同,Golang没有直接支持引用传递的方式[^2][^3]。下面我将逐步解释Golang参数传递机制,并辅以示例来说明。 #### 1. **值传递的本质** - **定义**:值传递(Pass by Value)意味着函数调用时,参数的**值被复制一份**,作为函数的参数。修改函数内部的参数(形参)会影响原始变量(实参),除非使用指针。 - 例如,传递一个基本类型如`int`时,函数内对参数的修改会改变外部变量。 - Golang的函数参数总是以这种方式工作,即使在传递指针或引用类型时也是如此,因为它复制的是指针值或描述符的拷贝[^1][^2]。 - **为什么是值传递**:这与Golang的设计哲学有关,它强调简单性和安全性,避免意外副作用。C++等语言有引用传递,但Golang通过值传递配合指针来实现类似效果[^2][^4]。 #### 2. **值传递在同类型中的表现** - **基本类型(如int、string)**: - 传递时,整个值的字节被复制。 - 函数内部修改参数会影响外部变量。 - 示例代码: ```go package main import "fmt" func modifyValue(a int) { a = 20 // 只修改副本 } func main() { x := 10 modifyValue(x) fmt.Println(x) // 输出: 10,外部变量未变 } ``` 在示例中,`modifyValue`函数内部改变的是复制的值,外部变量`x`受影响。这证实了值传递机制[^3]。 - **指针类型(如*int)**: - 参数传递的是指针的**值(地址)**,即指针本身的拷贝,而是引用。 - 虽然指针值被复制,但指针指向的内存地址是相同的,因此函数内部可以通过指针修改外部数据。 - 示例代码(基于引用[^4]): ```go package main import "fmt" func modifyViaPointer(a *int) { *a = *a + 1 // 通过指针修改指向的值 } func main() { y := 1 modifyViaPointer(&y) fmt.Println(y) // 输出: 2,外部变量被修改 } ``` 这里,传递的指针是地址值的拷贝(值传递),但由于指针指向相同的内存位置,外部变量`y`被改变。这就是所谓的“值传递中的指针传递”[^1]。 - **引用类型(如切片、map、channel)**: - 这些类型内部包含指针或其他引用机制(例如切片包含指向底层数组的指针),但传递时,**复制的是描述符(descriptor),而是数据本身**。 - 函数内部修改引用类型(如添加元素到切片)会影响外部变量,因为描述符复制后指向的底层数据未变。 - 示例代码(切片传递): ```go package main import "fmt" func modifySlice(s []int) { s[0] = 100 // 修改底层数组 s = append(s, 5) // 一定会影响外部,因为append可能创建新切片 } func main() { slice := []int{1, 2, 3} modifySlice(slice) fmt.Println(slice) // 输出: [100 2 3],因为修改了底层数组 } ``` 在这个案例中,切片`s`传递时被复制(值传递),但复制的描述符共享底层数组,所以数组内容被修改[^1][^3]。 #### 3. **为什么Golang没有引用传递** - 引用传递(Pass by Reference)在语言如C++中可直接传递地址并影响实参,但Golang通过值传递实现类似控制。 - 好处:提高代码可预测性(避免意外修改),简化内存管理。 - 缺点:对于大型对象,值传递可能导致性能问题(因为复制开销),此时建议用指针传递作为优化[^2][^4]。 #### 4. **关键总结** - Golang**只有值传递**,即所有参数传递都是创建值的拷贝。 - 指针和引用类型在传递时也体现值传递:指针是地址值的拷贝,但能通过解引用修改数据;引用类型传递描述符的拷贝,但共享底层数据。 - 这机制确保了代码安全性和一致性,但开发者需根据需求选择使用指针(for 修改外部状态或性能)或直接值传递(for 避免副作用)[^1][^2]。 通过上述解释,您可以看到Golang参数传递机制是值传递优先,配合指针来模拟部分引用功能。如果您有更多具体场景(如并发安全或性能优化),可以进一步讨论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值