flysnow_org

对于了解一门语言来说,会关心我们在函数调用的时候,参数到底是传的值,还是引用?

其实对于传值和传引用,是一个比较古老的话题,做研发的都有这个概念,但是可能不是非常清楚。对于我们做Go语言开发的来说,也想知道到底是什么传递。

那么我们先来看看什么是值传递,什么是引用传递。

什么是传值(值传递)

int

对于int这类基础类型我们可以很好的理解,它们就是一个拷贝,但是指针呢?我们觉得可以通过它修改原来的值,怎么会是一个拷贝呢?下面我们看个例子。

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

首先我们要知道,任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。

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


i100xc420018070iiip
ip0xc42000c028ipmodifyip0xc42000c038
0xc42000c0280xc42000c0380xc4200180700xc420018070ii

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

Go语言(Golang)是没有引用传递的,这里我不能使用Go举例子,但是可以通过说明描述。

modify0xc42000c028

迷惑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]
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
{张三}
PersonName李四张三
mapstructmodifyPerson
func main() {
    p:=Person{"张三"}
    modify(&p)
    fmt.Println(p)
}

type Person struct {
    Name string
}

 func modify(p *Person) {
     p.Name = "李四"
 }
intmakemap
// 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.gomakehmap*hmapmap===*hmapfunc modify(p map)func modify(p *hmap)func modify(ip *int)
makemap

chan类型

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

和map、chan都不一样的slice

slicemapchan
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&
makefmt.Printfslice
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
    }
    //省略部分代码
}
chanmapslicevalue.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
    }
}
sliceslice
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

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

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

slicemodifylencap*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
PersonslicePersonnameslicelencapagearrayagenamename
modify(&p)
func modify(p *Person){
    p.name = "李四"
    *p.age = 20
}
nameage
slice

小结

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

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

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

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

flysnow_org