本文为原创文章,转载注明出处,欢迎扫码关注公众号
flysnow_org
或者网站www.flysnow.org/,第一时间看后续精彩文章。以为好的话,顺手分享到朋友圈吧,感谢支持。数组
对于了解一门语言来讲,会关心咱们在函数调用的时候,参数究竟是传的值,仍是引用?bash
其实对于传值和传引用,是一个比较古老的话题,作研发的都有这个概念,可是可能不是很是清楚。对于咱们作Go语言开发的来讲,也想知道究竟是什么传递。函数
那么咱们先来看看什么是值传递,什么是引用传递。网站
传值的意思是:函数传递的老是原来这个东西的一个副本,一副拷贝。好比咱们传递一个int
类型的参数,传递的实际上是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。ui
对于int这类基础类型咱们能够很好的理解,它们就是一个拷贝,可是指针呢?咱们以为能够经过它修改原来的值,怎么会是一个拷贝呢?下面咱们看个例子。spa
func main() {
i:=10
ip:=&i
fmt.Printf("原始指针的内存地址是:%p\n",&ip)
modify(ip)
fmt.Println("int值被修改了,新值为:",i)
}
func modify(ip *int){
fmt.Printf("函数里接收到的指针的内存地址是:%p\n",&ip)
*ip=1
}
复制代码
咱们运行,能够看到输入结果以下:指针
原始指针的内存地址是:0xc42000c028
函数里接收到的指针的内存地址是:0xc42000c038
int值被修改了,新值为: 1
复制代码
首先咱们要知道,任何存放在内存里的东西都有本身的地址,指针也不例外,它虽然指向别的数据,可是也有存放该指针的内存。code
因此经过输出咱们能够看到,这是一个指针的拷贝,由于存放这两个指针的内存地址是不一样的,虽然指针的值相同,可是是两个不一样的指针。cdn
经过上面的图,能够更好的理解。 首先咱们看到,咱们声明了一个变量i
,值为10
,它的内存存放地址是0xc420018070
,经过这个内存地址,咱们能够找到变量i
,这个内存地址也就是变量i
的指针ip
。blog
指针ip
也是一个指针类型的变量,它也须要内存存放它,它的内存地址是多少呢?是0xc42000c028
。 在咱们传递指针变量ip
给modify
函数的时候,是该指针变量的拷贝,因此新拷贝的指针变量ip
,它的内存地址已经变了,是新的0xc42000c038
。
不论是0xc42000c028
仍是0xc42000c038
,咱们均可以称之为指针的指针,他们指向同一个指针0xc420018070
,这个0xc420018070
又指向变量i
,这也就是为何咱们能够修改变量i
的值。
Go语言(Golang)是没有引用传递的,这里我不能使用Go举例子,可是能够经过说明描述。
以上面的例子为例,若是在modify
函数里打印出来的内存地址是不变的,也是0xc42000c028
,那么就是引用传递。
了解清楚了传值和传引用,可是对于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
类型本质上和map
类型是同样的,这里不作过多的介绍,参考下源代码:
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}
复制代码
chan
也是一个引用类型,和map
相差无几,make
返回的是一个*hchan
。
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里只有传值(值传递)。
本文为原创文章,转载注明出处,欢迎扫码关注公众号
flysnow_org
或者网站www.flysnow.org/,第一时间看后续精彩文章。以为好的话,顺手分享到朋友圈吧,感谢支持。