关于golang:go语言参数传递到底是传值还是传引用

1次阅读

共计 8821 个字符,预计需要花费 23 分钟才能阅读完成。

前言

哈喽,大家好,我是 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++ 使用者,在 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

这里正好验证了 go 是值传递,然而还不能齐全确定 go 就只有值传递,咱们在写一个例子验证一下:

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

所以通过输入咱们能够看到,这是一个指针的拷贝,因为寄存这两个指针的内存地址是不同的,尽管指针的值雷同,然而是两个不同的指针。

通过下面的图,咱们能够更好的了解。咱们申明了一个变量 args,其值为1,并且他的内存寄存地址是0xc0000b4008,通过这个地址,咱们就能够找到变量args,这个地址也就是变量args 的指针 addr。指针addr 也是一个指针类型的变量,它也须要内存寄存它,它的内存地址是多少呢?是 0xc0000ae018。在咱们传递指针变量addrmodifiedNumber函数的时候,是该指针变量的拷贝, 所以新拷贝的指针变量 addr,它的内存地址曾经变了,是新的0xc0000ae028。所以,不论是0xc0000ae018 还是 0xc0000ae028,咱们都能够称之为指针的指针,他们指向同一个指针0xc0000b4008,这个0xc0000b4008 又指向变量 args, 这也就是为什么咱们能够批改变量args 的值。

通过下面的剖析,咱们就能够确定 go 就是值传递,因为咱们在 modifieNumber 办法中打印进去的内存地址产生了扭转,所以不是援用传递,实锤了奥兄弟们,证据确凿~~~。等等,如同好落下了点什么,说好的 go 中只有值传递呢,为什么 chanmapslice 类型传递却能够扭转其中的值呢?白焦急,咱们顺次来验证一下。

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]

通过这个例子咱们能够看到,应用 & 操作符示意 slice 的地址是有效的,而且应用 %p 输入的内存地址与 slice 的第一个元素的地址是一样的,那么为什么会呈现这样的状况呢?会不会是 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('&')这行代码就是为什么咱们应用 & 打印地址输入后果后面带有 & 的语音。因为咱们要打印的是一个 slice 类型,就会调用 p.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] 的起因。这个问题解决了,咱们再来看一看应用 %p 输入的内存地址与 slice 的第一个元素的地址是一样的。在下面的源码中,有这样一行代码 fallthrough,代表着接下来的fmt.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 {....... 省略局部代码

这里咱们能够看到下面有这样一句正文:If v’s Kind is Slice, the returned pointer is to the first。翻译成中文就是如果是 slice 类型,返回 slice 这个构造里的第一个元素的地址。这里正好解释下面为什么 fmt.Printf("切片 args 的地址:%p \\n",args)fmt.Printf("形参切片的地址 %p \\n",args)打印进去的地址是一样的,因为 args 是援用类型,所以他们都返回 slice 这个构造里的第一个元素的地址,为什么这两个 slice 构造里的第一个元素的地址一样呢,这就要在说一说 slice 的底层构造了。

咱们看一下 slice 底层构造:

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

slice是一个构造体,他的第一个元素是一个指针类型,这个指针指向的是底层数组的第一个元素。所以当是 slice 类型的时候,fmt.Printf返回是 slice 这个构造体里第一个元素的地址。说到底,又转变成了指针解决,只不过这个指针是 slice 中第一个元素的内存地址。

说了这么多,最初再做一个总结吧,为什么 slice 也是值传递。之所以对于援用类型的传递能够批改原内容的数据,这是因为在底层默认应用该援用类型的指针进行传递,但也是应用指针的正本,仍旧是值传递。所以 slice 传递的就是第一个元素的指针的正本,因为 fmt.printf 缘故造成了打印的地址一样,给人一种混同的感觉。

map 也是值传递吗?

mapslice 一样都具备蛊惑行为,哼,渣女。map咱们能够通过办法批改它的内容,并且它没有显著的指针。比方这个例子:

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()

从以上源码,咱们能够看出,应用 make 函数返回的是一个 hmap 类型的指针 *hmap。回到下面那个例子,咱们的func modifiedAge(person map[string]int) 函数,其实就等于 func modifiedAge(person *hmap), 实际上在作为传递参数时还是应用了指针的正本进行传递,属于值传递。在这里,Go 语言通过make 函数,字面量的包装,为咱们省去了指针的操作,让咱们能够更容易的应用 map。这里的 map 能够了解为援用类型,然而记住援用类型不是传援用。

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

这个怎么回事,实参加形参地址不一样,然而这个值是怎么传回来的,说好的值传递呢?白焦急,铁子,咱们像剖析 map 那样,再来剖析一下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"))
    }

从以上源码,咱们能够看出,应用 make 函数返回的是一个 hchan 类型的指针 *hchan。这不是与map 一个情理嘛,再次回到下面的例子,理论咱们的 fun (p chan bool)fun (p *hchan)是一样的,实际上在作为传递参数时还是应用了指针的正本进行传递,属于值传递。

是不是到这里,根本就能够确定 go 就是值传递了呢?还剩最初一个没有测试,那就是struct,咱们最初来验证一下struct

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
}

咱们发现,咱们本人定义的 Person 类型,在函数传参的时候也是值传递,然而它的值 (Age 字段)并没有被批改,咱们想改成10,发现最初的后果还是8

前文总结

兄弟们实锤了奥,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] 即可获取。

我是 asong,一名普普通通的程序猿,让 gi 我一起缓缓变强吧。我本人建了一个 golang 交换群,有须要的小伙伴加我vx, 我拉你入群。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • machinery-go 异步工作队列
  • 手把手教姐姐写音讯队列
  • 常见面试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解 Context 包,看这一篇就够了!!!
  • go-ElasticSearch 入门看这一篇就够了(一)
  • 面试官:go 中 for-range 应用过吗?这几个问题你能解释一下起因吗
  • 学会 wire 依赖注入、cron 定时工作其实就这么简略!
  • 据说你还不会 jwt 和 swagger- 饭我都不吃了带着实际我的项目我就来了
  • 把握这些 Go 语言个性,你的程度将进步 N 个品位(二)
正文完
 0