• Go 学习笔记(65)— Go 中函数参数是传值(函数参数为数组、切片、map、chan、struct 等)


    Go 语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

    但是像 stringslicemap 这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。

    不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时 Go 编译器会介入:对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。

    1. 函数传参为数组

    package main
    
    import "fmt"
    
    func main() {
    	srcArray := [3]string{"a", "b", "c"}
    	fmt.Printf("srcArray address is %p\n", &srcArray) //	srcArray address is 0xc00005a150
    	modify(srcArray)
    	fmt.Printf("srcArray is %v\n", srcArray) //	srcArray is [a b c]
    
    }
    
    func modify(modifyArr [3]string) [3]string {
    	fmt.Printf("modifyArr address is %p\n", &modifyArr) //	modifyArr address is 0xc00005a180
    	modifyArr[1] = "x"
    	fmt.Printf("modifyArr is %v\n", modifyArr) //	modifyArr is [a x c]
    	return modifyArr
    }

    可以看到,函数传参外面和函数里面的参数的地址不相同,分别为 0xc00005a1500xc00005a180 ,所以在函数内修改参数值并不会影响函数外面的原始参数。

    所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值

    2. 函数传参为切片

    package main
    
    import "fmt"
    
    func main() {
    	srcSlice := []string{"a", "b", "c"}
    	fmt.Printf("srcSlice address is %p\n", srcSlice) //	srcSlice address is 0xc00005a150
    	modify(srcSlice)
    	fmt.Printf("srcSlice is %v\n", srcSlice) // modifySlice is [a x c]
    
    }
    
    func modify(modifySlice []string) []string {
    	fmt.Printf("modifySlice address is %p\n", modifySlice) // modifySlice address is 0xc00005a150
    	modifySlice[1] = "x"
    	fmt.Printf("modifySlice is %v\n", modifySlice) // srcSlice is [a x c]
    	return modifySlice
    }

    可以看到,函数传参外面和函数里面的参数的地址相同,都为 0xc00005a150,所以在函数内修改参数值会影响到原始参数值。

    因为这里 srcSlice 本身就是指针地址,所以不需要再用 & 取地址,如果再加上 & 则为指向指针的指针。

    对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。以切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,而它的底层数组并不会被拷贝。

    3. 函数传参为字典map

    Go 语言中,任何创建 map 的代码(不管是字面量还是 make 函数)最终调用的都是 runtime.makemap 函数。

    小提示:用字面量或者 make 函数的方式创建 map,并转换成 makemap 函数的调用,这个转换是 Go 语言编译器自动帮我们做的。

    从下面的代码可以看到,makemap 函数返回的是一个 *hmap 类型,也就是说返回的是一个指针,所以我们创建的 map 其实就是一个 *hmap

    src/runtime/map.go

    // makemap implements Go map creation for make(map[k]v, hint).
    func makemap(t *maptype, hint int, h *hmap) *hmap{
      //省略无关代码
    }

    这也是通过 map 类型的参数可以修改原始数据的原因,因为它本质上就是个指针。

    package main
    
    import "fmt"
    
    func main() {
    	srcMap := map[string]int{"a": 1, "b": 2, "c": 3}
    	fmt.Printf("srcMap address is %p\n", srcMap) //	srcMap address is 0xc00005a150
    	modify(srcMap)
    	fmt.Printf("srcMap is %#v\n", srcMap) // srcMap is map[string]int{"a":1, "b":2, "c":100}
    
    }
    
    func modify(modifyMap map[string]int) map[string]int {
    	fmt.Printf("modifyMap address is %p\n", modifyMap) // modifyMap address is 0xc00005a150
    	modifyMap["c"] = 100
    	fmt.Printf("modifyMap is %#v\n", modifyMap) // modifyMap is map[string]int{"a":1, "b":2, "c":100}
    	return modifyMap
    }

    从输出结果可以看到,它们的内存地址一模一样,所以才可以修改原始数据。而且在打印指针的时候,直接使用的是变量 srcMapmodifyMap,并没有用到取地址符 &,这是因为它们本来就是指针,所以就没有必要再使用 & 取地址了。

    注意:这里的 map 可以理解为引用类型,但是它本质上是个指针,只是可以叫作引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。

    4. 函数传参为 channel

    channel 也可以理解为引用类型,而它本质上也是个指针。

    通过下面的源代码可以看到,所创建的 chan 其实是个 *hchan,所以它在参数传递中也和 map 一样。

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

    严格来说,Go 语言没有引用类型,但是我们可以把 mapchan 称为引用类型,这样便于理解。除了 mapchan 之外,Go 语言中的函数、接口、slice 切片都可以称为引用类型。指针类型也可以理解为是一种引用类型。

    5. 函数传参为 struct

    package main
    
    import "fmt"
    
    type Student struct {
    	name string
    	age  int
    }
    
    func main() {
    	s := Student{name: "wohu", age: 20}
    	fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060
    	modify(s)
    	fmt.Printf("s is %v\n", s) // s is {wohu 20}
    
    }
    
    func modify(stu Student) Student {
    	fmt.Printf("stu address is %p\n", &stu) // stu address is 0xc00000c080
    	stu.age = 30
    	fmt.Printf("stu is %v\n", stu) // stu is {wohu 30}
    	return stu
    }

    发现它们的内存地址都不一样,这就意味着,在 modify 函数中修改的参数 stumain 函数中的变量 stu 不是同一个,这也是我们在 modify 函数中修改参数 stu,但是在 main 函数中打印后发现并没有修改的原因。

    导致这种结果的原因是 Go 语言中的函数传参都是值传递。 值传递指的是传递原来数据的一份拷贝,而不是原来的数据本身。

    modify 函数来说,在调用 modify 函数传递变量 stu 的时候,Go 语言会拷贝一个 stu 放在一个新的内存中,这样新的 p 的内存地址就和原来不一样了,但是里面的 nameage 是一样的,还是 wohu和 20。这就是副本的意思,变量里的数据一样,但是存放的内存地址不一样。

    除了 struct 外,还有浮点型、整型、字符串、布尔、数组,这些都是值类型。

    指针类型的变量保存的值就是数据对应的内存地址,所以在函数参数传递是传值的原则下,拷贝的值也是内存地址。现在对以上示例稍做修改,修改后的代码如下:

    package main
    
    import "fmt"
    
    type Student struct {
    	name string
    	age  int
    }
    
    func main() {
    	s := Student{name: "wohu", age: 20}
    	fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060
    	modify(&s)
    	fmt.Printf("s is %v\n", s) // s is {wohu 30}
    }
    
    func modify(stu *Student) *Student {
    	fmt.Printf("stu address is %p\n", stu) // stu address is 0xc00000c060
    	stu.age = 30
    	fmt.Printf("stu is %v\n", *stu) // stu is &{wohu 30}
    	return stu
    }

    所以指针类型的参数是永远可以修改原数据的,因为在参数传递时,传递的是内存地址。

    注意:值传递的是指针,也是内存地址。通过内存地址可以找到原数据的那块内存,所以修改它也就等于修改了原数据。

    定义的普通变量 stustudent 类型的。在 Go 语言中,student 是一个值类型,而 &stu 获取的指针是 *student 类型的,即指针类型。

    总结:在 Go 语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 mapchan 等),那么就可以在函数中修改原始数据。

    6. 其它示例

    直接上代码

    
    package main
    
    import "fmt"
    
    func main() {
    	// 示例1。
    	array1 := [3]string{"a", "b", "c"}
    	fmt.Printf("The array: %v\n", array1)
    	array2 := modifyArray(array1)
    	fmt.Printf("The modified array: %v\n", array2)
    	fmt.Printf("The original array: %v\n", array1)
    	fmt.Println()
    
    	// 示例2。
    	slice1 := []string{"x", "y", "z"}
    	fmt.Printf("The slice: %v\n", slice1)
    	slice2 := modifySlice(slice1)
    	fmt.Printf("The modified slice: %v\n", slice2)
    	fmt.Printf("The original slice: %v\n", slice1)
    	fmt.Println()
    
    	// 示例3。
    	complexArray1 := [3][]string{
    		[]string{"d", "e", "f"},
    		[]string{"g", "h", "i"},
    		[]string{"j", "k", "l"},
    	}
    	fmt.Printf("The complex array: %v\n", complexArray1)
    	complexArray2 := modifyComplexArray(complexArray1)
    	fmt.Printf("The modified complex array: %v\n", complexArray2)
    	fmt.Printf("The original complex array: %v\n", complexArray1)
    }
    
    // 示例1。
    func modifyArray(a [3]string) [3]string {
    	a[1] = "x"
    	return a
    }
    
    // 示例2。
    func modifySlice(a []string) []string {
    	a[1] = "i"
    	return a
    }
    
    // 示例3。
    func modifyComplexArray(a [3][]string) [3][]string {
    	a[1][1] = "s"
    	a[2] = []string{"o", "p", "q"}
    	return a
    }
    • 如果是进行一层修改,即数组的某个完整元素进行修改(指针变化),那么原有数组不变;
    • 如果是进行二层修改,即数组中某个元素切片内的某个元素再进行修改(指针未改变),那么原有数据也会跟着改变,传参可以理解是浅copy,参数本身的指针是不同,但是元素指针相同,对元素指针所指向目的的操作会影响传参过程中的原始数据;

    7. 函数传参为地址

    当变量被当做参数传入调用函数时,是值传递,也称变量的一个拷贝传递。如果传递过来的值是指针,就相当于把变量的地址作为参数传递到函数内,那么在函数内对这个指针所指向的内容进行修改,将会改变这个变量的值。如下边示例代码:

    package main
    import (
        "fmt"
    )
    func demo(str *string) {
        *str = "world"
    }
    func main() {
        var str = "hello"
        demo(&str)
        fmt.Println("str value is:", str)
    }

    输出结果:

    str value is: world

    从上边的输出信息可知,str 变量地址当做参数传入函数后,在函数中对地址所指向内容进行了修改,导致了变量 str 值发生了变化。这个过程能否说明函数调用传递的是指针,而不是变量的拷贝呢?下边通过另一个例子来进行说明:

    package main
    import (
        "fmt"
    )
    var world = "hello wolrd"
    func demo(str *string) {
        str = &world
        fmt.Println("str in demo func is:", *str)
    }
    func main() {
        var str = "hello"
        demo(&str)
        fmt.Println("str in main func is:", str)
    }

    输出结果:

    str in demo func is: hello wolrd
    str in main func is: hello

    上边示例中,str 变量地址被作为参数传入到了函数 demo 中,在函数中对参数进行重新赋值,将 world 变量地址赋值给了参数,函数调用结束后,重新打印变量 str 值,发现值没有被修改。

    所以,在函数调用中,变量被拷贝了一份传入函数,函数调用结束后,拷贝的值被丢弃。

    如果拷贝的是变量的地址,那么在函数内,其实是通过修改这个地址所指向内存中内容,从而达到修改变量值的目的,但是函数内并不能修改这个变量的地址,也就是 str 变量虽然将地址当做参数传入到 demo 函数中,demo 函数中虽然对这个地址进行了修改,但是在函数调用结束后,拷贝传递进去并被修改的参数被丢弃,str 变量地址未发生变化。

    8. 综合示例

    package main
    
    import "fmt"
    
    // 用于测试值传递效果的结构体,结构体是拥有多个字段的复杂结构。
    type Data struct {
    	complax  []int      // complax 为整型切片类型,切片是一种动态类型,内部以指针存在。
    	instance InnerData  // instance 成员以 InnerData 类型作为 Data 的成员 。
    	ptr      *InnerData // 将 ptr 声明为 InnerData 的指针类型
    }
    
    // 代表各种结构体字段
    type InnerData struct {
    	a int
    }
    
    // 值传递测试函数,该函数的参数和返回值都是 Data 类型。
    // 在调用中, Data 的内存会被复制后传入函数,当函数返回时,又会将返回值复制一次,
    // 赋给函数返回值的接收变量。
    func passByValue(inFunc Data) Data {
    	// 输出参数的成员情况
    	fmt.Printf("inFunc value: %+v\n", inFunc)
    	// 打印inFunc的指针
    	fmt.Printf("inFunc ptr: %p\n", &inFunc)
    	// 将传入的变量作为返回值返回,返回的过程将发生值复制。
    	return inFunc
    }
    
    func main() {
    	// 准备传入函数的结构
    	in := Data{
    		complax: []int{1, 2, 3},
    		instance: InnerData{
    			5,
    		},
    		ptr: &InnerData{1},
    	}
    	// 输入结构的成员情况
    	fmt.Printf("in value: %+v\n", in)
    	// 输入结构的指针地址
    	fmt.Printf("in ptr: %p\n", &in)
    	// 传入结构体,返回同类型的结构体
    	out := passByValue(in)
    	// 输出结构的成员情况
    	fmt.Printf("out value: %+v\n", out)
    	// 输出结构的指针地址
    	fmt.Printf("out ptr: %p\n", &out)
    }

    输出结果:

    in value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
    in ptr: 0xc000078150
    inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
    inFunc ptr: 0xc0000781e0
    out value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000180e8}
    out ptr: 0xc0000781b0

    从运行结果中发现:

    • 所有的 Data 结构的指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是将 Data 结构传入函数内部,还是通过函数返回值传回 Data 都会发生复制行为 。
    • 所有的 Data 结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递。
    • Data 结构的 ptr 成员在传递过程中保持 一致,表示指针在函数参数值传递中传递的只是指针值,不会复制指针指向的部分。

    参考:
    https://juejin.cn/post/6844903618890432520
    https://www.zhihu.com/question/312356800/answer/739572672
    https://segmentfault.com/q/1010000019965306/a-1020000019996800
    https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html

    https://blog.csdn.net/wohu1104/article/details/109661126

     
  • 相关阅读:
    南桥-- 算法训练 2的次幂表示
    Ajax系列之中的一个:ajax旧貌换新颜
    ASP.NET综合管理ERP系统100%源代码+所有开发文档
    创业建议干货分享
    读取properties属性文件——国际化
    測试赛C
    Android 自己定义ViewGroup手把手教你实现ArcMenu
    【VBA研究】利用DateAdd函数取上月或上年同期的日期
    【Java集合源代码剖析】TreeMap源代码剖析
    openstack neutron L3 HA
  • 原文地址:https://www.cnblogs.com/lidabo/p/16393049.html
Copyright © 2020-2023  润新知