網站首頁 編程語言 正文
Go 官方的定義
本部分引用 Go 官方 FAQ 的 “When are function parameters passed by value?”,內容如下。
如同 C 系列的所有語言一樣,Go 語言中的所有東西都是以值傳遞的。也就是說,一個函數總是得到一個被傳遞的東西的副本,就像有一個賦值語句將值賦給參數一樣。
傳值和傳引用
什么是傳值(值傳遞)
傳值的意思是:函數傳遞的總是原來這個東西的一個副本,一副拷貝。其指的是在調用函數時將實際參數復制一份傳遞到函數中,這樣在函數中如果對參數進行修改,將不會影響到實際參數。 比如我們傳遞一個int類型的參數,傳遞的其實是這個參數的一個副本;傳遞一個指針類型的參數,其實傳遞的是這個該指針的一份拷貝,而不是這個指針指向的值。
對于int這類基礎類型我們可以很好的理解,它們就是一個拷貝,但是指針呢?我們覺得可以通過它修改原來的值,怎么會是一個拷貝呢?下面我們看個例子。
test_demo.go
package main import ( "fmt" "testing" ) func modify(ip *int) { fmt.Printf("函數里接收到的指針的內存地址是:%p\n", &ip) *ip = 1 } func TestDemo(t *testing.T) { i := 10 ip := &i fmt.Printf("原始指針的內存地址是:%p\n", &ip) modify(ip) fmt.Println("int值被修改了,新值為:", i) }
輸出結果:
原始指針的內存地址是:0xc00000e038
函數里接收到的指針的內存地址是:0xc00000e040
int值被修改了,新值為: 1
什么是傳引用(引用傳遞)
傳引用,也叫做引用傳遞, 指在調用函數時將實際參數的地址直接傳遞到函數中,那么在函數中對參數所進行的修改,將影響到實際參數。
在 Go 語言中,官方已經明確了沒有傳引用,也就是沒有引用傳遞這一情況。
爭議最大的 map 和 slice
這時候又有小伙伴疑惑了,你看 Go 語言中的 map 和 slice 類型,能直接修改,難道不是同個內存地址,不是引用了?
其實在 FAQ 中有一句提醒很重要:“map 和 slice 的行為類似于指針,它們是包含指向底層 map 或 slice 數據的指針的描述符”。
迷惑map
package main import ( "fmt" "testing" ) func modify(p map[string]int) { fmt.Printf("函數里接收到map的內存地址是:%p\n", &p) p["張三"] = 20 } func TestDemo(t *testing.T) { persons := make(map[string]int) persons["張三"] = 19 mp := &persons fmt.Printf("原始map的內存地址是:%p\n", mp) modify(persons) fmt.Println("map值被修改了,新值為:", persons) }
輸出結果:
原始map的內存地址是:0xc000114028
函數里接收到map的內存地址是:0xc000114030
確實是值傳遞,那修改后的 map 的結果應該是什么。既然是值傳遞,那肯定就是 “這次一定!",對嗎?
輸出結果:
map值被修改了,新值為: map[張三:20]
原因:
指針類型可以修改,非指針類型不行,可以大膽的猜測,使用make
函數創建的map
是不是一個指針類型呢?看一下源代碼:
// makemap implements a Go map creation 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 bucket != nil, bucket can be used as the first bucket. func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap { //省略無關代碼 }
通過查看src/runtime/hashmap.go
源代碼發現,注意其返回的是 *hmap
類型,是一個指針。也就是 Go 語言通過對 map
類型的相關方法進行封裝,達到了用戶需要關注指針傳遞的作用。
現在看func modify(p map)
這樣的函數,其實就等于func modify(p *hmap)
,和前面什么是值傳遞里舉的func modify(ip *int)
的例子一樣,可以參考分析。
這類情況我們稱其為 “引用類型” ,但 “引用類型” 不等同于就是傳引用,又或是引用傳遞了,還是有比較明確的區別的。
chan類型
chan類型本質上和map類型是一樣的,這里不做過多的介紹,參考下源代碼:
func makechan(t *chantype, size int64) *hchan { //省略無關代碼 }
chan
也是一個引用類型,和map
相差無幾,make
返回的是一個*hchan
。
和map、chan都不一樣的slice
slice
和map
、chan
都不太一樣的,一樣的是,它也是引用類型,它也可以在函數中修改對應的內容。
package main import ( "fmt" "testing" ) func modify(ages []int) { fmt.Printf("函數里接收到slice的內存地址是%p\n", ages) ages[0] = 1 } func TestDemo(t *testing.T) { ages := []int{6, 6, 6} fmt.Printf("原始slice的內存地址是%p\n", ages) modify(ages) fmt.Println(ages) }
從結果來看,兩者的內存地址一樣,也成功的變更到了變量 ages
的值。這難道不是引用傳遞嗎?
關注兩個細節:
- 沒有用
&
來取地址。 - 可以直接用
%p
來打印。
之所以可以同時做到上面這兩件事,是因為標準庫 fmt
針對在這一塊做了優化:
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 } //省略部分代碼 }
通過源代碼發現,對于chan
、map
、slice
等被當成指針處理,通過value.Pointer()
獲取對應的值的指針。
// 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 Slice: return (*SliceHeader)(v.ptr).Data } }
很明顯了,當是slice
類型的時候,返回是slice
這個結構體里,字段Data
第一個元素的地址。
type SliceHeader struct { Data uintptr Len int Cap int } type slice struct { array unsafe.Pointer len int cap int }
在 Go 語言運行時,傳遞的也是相應 slice
類型的底層數組的指針,但需要注意,其使用的是指針的副本。嚴格意義是引用類型,依舊是值傳遞。slice
是一種結構體+元素指針的混合類型,通過元素array(Data)
的指針,可以達到修改slice
里存儲元素的目的。
總結
最終可以確認的是Go語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。
讓最多人犯迷糊的就是 slice
、map
、chan
等類型,都會認為是 “引用傳遞”,從而認為 Go 語言的 xxx 就是引用傳遞。正因為它們還引用類型(指針、map、slice、chan等這些),這樣就可以修改原內容數據。
再記住,Go里只有傳值(值傳遞)。
參考資料
群里又吵起來了,Go 是傳值還是傳引用?
Go語言參數傳遞是傳值還是傳引用
原文鏈接:https://blog.csdn.net/weixin_43064185/article/details/128618784
相關推薦
- 2022-03-16 C#?程序通用結構_C#教程
- 2023-11-13 【云原生】python獲取docker stats 容器cpu使用率
- 2022-09-19 Tomcat配置HTTPS訪問的實現步驟_Tomcat
- 2023-11-17 python中numpy ndarray 按條件篩選數組,關聯篩選的例子——numpyarray對數
- 2022-10-02 C++?OpenCV實戰之手寫數字識別_C 語言
- 2022-07-17 Python中使用tkFileDialog實現文件選擇、保存和路徑選擇_python
- 2022-12-10 Qt之簡單的異步操作實現方法_C 語言
- 2021-12-15 幾個小技巧幫你實現Golang永久阻塞_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支