前言

哈喽,大家好,我是asong。今天女朋友问我,小松子,你知道Go语言参数传递是传值还是传引用吗?哎呀哈,我竟然被瞧不起了,我立马一顿操作,给他讲的明明白白的,小丫头片子,还是太嫩,大家且听我细细道来~~~。​

实参与形参数

go
func printNumber(args ...int)
args

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。

举例如下:

func main()  {
 var args int64= 1
 printNumber(args)  // args就是实际参数
}

func printNumber(args ...int64)  { //这里定义的args就是形式参数
	for _,arg := range args{
		fmt.Println(arg) 
	}
}

什么是值传递

int

什么是引用传递

C++

golang是值传递

我们先写一个简单的例子验证一下:

func main()  {
 var args int64= 1
 modifiedNumber(args) // args就是实际参数
 fmt.Printf("实际参数的地址 %p\n", &args)
 fmt.Printf("改动后的值是  %d\n",args)
}

func modifiedNumber(args int64)  { //这里定义的args就是形式参数
	fmt.Printf("形参地址 %p \n",&args)
	args = 10
}

运行结果:

形参地址 0xc0000b4010 
实际参数的地址 0xc0000b4008
改动后的值是  1
gogo
func main()  {
 var args int64= 1
 addr := &args
 fmt.Printf("原始指针的内存地址是 %p\n", addr)
 fmt.Printf("指针变量addr存放的地址 %p\n", &addr)
 modifiedNumber(addr) // args就是实际参数
 fmt.Printf("改动后的值是  %d\n",args)
}

func modifiedNumber(addr *int64)  { //这里定义的args就是形式参数
	fmt.Printf("形参地址 %p \n",&addr)
	*addr = 10
}

运行结果:

原始指针的内存地址是 0xc0000b4008
指针变量addr存放的地址 0xc0000ae018
形参地址 0xc0000ae028 
改动后的值是  10

所以通过输出我们可以看到,这是一个指针的拷贝,因为存放这两个指针的内存地址是不同的,虽然指针的值相同,但是是两个不同的指针。

args10xc0000b4008argsargsaddraddr0xc0000ae018addrmodifiedNumberaddr0xc0000ae0280xc0000ae0180xc0000ae0280xc0000b40080xc0000b4008argsargs
gomodifieNumberchanmapslice
slice

先看一段代码:

func main()  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p\n",args)
 modifiedNumber(args)
 fmt.Println(args)
}

func modifiedNumber(args []int64)  {
	fmt.Printf("形参切片的地址 %p \n",args)
	args[0] = 10
}

运行结果:

切片args的地址: 0xc0000b8000
形参切片的地址 0xc0000b8000 
[10 2 3]
&slice
func main()  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p \n",args)
 fmt.Printf("切片args第一个元素的地址: %p \n",&args[0])
 fmt.Printf("直接对切片args取地址%v \n",&args)
 modifiedNumber(args)
 fmt.Println(args)
}

func modifiedNumber(args []int64)  {
	fmt.Printf("形参切片的地址 %p \n",args)
	fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0])
	fmt.Printf("直接对形参切片args取地址%v \n",&args)
	args[0] = 10
}

运行结果:

切片args的地址: 0xc000016140 
切片args第一个元素的地址: 0xc000016140 
直接对切片args取地址&[1 2 3] 
形参切片的地址 0xc000016140 
形参切片args第一个元素的地址: 0xc000016140 
直接对形参切片args取地址&[1 2 3] 
[10 2 3]
fmt.Printf
fmt包,print.go中的printValue这个方法,截取重点部分,因为`slice`也是引用类型,所以会进入这个`case`:
case reflect.Ptr:
		// pointer to array or slice or struct? ok at top level
		// but not embedded (avoid loops)
		if depth == 0 && f.Pointer() != 0 {
			switch a := f.Elem(); a.Kind() {
			case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
				p.buf.writeByte('&')
				p.printValue(a, verb, depth+1)
				return
			}
		}
		fallthrough
	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
		p.fmtPointer(f, verb)
