Go 參數傳遞是傳值還是傳引用


什么是傳值(值傳遞)?

     傳值的意思是:函數傳遞的總是原來這個東西的一個副本、一個副拷貝。比如我們傳遞一個 int 類型的參數,傳遞

的其實這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個指針的一份拷貝,而不是這個指針指向的

    對於 int 這類基礎類型的我們可以很容易理解,它們就是一個拷貝,但是指針呢?我們可以通過它修改原來的值,怎

么會是一個拷貝呢?看如下示例:

package main

import "fmt"

func modify(ip *int) {
	fmt.Printf("函數里接收指針的內存地址是:%p\n",&ip)

	*ip = 1
}

func main() {
	i := 10
	ip := &i

	fmt.Printf("原始指針的內存地址是:%p\n",&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\n",&p)

	p["張三"] = 25
}

func main() {
	// 聲明一個 map
	persons := make(map[string]int)
	persons["張三"] = 19

	mp := &persons
	fmt.Printf("原始map的內存地址是:%p\n",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\n",&p)

	p.Name = "李四"
}

func main() {
	p := Person{"張三"}
	fmt.Printf("原始Person的內存地址是:%p\n",&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\n",&p)

	p.Name = "李四"
}

func main() {
	p := Person{"張三"}
	fmt.Printf("原始Person的內存地址是:%p\n",&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\n",ages)
	ages[0] = 6
}

func main() {
	ages := []int{10,20,30}
	fmt.Printf("原始 slice 的內存地址:%p\n",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 只有值傳遞


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM