最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析

 更新時(shí)間:2023年12月12日 09:48:03   作者:菜皮日記  
這篇文章主要為大家介紹了Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

這篇筆記的思考開始于一篇帖子中提的問題:下面這段代碼中,都是從 map 中取一個(gè)元素并調(diào)用其方法,為什么最后一行無法編譯通過

import "testing"
type S struct {
    Name string
}
func (s *S) Write() {
    s.Name = "name"
}
func TestX(t *testing.T) {
    m := map[int]S{1: {"A"}}
    // 這能編譯通過:
    s := sVals[1]
    s.Write()
    // 這里不能編譯通過
    sVals[1].Write()
    // 報(bào)錯(cuò) cannot call pointer method Write on S
}

要回答這個(gè)問題,涉及到 Go 中的幾個(gè)概念,隱式引用轉(zhuǎn)換和可尋址 Addressable

隱式引用轉(zhuǎn)換

先看第一次調(diào)用 Write 的地方,首先 sVals[1] 返回的是一個(gè) S 類型的值賦值給變量 s,而之所以能夠在 S 類型的變量 s 上調(diào)用 *S 類型的 Write ,是因?yàn)?Go 支持隱式引用轉(zhuǎn)換,這個(gè)調(diào)用的完整寫法應(yīng)該是:

s := sVals[1]
(&s).Write()

Go 隱式引用轉(zhuǎn)換后可以簡寫成

s := sVals[1]
s.Write()

那么為什么第二個(gè) Write 調(diào)用無法編譯通過呢?這涉及到另一個(gè)概念:可尋址與臨時(shí)值。

可尋址和臨時(shí)值

可尋址 Addressable 指的是能夠通過內(nèi)存地址來訪問變量的特性。如果一個(gè)變量是可尋址的,那么你可以使用取地址操作符 & 來獲取它的內(nèi)存地址。

而臨時(shí)值都是不可尋址的,臨時(shí)值一句話概括就是表達(dá)式的中間狀態(tài),它們的生命周期很短,只在表達(dá)式計(jì)算過程中存在。臨時(shí)值只有在賦值給某個(gè)變量后臨時(shí)值才算完成了使命,這個(gè)過程相當(dāng)于一個(gè)值被創(chuàng)建出來最終安家落戶,有了自己的地址,之后才能詢問這個(gè)值的地址是多少。

下面是幾個(gè)可尋址例子

// **局部變量**:函數(shù)內(nèi)的局部變量是可尋址的。
func main() {
    x := 5
    p := &x // x 是可尋址的
}
// **全局變量**:全局變量也是可尋址的。
var globalVar int
func main() {
    p := &globalVar // globalVar 是可尋址的
}
// **數(shù)組的元素**:數(shù)組或切片的元素是可尋址的。
func main() {
    arr := [3]int{1, 2, 3}
    p := &arr[1] // arr[1] 是可尋址的
}
// **結(jié)構(gòu)體的字段**:如果你有一個(gè)結(jié)構(gòu)體變量,那么它的字段是可尋址的。
type MyStruct struct {
    Field int
}
func main() {
    s := MyStruct{Field: 5}
    p := &s.Field // s.Field 是可尋址的
}

下面是幾個(gè)不可尋址的例子

// **直接從函數(shù)調(diào)用返回的值**:不能對函數(shù)調(diào)用的結(jié)果直接取地址。
func myFunc() int {
    return 5
}
func main() {
    // p := &myFunc() // 這是錯(cuò)誤的,因?yàn)?myFunc() 的結(jié)果不可尋址
}
// **基本類型字面量**:如直接對 **5** 取地址是不允許的。
func main() {
    // p := &5 // 錯(cuò)誤,字面量不可尋址
}
// **臨時(shí)結(jié)果**:如表達(dá)式的中間結(jié)果。
func main() {
    x := &MyStruct{5} // 正確,因?yàn)檫@是一個(gè)變量
    // y := &MyStruct{5}.Field // 錯(cuò)誤,.Field 是一個(gè)臨時(shí)值
}

再回到剛才的問題,當(dāng)調(diào)用

sVals[1].Write()

