欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

引言

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

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()
    // 報錯 cannot call pointer method Write on S
}

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

隱式引用轉換

先看第一次調用 Write 的地方,首先 sVals[1] 返回的是一個 S 類型的值賦值給變量 s,而之所以能夠在 S 類型的變量 s 上調用 *S 類型的 Write ,是因為 Go 支持隱式引用轉換,這個調用的完整寫法應該是:

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

Go 隱式引用轉換后可以簡寫成

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

那么為什么第二個 Write 調用無法編譯通過呢?這涉及到另一個概念:可尋址與臨時值。

可尋址和臨時值

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

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

下面是幾個可尋址例子

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

下面是幾個不可尋址的例子

// **直接從函數調用返回的值**:不能對函數調用的結果直接取地址。
func myFunc() int {
    return 5
}
func main() {
    // p := &myFunc() // 這是錯誤的,因為 myFunc() 的結果不可尋址
}
// **基本類型字面量**:如直接對 **5** 取地址是不允許的。
func main() {
    // p := &5 // 錯誤,字面量不可尋址
}
// **臨時結果**:如表達式的中間結果。
func main() {
    x := &MyStruct{5} // 正確,因為這是一個變量
    // y := &MyStruct{5}.Field // 錯誤,.Field 是一個臨時值
}

再回到剛才的問題,當調用

sVals[1].Write()

時,如果 Go 可以進行隱式引用轉換,那么就應該轉換成下面這種形式:

(&sVals[1]).Write

但實際上卻報了下面的錯誤

cannot call pointer method Write on S

這個錯誤是說不能在類型 S 上調用指針方法 Write,這說明 Go 沒有將 sVals[1] 進行引用轉換。為什么沒有進行引用轉換呢?

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

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

map 的值傳遞

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

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

sVals[1]

返回的是一個副本,也就是說這是一個臨時值,而對于臨時值是不可尋址的。所以引用轉換是不可能的,最后無法編譯通過報出錯誤。

回答最初的問題

到這里就已經可以回答前面的問題了,由于 sVals[1] 是一個臨時值所以不可尋址,所以無法進行引用轉換,無法將 S 類型的變量 s 轉換成 *S 類型,最后導致編譯錯誤,報出不能在 S 類型上調用 Write 方法。

為什么要這樣設計

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

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

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 中取一個元素并修改其內容并不會影響 map 中原有元素。

那么應該如何修改 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}

用指針操作賦值是完整寫法應該是

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

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

前面一直在討論值傳遞,與之相對應的是引用傳遞。這兩種傳遞方式決定了函數調用時參數是如何傳遞的:

  • 值傳遞:值傳遞復制數據
  • 引用傳遞:引用傳遞復制的是數據的地址

Go 采用的就是值傳遞,當調用一個需要參數的函數時,函數參數會復制一份,如果參數是一個指針,也會復制出來一個新的指針對象,但注意復制的是指針對象,即新舊兩個指針對象已經完全獨立,有各自的內存地址,但是兩個指針對象內部指向的目標對象地址沒有改變,如下面代碼和圖示:

s := &S{Name: "s"}
fmt.Printf("函數外,s指針本身的地址:%p\n", &s)
fmt.Printf("函數外,s指向對象的地址:%p\n", s)
fmt.Println("---")
updateObj(s)
func updateObj(s *S) {
    fmt.Printf("函數內,s指針本身的地址:%p\n", &s)
    fmt.Printf("函數內,s指向對象的地址:%p\n", s)
    s.Name = "updated"
}
// 輸出
// 函數外,s指針本身的地址:0x1400000e058
// 函數外,s指向對象的地址:0x1400005e6d0
// ---
// 函數內,s指針本身的地址:0x1400000e060
// 函數內,s指向對象的地址:0x1400005e6d0
// &{updated}

這也證明了有種說法稱 Go 支持引用傳遞的說法是不嚴謹的,這種說法認為,通過傳遞指針,可以實現在函數內部修改對象的效果,所以 Go 支持引用傳遞,而事實上這里面依舊是值傳遞,只不過復制的是指針本身。

除此之外 Go 中數據類型還分為值類型和引用類型,這兩種類型決定了數據是如何在內存中存儲的:

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

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

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

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

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

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

可能得性能問題

最后一個問題,既然函數傳遞和容器類結構維護存取的都是副本,那么如果反復傳遞一些大對象,就會頻繁復制對象,導致性能下降,所以傳遞對象時,應該盡量傳遞對象的指針,因為即使復制指針,指針類型長度也在可控范圍內,如在 32 位機上占用 4 字節(jié),在 64 位機上占用 8 字節(jié)。

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

相關文章

  • Go語言二進制文件的讀寫操作

    Go語言二進制文件的讀寫操作

    本文主要介紹了Go語言二進制文件的讀寫操作,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-04-04
  • Makefile構建Golang項目示例詳解

    Makefile構建Golang項目示例詳解

    這篇文章主要為大家介紹了Makefile構建Golang項目的過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • 深入理解Go中defer的機制

    深入理解Go中defer的機制

    本文主要介紹了Go中defer的機制,包括執(zhí)行順序、參數預計算、閉包和與返回值的交互,具有一定的參考價值,感興趣的可以了解一下
    2025-02-02
  • Go使用chan或context退出協(xié)程示例詳解

    Go使用chan或context退出協(xié)程示例詳解

    這篇文章主要為大家介紹了Go使用chan或context退出協(xié)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-08-08
  • ?Go?語言實現?HTTP?文件上傳和下載

    ?Go?語言實現?HTTP?文件上傳和下載

    這篇文章主要介紹了Go語言實現HTTP文件上傳和下載,文章圍繞主題展開詳細的內容戒殺,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-09-09
  • 詳解Gotorch多機定時任務管理系統(tǒng)

    詳解Gotorch多機定時任務管理系統(tǒng)

    遵循著“學一門語言最好的方式是使用它”的理念,想著用Go來實現些什么,剛好有一個比較讓我煩惱的問題,于是用Go解決一下,即使不在生產環(huán)境使用,也可以作為Go語言學習的一種方式。
    2021-05-05
  • go語言異常panic和恢復recover用法實例

    go語言異常panic和恢復recover用法實例

    這篇文章主要介紹了go語言異常panic和恢復recover用法,實例分析了異常panic和恢復recover使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-03-03
  • Go整合ElasticSearch的示例代碼

    Go整合ElasticSearch的示例代碼

    這篇文章主要介紹了Go整合ElasticSearch的相關知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • 基于go實例網絡存儲協(xié)議詳解

    基于go實例網絡存儲協(xié)議詳解

    這篇文章主要為大家介紹了基于go實例網絡存儲協(xié)議詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • Go語言如何實現Benchmark函數

    Go語言如何實現Benchmark函數

    go想要在main函數中測試benchmark會麻煩一些,所以這篇文章主要為大家介紹了如何實現了一個簡單的且沒有開銷的benchmark函數,希望對大家有所幫助
    2024-12-12

最新評論