Go 学习笔记(65)— Go 中函数参数是传值还是传引用
Go 語言中,函數參數傳遞采用是值傳遞的方式。所謂“值傳遞”,就是將實際參數在內存中的表示逐位拷貝到形式參數中。對于像整型、數組、結構體這類類型,它們的內存表示就是它們自身的數據內容,因此當這些類型作為實參類型時,值傳遞拷貝的就是它們自身,傳遞的開銷也與它們自身的大小成正比。
但是像 string、slice 、map 這些類型就不是了,它們的內存表示對應的是它們數據內容的“描述符”。當這些類型作為實參類型時,值傳遞拷貝的也是它們數據內容的“描述符”,不包括數據內容本身,所以這些類型傳遞的開銷是固定的,與數據內容大小無關。這種只拷貝“描述符”,不拷貝實際數據內容的拷貝過程,也被稱為“淺拷貝”。
不過函數參數的傳遞也有兩個例外,當函數的形參為接口類型,或者形參是變長參數時,簡單的值傳遞就不能滿足要求了,這時 Go 編譯器會介入:對于類型為接口類型的形參,Go 編譯器會把傳遞的實參賦值給對應的接口類型形參;對于為變長參數的形參,Go 編譯器會將零個或多個實參按一定形式轉換為對應的變長形參。
1. 函數傳參為數組
package mainimport "fmt"func main() {srcArray := [3]string{"a", "b", "c"}fmt.Printf("srcArray address is %p\n", &srcArray) // srcArray address is 0xc00005a150modify(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 0xc00005a180modifyArr[1] = "x"fmt.Printf("modifyArr is %v\n", modifyArr) // modifyArr is [a x c]return modifyArr
}
可以看到,函數傳參外面和函數里面的參數的地址不相同,分別為 0xc00005a150 和 0xc00005a180 ,所以在函數內修改參數值并不會影響函數外面的原始參數。
所有傳給函數的參數值都會被復制,函數在其內部使用的并不是參數值的原值,而是它的副本。由于數組是值類型,所以每一次復制都會拷貝它,以及它的所有元素值
2. 函數傳參為切片
package mainimport "fmt"func main() {srcSlice := []string{"a", "b", "c"}fmt.Printf("srcSlice address is %p\n", srcSlice) // srcSlice address is 0xc00005a150modify(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 0xc00005a150modifySlice[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 mainimport "fmt"func main() {srcMap := map[string]int{"a": 1, "b": 2, "c": 3}fmt.Printf("srcMap address is %p\n", srcMap) // srcMap address is 0xc00005a150modify(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 0xc00005a150modifyMap["c"] = 100fmt.Printf("modifyMap is %#v\n", modifyMap) // modifyMap is map[string]int{"a":1, "b":2, "c":100}return modifyMap
}
從輸出結果可以看到,它們的內存地址一模一樣,所以才可以修改原始數據。而且在打印指針的時候,直接使用的是變量 srcMap 和 modifyMap,并沒有用到取地址符 &,這是因為它們本來就是指針,所以就沒有必要再使用 & 取地址了。
注意:這里的
map可以理解為引用類型,但是它本質上是個指針,只是可以叫作引用類型而已。在參數傳遞時,它還是值傳遞,并不是其他編程語言中所謂的引用傳遞。
4. 函數傳參為 channel
channel 也可以理解為引用類型,而它本質上也是個指針。
通過下面的源代碼可以看到,所創建的 chan 其實是個 *hchan,所以它在參數傳遞中也和 map 一樣。
func makechan(t *chantype, size int64) *hchan {//省略無關代碼
}
嚴格來說,
Go語言沒有引用類型,但是我們可以把map、chan稱為引用類型,這樣便于理解。除了map、chan之外,Go語言中的函數、接口、slice切片都可以稱為引用類型。指針類型也可以理解為是一種引用類型。
5. 函數傳參為 struct
package mainimport "fmt"type Student struct {name stringage int
}func main() {s := Student{name: "wohu", age: 20}fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060modify(s)fmt.Printf("s is %v\n", s) // stu is {wohu 30}}func modify(stu Student) Student {fmt.Printf("stu address is %p\n", &stu) // stu address is 0xc00000c080stu.age = 30fmt.Printf("stu is %v\n", stu) // s is {wohu 20}return stu
}
發現它們的內存地址都不一樣,這就意味著,在 modify 函數中修改的參數 stu 和 main 函數中的變量 stu 不是同一個,這也是我們在 modify 函數中修改參數 stu,但是在 main 函數中打印后發現并沒有修改的原因。
導致這種結果的原因是 Go 語言中的函數傳參都是值傳遞。 值傳遞指的是傳遞原來數據的一份拷貝,而不是原來的數據本身。
以 modify 函數來說,在調用 modify 函數傳遞變量 stu 的時候,Go 語言會拷貝一個 stu 放在一個新的內存中,這樣新的 p 的內存地址就和原來不一樣了,但是里面的 name 和 age 是一樣的,還是 wohu和 20。這就是副本的意思,變量里的數據一樣,但是存放的內存地址不一樣。
除了 struct 外,還有浮點型、整型、字符串、布爾、數組,這些都是值類型。
指針類型的變量保存的值就是數據對應的內存地址,所以在函數參數傳遞是傳值的原則下,拷貝的值也是內存地址。現在對以上示例稍做修改,修改后的代碼如下:
package mainimport "fmt"type Student struct {name stringage int
}func main() {s := Student{name: "wohu", age: 20}fmt.Printf("s address is %p\n", &s) // s address is 0xc00000c060modify(&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 0xc00000c060stu.age = 30fmt.Printf("stu is %v\n", *stu) // stu is &{wohu 30}return stu
}
所以指針類型的參數是永遠可以修改原數據的,因為在參數傳遞時,傳遞的是內存地址。
注意:值傳遞的是指針,也是內存地址。通過內存地址可以找到原數據的那塊內存,所以修改它也就等于修改了原數據。
定義的普通變量 stu 是 student 類型的。在 Go 語言中,student 是一個值類型,而 &stu 獲取的指針是 *student 類型的,即指針類型。
總結:在 Go 語言中,函數的參數傳遞只有值傳遞,而且傳遞的實參都是原始數據的一份拷貝。如果拷貝的內容是值類型的,那么在函數中就無法修改原始數據;如果拷貝的內容是指針(或者可以理解為引用類型 map、chan 等),那么就可以在函數中修改原始數據。
6. 其它示例
直接上代碼
package mainimport "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 = &worldfmt.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 mainimport "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
總結
以上是生活随笔為你收集整理的Go 学习笔记(65)— Go 中函数参数是传值还是传引用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022-2028中国空中互联网系统市场
- 下一篇: 2022-2028年中国氟硅橡胶产业发展