時(shí),如果 Go 可以進(jìn)行隱式引用轉(zhuǎn)換,那么就應(yīng)該轉(zhuǎn)換成下面這種形式:

(&sVals[1]).Write

但實(shí)際上卻報(bào)了下面的錯(cuò)誤

cannot call pointer method Write on S

這個(gè)錯(cuò)誤是說不能在類型 S 上調(diào)用指針方法 Write,這說明 Go 沒有將 sVals[1] 進(jìn)行引用轉(zhuǎn)換。為什么沒有進(jìn)行引用轉(zhuǎn)換呢?

這里可以做一個(gè)假設(shè),按理說 sVals[1] 的元素已經(jīng)存在于內(nèi)存了,也就是說應(yīng)該可以被尋址的,所以應(yīng)該進(jìn)行隱式引用轉(zhuǎn)換成功。如果沒有進(jìn)行引用轉(zhuǎn)換,是不是說取出來的對象是一個(gè)不能被尋址的對象呢?

事實(shí)上確實(shí)是就是這樣,sVals[1] 取出來的并不是原始的對象,而是原對象的一個(gè)重新生成的副本,這就涉及到另一個(gè)概念:值傳遞。

map 的值傳遞

在 Go 中,所有的函數(shù)參數(shù)和返回值都是通過值傳遞的,這意味著它們都是原始數(shù)據(jù)的副本,而不是引用或指針。

這個(gè)原則在 map 中也成立,從 map 中取出一個(gè)元素返回的也是該元素的副本,而并不是該元素本身。所以上述代碼中

sVals[1]

返回的是一個(gè)副本,也就是說這是一個(gè)臨時(shí)值,而對于臨時(shí)值是不可尋址的。所以引用轉(zhuǎn)換是不可能的,最后無法編譯通過報(bào)出錯(cuò)誤。

回答最初的問題

到這里就已經(jīng)可以回答前面的問題了,由于 sVals[1] 是一個(gè)臨時(shí)值所以不可尋址,所以無法進(jìn)行引用轉(zhuǎn)換,無法將 S 類型的變量 s 轉(zhuǎn)換成 *S 類型,最后導(dǎo)致編譯錯(cuò)誤,報(bào)出不能在 S 類型上調(diào)用 Write 方法。

為什么要這樣設(shè)計(jì)

為什么 map 要返回一個(gè)副本回來,而不是返回原始對象的地址?這種設(shè)計(jì)選擇是出于安全性和一致性的考慮。由于 map 可能在運(yùn)行時(shí)進(jìn)行重新哈希以調(diào)整大小,重哈希后元素的地址可能發(fā)生變化,所以如果支持返回地址,那么可能會(huì)在程序運(yùn)行中出現(xiàn)錯(cuò)誤。例如一開始持有了一個(gè)元素的地址,之后 map 發(fā)生重哈希,地址都變了,再用之前獲取的地址做操作,肯定會(huì)出問題。

既然返回的是一個(gè)副本,那么想要做出修改的話就需要注意了。例如下面這段代碼

m := map[int]S{}
m[1] = S{Name: "11"}
s := m[1]
s.Name = "22"
fmt.Println(s)
fmt.Println(m)
// 輸出
// {22}
// map[1:{11}]

可以看到在 map 中取一個(gè)元素并修改其內(nèi)容并不會(huì)影響 map 中原有元素。

那么應(yīng)該如何修改 map 中的元素呢?

第一種是先修改,再回寫:

m := map[int]S{}
m[1] = S{Name: "11"}
s := m[1]
s.Name = "22"
m[1] = s // 回寫
fmt.Println(s)
fmt.Println(m)
// 輸出
// {22}
// map[1:{22}]

第二種就是 map 中存放指針類型

m := map[int]*S{}
m[1] = &S{Name: "11"}
s := m[1]
s.Name = "22"
fmt.Println(s)
fmt.Println(m[1])
// 輸出
// &{22}
// &{22}

用指針操作賦值是完整寫法應(yīng)該是

(*s).Name,而 *s 是從指責(zé)中取出對象操作,自然可以賦值。

容易混淆的值傳遞、引用傳遞與值類型、引用類型

前面一直在討論值傳遞,與之相對應(yīng)的是引用傳遞。這兩種傳遞方式?jīng)Q定了函數(shù)調(diào)用時(shí)參數(shù)是如何傳遞的:

  • 值傳遞:值傳遞復(fù)制數(shù)據(jù)
  • 引用傳遞:引用傳遞復(fù)制的是數(shù)據(jù)的地址

Go 采用的就是值傳遞,當(dāng)調(diào)用一個(gè)需要參數(shù)的函數(shù)時(shí),函數(shù)參數(shù)會(huì)復(fù)制一份,如果參數(shù)是一個(gè)指針,也會(huì)復(fù)制出來一個(gè)新的指針對象,但注意復(fù)制的是指針對象,即新舊兩個(gè)指針對象已經(jīng)完全獨(dú)立,有各自的內(nèi)存地址,但是兩個(gè)指針對象內(nèi)部指向的目標(biāo)對象地址沒有改變,如下面代碼和圖示:

s := &S{Name: "s"}
fmt.Printf("函數(shù)外,s指針本身的地址:%p\n", &s)
fmt.Printf("函數(shù)外,s指向?qū)ο蟮牡刂?%p\n", s)
fmt.Println("---")
updateObj(s)
func updateObj(s *S) {
    fmt.Printf("函數(shù)內(nèi),s指針本身的地址:%p\n", &s)
    fmt.Printf("函數(shù)內(nèi),s指向?qū)ο蟮牡刂?%p\n", s)
    s.Name = "updated"
}
// 輸出
// 函數(shù)外,s指針本身的地址:0x1400000e058
// 函數(shù)外,s指向?qū)ο蟮牡刂?0x1400005e6d0
// ---
// 函數(shù)內(nèi),s指針本身的地址:0x1400000e060
// 函數(shù)內(nèi),s指向?qū)ο蟮牡刂?0x1400005e6d0
// &{updated}

這也證明了有種說法稱 Go 支持引用傳遞的說法是不嚴(yán)謹(jǐn)?shù)?,這種說法認(rèn)為,通過傳遞指針,可以實(shí)現(xiàn)在函數(shù)內(nèi)部修改對象的效果,所以 Go 支持引用傳遞,而事實(shí)上這里面依舊是值傳遞,只不過復(fù)制的是指針本身。

除此之外 Go 中數(shù)據(jù)類型還分為值類型和引用類型,這兩種類型決定了數(shù)據(jù)是如何在內(nèi)存中存儲(chǔ)的:

  • 值類型:值類型直接存儲(chǔ)數(shù)據(jù),如基本數(shù)據(jù)類型(如 int、float、bool)、結(jié)構(gòu)體(struct)和數(shù)組都是值類型。
  • 引用類型:而引用類型存儲(chǔ)的是數(shù)據(jù)的引用,如切片(slice)、映射(map)、通道(channel)等都是引用類型。

可以在 runtime/map.go 中看到通過 makemap 函數(shù)創(chuàng)建一個(gè) map 對象,實(shí)際上返回的是一個(gè) *hmap 的指針類型;

在 runtime/chan.go 中可以看到通過 makechan 創(chuàng)建 channel 時(shí)返回的是一個(gè) *hchan 指針類型;

在 runtime/slice.go 的 makeslice 返回的直接就是一個(gè)指針 unsafe.Pointer

這些都證明了上述幾個(gè)類型都是引用類型,也就意味著這些類型作為函數(shù)參數(shù)傳遞時(shí)復(fù)制的都是指針。

無論是值類型還是引用類型(如指針),在作為參數(shù)傳遞給函數(shù)時(shí)都是通過值傳遞的方式。對于指針,雖然函數(shù)接收的是指針的副本,但由于這個(gè)副本指向原始數(shù)據(jù)的相同內(nèi)存地址,所以函數(shù)內(nèi)部對該地址的數(shù)據(jù)所做的修改會(huì)影響到原始數(shù)據(jù)。

可能得性能問題