p.buf.writeByte('&')&&slicep.printValue(a, verb, depth+1)[]printValue
case reflect.Array, reflect.Slice:
//省略部分代码
} else {
			p.buf.writeByte('[')
			for i := 0; i < f.Len(); i++ {
				if i > 0 {
					p.buf.writeByte(' ')
				}
				p.printValue(f.Index(i), verb, depth+1)
			}
			p.buf.writeByte(']')
		}
fmt.Printf("直接对切片args取地址%v \n",&args)直接对切片args取地址&[1 2 3]fallthroughfmt.Poniter
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
	}
...... 省略部分代码
// 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:
		if v.flag&flagMethod != 0 {
 ....... 省略部分代码
sliceslicefmt.Printf("切片args的地址: %p \n",args)fmt.Printf("形参切片的地址 %p \n",args)argsslicesliceslice
slice
//runtime/slice.go
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
sliceslicefmt.Printfsliceslice
sliceslicefmt.printf

map也是值传递吗?

mapslicemap
func main()  {
	persons:=make(map[string]int)
	persons["asong"]=8

	addr:=&persons

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

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

看一眼运行结果:

原始map的内存地址是:0xc00000e028
函数里接收到map的内存地址是:0xc00000e038
map值被修改了,新值为: map[asong:9]
map

为了解决我们的疑惑,我们从源码入手,看一看什么原理:

//src/runtime/map.go
// 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 {
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()
makehmap*hmapfunc modifiedAge(person map[string]int)func modifiedAge(person *hmap)makemap

chan是值传递吗?

老样子,先看一个例子:

func main()  {
	p:=make(chan bool)
	fmt.Printf("原始chan的内存地址是:%p\n",&p)
	go func(p chan bool){
		fmt.Printf("函数里接收到chan的内存地址是:%p\n",&p)
		//模拟耗时
		time.Sleep(2*time.Second)
		p<-true
	}(p)

	select {
	case l := <- p:
		fmt.Println(l)
	}
}

再看一看运行结果:

原始chan的内存地址是:0xc00000e028
函数里接收到chan的内存地址是:0xc00000e038
true
mapchan
// src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
	elem := t.elem

	// compiler checks this but be safe.
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}

	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

makehchan*hchanmapfun (p chan bool)fun (p *hchan)
gostructstruct
struct
struct
func main()  {
	per := Person{
		Name: "asong",
		Age: int64(8),
	}
	fmt.Printf("原始struct地址是:%p\n",&per)
	modifiedAge(per)
	fmt.Println(per)
}

func modifiedAge(per Person)  {
	fmt.Printf("函数里接收到struct的内存地址是:%p\n",&per)
	per.Age = 10
}
PersonAge108

前文总结

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

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

有的小伙伴会在这里还是懵逼,因为你把引用类型和传引用当成一个概念了,这是两个概念,切记!!!

出个题考验你们一下

欢迎在评论区留下你的答案~~~

既然你们都知道了golang只有值传递,那么这段代码来帮我分析一下吧,这里的值能修改成功,为什么使用append不会发生扩容?

func main() {
	array := []int{7,8,9}
	fmt.Printf("main ap brfore: len: %d cap:%d data:%+v\n", len(array), cap(array), array)
	ap(array)
	fmt.Printf("main ap after: len: %d cap:%d data:%+v\n", len(array), cap(array), array)
}

func ap(array []int) {
	fmt.Printf("ap brfore:  len: %d cap:%d data:%+v\n", len(array), cap(array), array)
  array[0] = 1
	array = append(array, 10)
	fmt.Printf("ap after:   len: %d cap:%d data:%+v\n", len(array), cap(array), array)
}

后记

好啦,这一篇文章到这就结束了,我们下期见~~。希望对你们有用,又不对的地方欢迎指出,可添加我的golang交流群,我们一起学习交流。

结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。

我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin]即可下载。

翻译了一份Machinery中文文档,会定期进行维护,有需要的小伙伴们后台回复[machinery]即可获取。

golangvx

推荐往期文章: