Go语言函数参数传递类型详解

golang 同时被 2 个专栏收录
13 篇文章 0 订阅
11 篇文章 0 订阅

到底是值传递还是引用传递?

golang默认都是值传递引用,无论参数是指针还是普通参数传递,默认都是值拷贝传递

什么是值传递

函数传递的总是原来这个东西的一个副本,一副拷贝。比如我们传递一个int类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。

在网上看了一篇帖子,自己实际操作了进行验证,以int类型的数据进行验证,代码如下

func modify(point *int) {
	fmt.Printf("函数里接收到的指针的内存地址是:%p\n", &point)
	*point = 1
}

func funcB() {
	i := 10
	fmt.Printf("原始指针的内存地址是A:%p\n", &i)
	ip := &i
	fmt.Printf("原始指针的内存地址是:%p\n", &ip)
	modify(ip)
	fmt.Println("int值被修改了,新值为:", i)
	fmt.Printf("原始指针的内存地址是B:%p\n", &i)
}

运行结果如下

原始指针的内存地址是A:0xc0000b4288
原始指针的内存地址是:0xc0000b0020
函数里接收到的指针的内存地址是:0xc0000b0028
int值被修改了,新值为: 1
原始指针的内存地址是B:0xc0000b4288

对于任何存放在内存里面的东西,基本都是需要有内存地址,指针也是一样的,需要存放指针地址,用来指向实际数据地址,从结果来看,虽然指针的值是相同的,但是指针的地址却是不同的

在这里插入图片描述
这里面的变量i,值为10,系统申请的内存地址为0xc0000b4288
指针ip也是一个指针类型的变量,它也需要内存存放它,它的内存地址是多少呢?是0xc0000b0020。 在我们传递指针变量ip给modify函数的时候,是该指针变量的拷贝,所以新拷贝的指针变量ip,它的内存地址已经变了,是新的0xc0000b0028。

不管是0xc0000b0020还是0xc0000b0028,我们都可以称之为指针的指针,他们指向同一个指针0xc0000b4288,这个0xc0000b4288又指向变量i,这也就是为什么我们可以修改变量i的值。

什么是传引用(引用传递)

Go语言(Golang)是没有引用传递的

迷惑Map

了解清楚了传值和传引用,但是对于Map类型来说,可能觉得还是迷惑,一来我们可以通过方法修改它的内容,二来它没有明显的指针。

func main() {
	persons:=make(map[string]int)
	persons["张三"]=19

	mp:=&persons

	fmt.Printf("原始map的内存地址是:%p\n",mp)
	modify(persons)
	fmt.Println("map值被修改了,新值为:",persons)
}

 func modify(p map[string]int){
	 fmt.Printf("函数里接收到map的内存地址是:%p\n",&p)
	 p["张三"]=20
 }

打印输出

原始map的内存地址是:0xc42000c028
函数里接收到map的内存地址是:0xc42000c038
map值被修改了,新值为: map[张三:20]

两个内存地址是不一样的,所以这又是一个值传递(值的拷贝),那么为什么我们可以修改Map的内容呢?先不急,我们先看一个自己实现的struct。

func main() {
	p:=Person{"张三"}
	fmt.Printf("原始Person的内存地址是:%p\n",&p)
	modify(p)
	fmt.Println(p)
}

type Person struct {
	Name string
}

 func modify(p Person) {
	 fmt.Printf("函数里接收到Person的内存地址是:%p\n",&p)
	 p.Name = "李四"
 }

运行打印输出:

原始Person的内存地址是:0xc4200721b0
函数里接收到Person的内存地址是:0xc4200721c0
{张三}

我们发现,我们自己定义的Person类型,在函数传参的时候也是值传递,但是它的值(Name字段)并没有被修改,我们想改成李四,发现最后的结果还是张三。

这也就是说,map类型和我们自己定义的struct类型是不一样的。我们尝试把modify函数的接收参数改为Person的指针。

func main() {
	p:=Person{"张三"}
	modify(&p)
	fmt.Println(p)
}

type Person struct {
	Name string
}

 func modify(p *Person) {
	 p.Name = "李四"
 }

在运行查看输出,我们发现,这次被修改了。我们这里省略了内存地址的打印,因为我们上面int类型的例子已经证明了指针类型的参数也是值传递的。 指针类型可以修改,非指针类型不行,那么我们可以大胆的猜测,我们使用make函数创建的map是不是一个指针类型呢?看一下源代码:

// makemap implements a Go map creation 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 bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    //省略无关代码
}```

通过查看src/runtime/hashmap.go源代码发现,的确和我们猜测的一样,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。 现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),和我们前面第一节什么是值传递里举的func modify(ip *int)的例子一样,可以参考分析。

所以在这里,Go语言通过make函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。这里的map可以理解为引用类型,但是记住引用类型不是传引用。

chan类型

chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:

func makechan(t *chantype, size int64) *hchan {
    //省略无关代码
}

chan也是一个引用类型,和map相差无几,make返回的是一个*hchan。

和map、chan都不一样的slice

slice和map、chan都不太一样的,一样的是,它也是引用类型,它也可以在函数中修改对应的内容。

func main() {
	ages:=[]int{6,6,6}
	fmt.Printf("原始slice的内存地址是%p\n",ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int){
	fmt.Printf("函数里接收到slice的内存地址是%p\n",ages)
	ages[0]=1
}

运行打印结果,发现的确是被修改了,而且我们这里打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换。

这就可以证明make的slice也是一个指针了吗?不一定,也可能fmt.Printf把slice特殊处理了。

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
	}
	//省略部分代码
}

通过源代码发现,对于chan、map、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 Slice:
		return (*SliceHeader)(v.ptr).Data
	}
}

很明显了,当是slice类型的时候,返回是slice这个结构体里,字段Data第一个元素的地址。

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

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

所以我们通过%p打印的slice变量ages的地址其实就是内部存储数组元素的地址,slice是一种结构体+元素指针的混合类型,通过元素array(Data)的指针,可以达到修改slice里存储元素的目的。

所以修改类型的内容的办法有很多种,类型本身作为指针可以,类型里有指针类型的字段也可以。

单纯的从slice这个结构体看,我们可以通过modify修改存储元素的内容,但是永远修改不了len和cap,因为他们只是一个拷贝,如果要修改,那就要传递*slice作为参数才可以。

func main() {
	i:=19
	p:=Person{name:"张三",age:&i}
	fmt.Println(p)
	modify(p)
	fmt.Println(p)
}

type Person struct {
	name string
	age  *int
}

func (p Person) String() string{
	return "姓名为:" + p.name + ",年龄为:"+ strconv.Itoa(*p.age)
}

func modify(p Person){
	p.name = "李四"
	*p.age = 20
}

运行打印输出结果为:

姓名为:张三,年龄为:19
姓名为:张三,年龄为:20

通过这个Person和slice对比,就更好理解了,Person的name字段就类似于slice的len和cap字段,age字段类似于array字段。在传参为非指针类型的情况下,只能修改age字段,name字段无法修改。要修改name字段,就要把传参改为指针,比如:

modify(&p)
func modify(p *Person){
	p.name = "李四"
	*p.age = 20
}

这样name和age字段双双都被修改了。

所以slice类型也是引用类型。

小结

最终我们可以确认的是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。

这里也要记住,引用类型和传引用是两个概念。

再记住,Go里只有传值(值传递)

  • 2
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

冰释物语

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值