最后一個(gè)問題,既然函數(shù)傳遞和容器類結(jié)構(gòu)維護(hù)存取的都是副本,那么如果反復(fù)傳遞一些大對象,就會(huì)頻繁復(fù)制對象,導(dǎo)致性能下降,所以傳遞對象時(shí),應(yīng)該盡量傳遞對象的指針,因?yàn)榧词箯?fù)制指針,指針類型長度也在可控范圍內(nèi),如在 32 位機(jī)上占用 4 字節(jié),在 64 位機(jī)上占用 8 字節(jié)。

以上就是Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析的詳細(xì)內(nèi)容,更多關(guān)于Go結(jié)構(gòu)體指針值傳遞的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 如何使用proto組件編譯pb.go文件

    如何使用proto組件編譯pb.go文件

    這篇文章主要介紹了如何使用proto組件編譯pb.go文件的詳細(xì)過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • go select的用法

    go select的用法

    本文主要介紹了go select的用法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Go高級(jí)特性探究之recover捕獲panic詳解

    Go高級(jí)特性探究之recover捕獲panic詳解

    在Go語言中,當(dāng)程序出現(xiàn)panic(即運(yùn)行時(shí)錯(cuò)誤)時(shí),程序會(huì)立即停止當(dāng)前的執(zhí)行流程,而recover函數(shù)的作用就是捕獲這個(gè)panic,下面就來看看具體是怎么操作的吧
    2023-06-06
  • golang逗號(hào)ok模式整合demo

    golang逗號(hào)ok模式整合demo

    這篇文章主要為大家介紹了golang逗號(hào)ok模式整合demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11
  • 使用golang編寫一個(gè)并發(fā)工作隊(duì)列

    使用golang編寫一個(gè)并發(fā)工作隊(duì)列

    這篇文章主要介紹了使用golang編寫一個(gè)并發(fā)工作隊(duì)列的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go中的函數(shù)選項(xiàng)模式(Functional Options Pattern)詳解

    Go中的函數(shù)選項(xiàng)模式(Functional Options Pattern)詳解

    在 Go 語言中,函數(shù)選項(xiàng)模式是一種優(yōu)雅的設(shè)計(jì)模式,用于處理函數(shù)的可選參數(shù),本文將對其進(jìn)行講解,準(zhǔn)備好了嗎,快跟隨著本文一探究竟吧
    2023-06-06
  • Go語言Http?Server框架實(shí)現(xiàn)一個(gè)簡單的httpServer

    Go語言Http?Server框架實(shí)現(xiàn)一個(gè)簡單的httpServer

    這篇文章主要為大家介紹了Go語言Http?Server框架實(shí)現(xiàn)一個(gè)簡單的httpServer抽象,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • golang中配置?sql.DB獲得更好的性能

    golang中配置?sql.DB獲得更好的性能

    這篇文章主要介紹了golang中如何配置?sql.DB獲得更好的性能,在這篇文章中,我想準(zhǔn)確解釋這些設(shè)置的作用,并展示它們可能產(chǎn)生的(積極和消極)影響,需要的朋友可以參考下
    2023-10-10
  • go中使用curl實(shí)現(xiàn)https請求的示例代碼

    go中使用curl實(shí)現(xiàn)https請求的示例代碼

    本文主要介紹了go中使用curl實(shí)現(xiàn)https請求的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2025-06-06
  • 一文帶你了解Golang中的并發(fā)性

    一文帶你了解Golang中的并發(fā)性

    并發(fā)是一個(gè)很酷的話題,一旦你掌握了它,就會(huì)成為一筆巨大的財(cái)富。所以本文就來和大家一起來聊聊Golang中的并發(fā)性,感興趣的可以了解一下
    2023-03-03

最新評論

恭城| 芦溪县| 博乐市| 唐山市| 同心县| 三河市| 巴马| 兴城市| 浦县| 达孜县| 潞西市| 泸西县| 汉川市| 云龙县| 临高县| 阳江市| 福清市| 乐亭县| 天柱县| 武义县| 米泉市| 米林县| 顺昌县| 禄劝| 梅州市| 东海县| 彰化县| 赫章县| 尚义县| 延川县| 上栗县| 内江市| 久治县| 杨浦区| 昌乐县| 亚东县| 阳高县| 东海县| 远安县| 桦甸市| 诏安县|