Slice 也是值传递么?
看个例子吧:
func TestSliceReference(t *testing.T) { var args = []int64{1,2,3} fmt.Printf("切片args的地址: %p\n",args) modifiedNumber3(args) fmt.Println(args) } func modifiedNumber3(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) args[0] = 10 }
运行结果:
=== RUN TestSliceReference 切片args的地址: 0xc000016120 形参切片的地址 0xc000016120 [10 2 3] --- PASS: TestSliceReference (0.00s) PASS
发现没有,切片的地址和形参的地址为啥一样, 难道也问题?不是值传递?上面可以看到,我们并没有用取址符 & 来进行地址转换,就把 slice 打印出来来,再测试一下:
func TestSliceReference2(t *testing.T) { 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) modifiedNumber4(args) fmt.Println(args) } func modifiedNumber4(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0]) fmt.Printf("直接对形参切片args取地址%v \n",&args) args[0] = 10 }
运行结果
=== RUN TestSliceReference2 切片args的地址: 0xc0000ee030 切片args第一个元素的地址: 0xc0000ee030 直接对切片args取地址&[1 2 3] 形参切片的地址 0xc0000ee030 形参切片args第一个元素的地址: 0xc0000ee030 直接对形参切片args取地址&[1 2 3] [10 2 3] --- PASS: TestSliceReference2 (0.00s) PASS
可以看到的是 使用 & 地址符进行取址是无效的,而且使用 %p 取出的地址是和第一个元素地址是一样的。为啥这样?看 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('&') 这个代码打印地址输出结果带有 & , 这个就是为啥
fmt.Printf("切片args的地址: %p \n",args)
打印出来的是一个地址 0xc0000ee030。
为啥打印出来的切片中还会包含 "[]" 呢?
看下 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(']') }
因为递归调用了 p.printValue(a, verb, depth+1) 进行打印, 这个就是为啥
fmt.Printf("直接对切片args取地址%v \n",&args)
输出直接对切片args取地址
&[1 2 3]
还有个问题, 为啥 %p 输出的内存地址 和 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 } ...... 省略部分代码 // 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 { ....... 省略部分代码
上面有一句话,If v’s Kind is Slice, the returned pointer is to the first。意思是对于 slice 类型,返回的是元素第一个元素的地址,这里正好解释上面为什么 fmt.Printf("切片args的地址:%p \n",args)和fmt.Printf("形参切片的地址 %p \n",args)打印出来的地址是一样的,因为args是引用类型,所以他们都返回slice这个结构里的第一个元素的地址。
Slice 的结构
//runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }
slice 是一个结构体, 第一个元素是指针类型,这个指针指向的是底层数组的第一个指针。所以当 slice 类型的时候, fmt.Printlnf() d打印的是第一个元素的地址。其实也是指针处理,只不过指针的存放的内容是第一个元素的内存地址,但是这个指针的在传递过程中是会拷贝的。
slice 其实也是值传递,为啥引用类型传递可以修改原内容的数据, 因为底层默认传递的指向第一个元素地址的指针。容易混淆的是 fmt.printf() 打印的是第一个这个传递指针对应的内容,而不是存储指针的地址,会给人一种错觉,以为是引用传递。
map 是值传递么?
map 没有明显的指针
func TestMapReference(t *testing.T) { 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 }
运行结果:
=== RUN TestMapReference 原始map的内存地址是:0xc00000e038 函数里接收到map的内存地址是:0xc00000e040 map值被修改了,新值为: map[asong:9] --- PASS: TestMapReference (0.00s) PASS
接着看一下 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()
map 使用 make 函数返回的是一个 hmap 类型的指针,func modifiedAge(persons map[string]int) 其实和 func modifiedAge(person *hmap) 意思是一样的,这样实际上传递也是使用了指针的副本进行传递, 属于值传递, map 也是引用类型,但是传递的类型不是引用,也是值传递,传递的是指针的拷贝。
chan 是值传递么
func TestChanReferencr(t *testing.T) { 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) } }
运行结果
=== RUN TestChanReferencr 原始chan的内存地址是:0xc000120028 函数里接收到chan的内存地址是:0xc000120030 true --- PASS: TestChanReferencr (2.00s) PASS
看到实参和形参地址返回的是不一样的,chan 的底层实现
// 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")) }
其实可以看到 和 map 有点类型,通过 mak 函数,返回的也是一个 hchan 类型的指针,实际上在操作中,传递的是指针的副本。属于值传递。
struct 是值传递么?
看个例子
type Persons struct { Name string Age int64 } func TestStructRerence(t *testing.T) { per := Persons{ Name: "asong", Age: int64(8), } fmt.Printf("原始struct地址是:%p\n", &per) modifiedAge2(per) fmt.Println(per) } func modifiedAge2(per Persons) { fmt.Printf("函数里接收到struct的内存地址是:%p\n", &per) per.Age = 10 }
运行结果:
=== RUN TestStructRerence 原始struct地址是:0xc00000c048 函数里接收到struct的内存地址是:0xc00000c060 {asong 8} --- PASS: TestStructRerence (0.00s) PASS
可以看到 struct 就是值传递, 没有指针发现没?当你修改指为10 的时候,发现没有修改成功,原来 struct 中 Age 值,还是 8
总结
Go 语言中的参数传递都是值传递,虽然 Go 语言中都是值传递,但我们还是可以修改原参数中的内容的,因此传递的参数是引用类型。