• Go 参数传递是传值还是传引用


    什么是传值(值传递)?

         传值的意思是:函数传递的总是原来这个东西的一个副本、一个副拷贝。比如我们传递一个 int 类型的参数,传递

    的其实这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个指针的一份拷贝,而不是这个指针指向的

        对于 int 这类基础类型的我们可以很容易理解,它们就是一个拷贝,但是指针呢?我们可以通过它修改原来的值,怎

    么会是一个拷贝呢?看如下示例:

    package main
    
    import "fmt"
    
    func modify(ip *int) {
    	fmt.Printf("函数里接收指针的内存地址是:%p
    ",&ip)
    
    	*ip = 1
    }
    
    func main() {
    	i := 10
    	ip := &i
    
    	fmt.Printf("原始指针的内存地址是:%p
    ",&ip)
    
    	modify(ip)
    	fmt.Println("int 值被修改了,新值是:",i)
    }

      输出结果如下:

    原始指针的内存地址是:0xc00007e018
    函数里接收指针的内存地址是:0xc00007e028
    int 值被修改了,新值是: 1
    

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

    针的内存

           首先,我们声明了一个变量 i,值为 10,它的内存存放地址是 0xc000056080,通过这个地址我们可以找到变量 i,

    这个内存地址也就是变量 i 的指针 ip,指针 ip 是一个指针类型的变量,它也需要内存存放它,它的内存地址是 0xc0000

    7e018,在我们传递指针变量 ip 给 modify 函数的时候,是对这个指针变量的一个拷贝,所以新拷贝的指针变量 ip 它的内

    存地址就变了,是新的 0xc00007e028,不管是 0xc00007e018 还是 xc00007e028 我们都可以称之为指针的指针,他们

    指向同一个指针 0xc000056080,这个 0xc000056080 又指向 变量 i,这也就是为什么我们可以修改变量 i 的值

    什么是引用传递? 

           在 Go 语言中没有引用传递,以上面的例子为例,如果在 modify 函数里面打印的地址不变,那么就是引用传递

    迷惑 Map

           了解清楚了值传递和引用传递,但是对于 Map 类型来说,可能觉得还是迷惑,一个是我们可以通过方法修改它的内容,

    二个是它没有明显的指针,如下示例:

    package main
    
    import "fmt"
    
    func modify(p map[string]int) {
    	fmt.Printf("函数里接收到的map内存地址:%p
    ",&p)
    
    	p["张三"] = 25
    }
    
    func main() {
    	// 声明一个 map
    	persons := make(map[string]int)
    	persons["张三"] = 19
    
    	mp := &persons
    	fmt.Printf("原始map的内存地址是:%p
    ",mp)
    
    	modify(persons)
    	fmt.Println("map值被修改了,新值是:",persons)
    }
    

      运行打印输出结果:

    原始map的内存地址是:0xc000006028
    函数里接收到的map内存地址:0xc000006038
    map值被修改了,新值是: map[张三:25]
    

      两个内存地址不一样,说明是值传递,那么为什么是值传递我们还可以修改 Map 的内容呢?我们先看一个

    自己实现的 struct ,如下示例:

    package main
    
    import "fmt"
    
    // 声明一个结构体
    type Person struct {
    	Name string
    }
    
    func modify(p Person) {
    	fmt.Printf("函数里接收到的Person内存地址:%p
    ",&p)
    
    	p.Name = "李四"
    }
    
    func main() {
    	p := Person{"张三"}
    	fmt.Printf("原始Person的内存地址是:%p
    ",&p)
    
    	modify(p)
    	fmt.Println(p)
    }
    

      运行打印输出:

    原始Person的内存地址是:0xc00004a1c0
    函数里接收到的Person内存地址:0xc00004a1d0
    {张三}
    

      我们看到,自己定义的 Person 类型,在传递参数的时候也是值传递,但是它的值并没有被修改,这也就

    是说 map 类型 和我们自己定义 struct 类型是不一样的,我们把 modify 函数接收的参数改为 Person 的指针:

    package main
    
    import "fmt"
    
    // 声明一个结构体
    type Person struct {
    	Name string
    }
    
    func modify(p *Person) {
    	fmt.Printf("函数里接收到的Person内存地址:%p
    ",&p)
    
    	p.Name = "李四"
    }
    
    func main() {
    	p := Person{"张三"}
    	fmt.Printf("原始Person的内存地址是:%p
    ",&p)
    
    	modify(&p)
    	fmt.Println(p)
    }
    

      运行打印结果:

    原始Person的内存地址是:0xc00004a1c0
    函数里接收到的Person内存地址:0xc00007e020
    {李四}
    

      看上面的输出结果,我们发现这次被修改了,那么指针类型的可以被修改,非指针类型的不能被修改,是不是我们

    使用 make 创建的 map 就是一个指针类型呢?,看一下 Go 源码:

           源码地址:https://github.com/golang/go/blob/master/src/runtime/map.go

    func makemap64(t *maptype, hint int64, h *hmap) *hmap {
    	// 省略无关代码
    }
    

      我们看到 make 函数返回的是一个 hmap 类型的指针 *hmap,也就是 map == *hmap。现在再来看 func modify(p Person) 

    其实就是 func modify(p *Persoon) ,所以,这里 Go 通过 make 函数,为我们省去了指针的操作,在这里 map 可以理解为引用

    类型但不是传引用

    Slice 类型

           slice 类型也是引用类型,它也可以在函数中修改对应的内容:

    package main
    
    import "fmt"
    
    func modify(ages []int) {
    	fmt.Printf("函数里接收到 slice 的内存地址:%p
    ",ages)
    	ages[0] = 6
    }
    
    func main() {
    	ages := []int{10,20,30}
    	fmt.Printf("原始 slice 的内存地址:%p
    ",ages)
    
    	modify(ages)
    	fmt.Println(ages)
    }
    

      运行打印结果,发现的确是被修改了,而且在这里我们打印 slice 内存地址可以直接通过 %p,不用使用

    & 取地址符转换,我们看一下 fmt.Printf 源码:

    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
    	}
            // 省略部分代码
    }
    

      通过上面的源码发现,对于 map、slice 等被当成指针处理,通过 value.Pointer() 获取对应值的指针:

    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 {
    			// As the doc comment says, the returned pointer is an
    			// underlying code pointer but not necessarily enough to
    			// identify a single function uniquely. All method expressions
    			// created via reflect have the same underlying code pointer,
    			// so their Pointers are equal. The function used here must
    			// match the one used in makeMethodValue.
    			f := methodValueCall
    			return **(**uintptr)(unsafe.Pointer(&f))
    		}
    		p := v.pointer()
    		// Non-nil func value points at data block.
    		// First word of data block is actual code.
    		if p != nil {
    			p = *(*unsafe.Pointer)(p)
    		}
    		return uintptr(p)
    
    	case Slice:
    		return (*SliceHeader)(v.ptr).Data
    	}
    	panic(&ValueError{"reflect.Value.Pointer", v.kind()})
    }
    

      看上面的代码,如果类型是 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 是一种结构体 + 

    元素指针的混合类型

          Go 语言中所有的传参都是值传递,都是一个副本、一个拷贝。因为拷贝的内容是非引用类型(int、string、struct等这些),这样就在函数

    中无法修改原内容数据的;有的是引用类型(map、slice等),这样的就可以修改原内容数据

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

    是引用类型,引用类型和传引用是两个概念

         Go 只有值传递

  • 相关阅读:
    Redis详解
    Redis详细介绍
    memcache数据组织
    memcache细节解析
    memcached命令和配置
    PHP的Cookie、Session和跟Laravel相关的几点了解
    Session机制
    Cookie和Session详解
    Apache的Directory配置指南
    [C语言](二)01 获取Windows图形构件大小信息
  • 原文地址:https://www.cnblogs.com/leeyongbard/p/10400279.html
Copyright © 2020-2023  润新知