Go 方法集合與選擇receiver類型

来源:https://www.cnblogs.com/taoxiaoxin/archive/2023/11/04/17809992.html
-Advertisement-
Play Games

Go 方法集合與選擇receiver類型 目錄Go 方法集合與選擇receiver類型一、receiver 參數類型對 Go 方法的影響二、選擇 receiver 參數類型原則2.1 選擇 receiver 參數類型的第一個原則2.2 選擇 receiver 參數類型的第二個原則三、方法集合(Met ...


Go 方法集合與選擇receiver類型

目錄

一、receiver 參數類型對 Go 方法的影響

要想為 receiver 參數選出合理的類型,我們先要瞭解不同的 receiver 參數類型會對 Go 方法產生怎樣的影響。其實,Go 方法實質上是以方法的 receiver 參數作為第一個參數的普通函數。

對於函數參數類型對函數的影響,我們是很熟悉的。那麼我們能不能將方法等價轉換為對應的函數,再通過分析 receiver 參數類型對函數的影響,從而間接得出它對 Go 方法的影響呢?

基於這個思路。我們直接來看下麵例子中的兩個 Go 方法,以及它們等價轉換後的函數:

func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)

這個例子中有方法 M1M2M1 方法是 receiver 參數類型為 T 的一類方法的代表,而 M2 方法則代表了 receiver 參數類型為 *T 的另一類。下麵我們分別來看看不同的 receiver 參數類型對 M1M2 的影響。

首先,當 receiver 參數的類型為 T:當我們選擇以 T 作為 receiver 參數類型時,M1 方法等價轉換為 F1(t T)。我們知道,Go 函數的參數採用的是值拷貝傳遞,也就是說,F1 函數體中的 tT 類型實例的一個副本。這樣,我們在 F1 函數的實現中對參數 t 做任何修改,都只會影響副本,而不會影響到原 T 類型實例。

據此我們可以得出結論:當我們的方法 M1 採用類型為 Treceiver 參數時,代表 T 類型實例的 receiver 參數以值傳遞方式傳遞到 M1 方法體中的,實際上是 T 類型實例的副本,M1 方法體中對副本的任何修改操作,都不會影響到原 T 類型實例。

第二,當 receiver 參數的類型為 *T:當我們選擇以 *T 作為 receiver 參數類型時,M2 方法等價轉換為 F2(t *T)。同上面分析,我們傳遞給 F2 函數的 tT 類型實例的地址,這樣 F2 函數體中對參數 t 做的任何修改,都會反映到原 T 類型實例上。

據此我們也可以得出結論:當我們的方法 M2 採用類型為 *Treceiver 參數時,代表 *T 類型實例的 receiver 參數以值傳遞方式傳遞到 M2 方法體中的,實際上是 T 類型實例的地址,M2 方法體通過該地址可以對原 T 類型實例進行任何修改操作。

我們再通過一個更直觀的例子,證明一下上面這個分析結果,看一下 Go 方法選擇不同的 receiver 類型對原類型實例的影響:

package main
  
type T struct {
    a int
}

func (t T) M1() {
    t.a = 10
}

func (t *T) M2() {
    t.a = 11
}

func main() {
    var t T
    println(t.a) // 0

    t.M1()
    println(t.a) // 0

    p := &t
    p.M2()
    println(t.a) // 11
}

在這個示例中,我們為基類型 T 定義了兩個方法 M1M2,其中 M1receiver 參數類型為 T,而 M2receiver 參數類型為 *TM1M2 方法體都通過 receiver 參數 tt 的欄位 a 進行了修改。

但運行這個示常式序後,我們看到,方法 M1 由於使用了 T 作為 receiver 參數類型,它在方法體中修改的僅僅是 T 類型實例 t 的副本,原實例並沒有受到影響。因此 M1 調用後,輸出 t.a 的值仍為 0。

而方法 M2 呢,由於使用了 *T 作為 receiver 參數類型,它在方法體中通過 t 修改的是實例本身,因此 M2 調用後,t.a 的值變為了 11,這些輸出結果與我們前面的分析是一致的。

二、選擇 receiver 參數類型原則

2.1 選擇 receiver 參數類型的第一個原則

基於上面的影響分析,我們可以得到選擇 receiver 參數類型的第一個原則:如果 Go 方法要把對 receiver 參數代表的類型實例的修改,反映到原類型實例上,那麼我們應該選擇 *T 作為 receiver 參數的類型。

可能會有個疑問:如果我們選擇了 *T 作為 Go 方法 receiver 參數的類型,那麼我們是不是只能通過 *T 類型變數調用該方法,而不能通過 T 類型變數調用了呢?我們改造上面例子看一下:

  type T struct {
      a int
  }
  
  func (t T) M1() {
      t.a = 10
  }
 
 func (t *T) M2() {
     t.a = 11
 }
 
 func main() {
     var t1 T
     println(t1.a) // 0
     t1.M1()
     println(t1.a) // 0
     t1.M2()
     println(t1.a) // 11
 
     var t2 = &T{}
     println(t2.a) // 0
     t2.M1()
     println(t2.a) // 0
     t2.M2()
     println(t2.a) // 11
 }

我們先來看看類型為 T 的實例 t1。我們看到它不僅可以調用 receiver 參數類型為 T 的方法 M1,它還可以直接調用 receiver 參數類型為 *T 的方法 M2,並且調用完 M2 方法後,t1.a 的值被修改為 11 了。

其實,T 類型的實例 t1 之所以可以調用 receiver 參數類型為 *T 的方法 M2,都是 Go 編譯器在背後自動進行轉換的結果。或者說,t1.M2() 這種用法是 Go 提供的“語法糖”:Go 判斷 t1 的類型為 T,也就是與方法 M2receiver 參數類型 *T 不一致後,會自動將 t1.M2() 轉換為 (&t1).M2()

同理,類型為 *T 的實例 t2,它不僅可以調用 receiver 參數類型為 *T 的方法 M2,還可以調用 receiver 參數類型為 T 的方法 M1,這同樣是因為 Go 編譯器在背後做了轉換。也就是,Go 判斷 t2 的類型為 *T,與方法 M1receiver 參數類型 T 不一致,就會自動將 t2.M1() 轉換為 (*t2).M1()

通過這個實例,我們知道了這樣一個結論:無論是 T 類型實例,還是 *T 類型實例,都既可以調用 receiverT 類型的方法,也可以調用 receiver*T 類型的方法。這樣,我們在為方法選擇 receiver 參數的類型的時候,就不需要擔心這個方法不能被與 receiver 參數類型不一致的類型實例調用了。

2.2 選擇 receiver 參數類型的第二個原則

前面我們第一個原則說的是,當我們要在方法中對 receiver 參數代表的類型實例進行修改,那我們要為 receiver 參數選擇 *T 類型,但是如果我們不需要在方法中對類型實例進行修改呢?這個時候我們是為 receiver 參數選擇 T 類型還是 *T 類型呢?

這也得分情況。一般情況下,我們通常會為 receiver 參數選擇 T 類型,因為這樣可以縮窄外部修改類型實例內部狀態的“接觸面”,也就是儘量少暴露可以修改類型內部狀態的方法。

不過也有一個例外需要你特別註意。考慮到 Go 方法調用時,receiver 參數是以值拷貝的形式傳入方法中的。那麼,如果 receiver 參數類型的 size 較大,以值拷貝形式傳入就會導致較大的性能開銷,這時我們選擇 *T 作為 receiver 類型可能更好些。

以上這些可以作為我們選擇 receiver 參數類型的第二個原則。

三、方法集合(Method Set)

3.1 引入

我們先通過一個示例,直觀瞭解一下為什麼要有方法集合,它主要用來解決什麼問題:

type Interface interface {
    M1()
    M2()
}

type T struct{}

func (t T) M1()  {}
func (t *T) M2() {}

func main() {
    var t T
    var pt *T
    var i Interface

    i = pt
    i = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver)
}

在這個例子中,我們定義了一個介面類型 Interface 以及一個自定義類型 TInterface 介面類型包含了兩個方法 M1M2,代碼中還定義了基類型為 T 的兩個方法 M1M2,但它們的 receiver 參數類型不同,一個為 T,另一個為 *T。在 main 函數中,我們分別將 T 類型實例 t*T 類型實例 pt 賦值給 Interface 類型變數 i

運行一下這個示常式序,我們在 i = t 這一行會得到 Go 編譯器的錯誤提示,Go 編譯器提示我們:T 沒有實現 Interface 類型方法列表中的 M2,因此類型 T 的實例 t 不能賦值給 Interface 變數。

可是,為什麼呢?為什麼 *T 類型的 pt 可以被正常賦值給 Interface 類型變數 i,而 T 類型的 t 就不行呢?如果說 T 類型是因為只實現了 M1 方法,未實現 M2 方法而不滿足 Interface 類型的要求,那麼 *T 類型也只是實現了 M2 方法,並沒有實現 M1 方法啊?

有些事情並不是錶面看起來這個樣子的。瞭解方法集合後,這個問題就迎刃而解了。同時,方法集合也是用來判斷一個類型是否實現了某介面類型的唯一手段,可以說,“方法集合決定了介面實現”。

3.2 類型的方法集合

Go 中任何一個類型都有屬於自己的方法集合,或者說方法集合是 Go 類型的一個“屬性”。但不是所有類型都有自巴基斯坦的方法呀,比如 int 類型就沒有。所以,對於沒有定義方法的 Go 類型,我們稱其擁有空方法集合。

介面類型相對特殊,它只會列出代表介面的方法列表,不會具體定義某個方法,它的方法集合就是它的方法列表中的所有方法,我們可以一目瞭然地看到。

為了方便查看一個非介面類型的方法集合,這裡提供了一個函數 dumpMethodSet,用於輸出一個非介面類型的方法集合:

func dumpMethodSet(i interface{}) {
    dynTyp := reflect.TypeOf(i)

    if dynTyp == nil {
        fmt.Printf("there is no dynamic type\n")
        return
    }

    n := dynTyp.NumMethod()
    if n == 0 {
        fmt.Printf("%s's method set is empty!\n", dynTyp)
        return
    }

    fmt.Printf("%s's method set:\n", dynTyp)
    for j := 0; j < n; j++ {
        fmt.Println("-", dynTyp.Method(j).Name)
    }
    fmt.Printf("\n")
}

下麵我們利用這個函數,試著輸出一下 Go 原生類型以及自定義類型的方法集合,看下麵代碼:

type T struct{}

func (T) M1() {}
func (T) M2() {}

func (*T) M3() {}
func (*T) M4() {}

func main() {
    var n int
    dumpMethodSet(n)
    dumpMethodSet(&n)

    var t T
    dumpMethodSet(t)
    dumpMethodSet(&t)
}

運行這段代碼,我們得到如下結果:

int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2

*main.T's method set:
- M1
- M2
- M3
- M4

我們看到以 int*int 為代表的 Go 原生類型由於沒有定義方法,所以它們的方法集合都是空的。自定義類型 T 定義了方法 M1M2,因此它的方法集合包含了 M1M2,也符合我們預期。但 *T 的方法集合中除了預期的 M3M4 之外,居然還包含了類型 T 的方法 M1M2

不過,這裡程式的輸出並沒有錯誤。

這是因為,Go 語言規定,*T 類型的方法集合包含所有以 *Treceiver 參數類型的方法,以及所有以 Treceiver 參數類型的方法。這就是這個示例中為何 *T 類型的方法集合包含四個方法的原因。

這個時候,你是不是也找到了前面那個示例中為何 i = pt 沒有報編譯錯誤的原因了呢?我們同樣可以使用 dumpMethodSet 工具函數,輸出一下那個例子中 ptt 各自所屬類型的方法集合:

type Interface interface {
    M1()
    M2()
}

type T struct{}

func (t T) M1()  {}
func (t *T) M2() {}

func main() {
    var t T
    var pt *T
    dumpMethodSet(t)
    dumpMethodSet(pt)
}

運行上述代碼,我們得到如下結果:

main.T's method set:
- M1

*main.T's method set:
- M1
- M2

通過這個輸出結果,我們可以一目瞭然地看到 T*T 各自的方法集合。

我們看到,T 類型的方法集合中只包含 M1,沒有 Interface 類型方法集合中的 M2 方法,這就是 Go 編譯器認為變數 t 不能賦值給 Interface 類型變數的原因

在輸出的結果中,我們還看到 *T 類型的方法集合除了包含它自身定義的 M2 方法外,還包含了 T 類型定義的 M1 方法,*T 的方法集合與 Interface 介面類型的方法集合是一樣的,因此 pt 可以被賦值給 Interface 介面類型的變數 i

到這裡,我們已經知道了所謂的方法集合決定介面實現的含義就是:如果某類型 T 的方法集合與某介面類型的方法集合相同,或者類型 T 的方法集合是介面類型 I 方法集合的超集,那麼我們就說這個類型 T 實現了介面 I。或者說,方法集合這個概念在 Go 語言中的主要用途,就是用來判斷某個類型是否實現了某個介面。

四、選擇 receiver 參數類型的第三個原則

理解了方法集合後,我們再理解第三個原則的內容就不難了。這個原則的選擇依據就是 T 類型是否需要實現某個介面,也就是是否存在將 T 類型的變數賦值給某介面類型變數的情況。

理解了方法集合後,我們再理解第三個原則的內容就不難了。這個原則的選擇依據就是 T 類型是否需要實現某個介面,也就是是否存在將 T 類型的變數賦值給某介面類型變數的情況。

如果 T 類型需要實現某個介面,那我們就要使用 T 作為 receiver 參數的類型,來滿足介面類型方法集合中的所有方法。

如果 T 不需要實現某一介面,但 *T 需要實現該介面,那麼根據方法集合概念,*T 的方法集合是包含 T 的方法集合的,這樣我們在確定 Go 方法的 receiver 的類型時,參考原則一和原則二就可以了。

如果說前面的兩個原則更多聚焦於類型內部,從單個方法的實現層面考慮,那麼這第三個原則則是更多從全局的設計層面考慮,聚焦於這個類型與介面類型間的耦合關係。

五、小結

在實際進行 Go 方法設計時,我們首先應該考慮的是原則三,即 T 類型是否要實現某一介面。如果 T 類型需要實現某一介面的全部方法,那麼我們就需要使用 T 作為 receiver 參數的類型來滿足介面類型方法集合中的所有方法。

如果 T 類型不需要實現某一介面,那麼我們就可以參考原則一和原則二來為 receiver 參數選擇類型了。也就是,如果 Go 方法要把對 receiver 參數所代表的類型實例的修改反映到原類型實例上,那麼我們應該選擇 *T 作為 receiver 參數的類型。否則通常我們會為 receiver 參數選擇 T 類型,這樣可以減少外部修改類型實例內部狀態的“渠道”。除非 receiver 參數類型的 size 較大,考慮到傳值的較大性能開銷,選擇 *T 作為 receiver 類型可能更適合。

方法集合在 Go 語言中的主要用途就是判斷某個類型是否實現了某個介面。方法集合像“膠水”一樣,將自定義類型與介面隱式地“粘結”在一起,

分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Go類型嵌入介紹和使用類型嵌入模擬實現“繼承” 目錄Go類型嵌入介紹和使用類型嵌入模擬實現“繼承”一、獨立的自定義類型二、繼承三、類型嵌入3.1 什麼是類型嵌入四、介面類型的類型嵌入4.1 介面類型的類型嵌入介紹4.2 一個小案例五、結構體類型的類型嵌入5.1 結構體類型的類型嵌入介紹5.2 小案例 ...
  • Python 允許用戶輸入數據。這意味著我們可以向用戶詢問輸入。在 Python 3.6 中,使用 input() 方法來獲取用戶輸入。在 Python 2.7 中,使用 raw_input() 方法來獲取用戶輸入。以下示例要求用戶輸入用戶名,併在輸入用戶名後將其列印在屏幕上: Python 3.6 ...
  • OpenSSL 中的 `SSL` 加密是通過 `SSL/TLS` 協議來實現的。`SSL/TLS` 是一種安全通信協議,可以保障通信雙方之間的通信安全性和數據完整性。在 `SSL/TLS` 協議中,加密演算法是其中最核心的組成部分之一,SSL可以使用各類加密演算法進行密鑰協商,一般來說會使用`RSA`等... ...
  • 四大函數式介面(必備) 程式員:泛型、反射、註解、枚舉 新時代程式員:lambda表達式、鏈式編程、函數式介面、Stream流式計算 函數式介面:只有一個方法的介面 @FunctionalInterface public interface Runnable { public abstract vo ...
  • 推薦一個分散式圖資料庫Nebula Graph,萬億級數據,毫秒級延時 什麼是Nebula Graph Nebula Graph 是一款開源的、分散式的、易擴展的原生圖資料庫,能夠承載包含數千億個點和數萬億條邊的超大規模數據集,並且提供毫秒級查詢 什麼是圖資料庫 圖資料庫是專門存儲龐大的圖形網路並從 ...
  • 高精度的本質是將數字以字元串的形式讀入,然後將每一位分別存放入`int`數組中,通過模擬每一位的運算過程,來實現最終的運算效果。 ...
  • 對於手工計算來說,積分計算是非常困難的,對於一些簡單的函數,我們可以直接通過已知的積分公式來求解,但在更多的情況下,原函數並沒有簡單的表達式,因此確定積分的反函數變得非常困難。 另外,相對於微分運算來說,積分運算則具有更多的多樣性,包括不同的積分方法(如換元積分法、分部積分法等)和積分技巧,需要根據 ...
  • 一、概念 AOP面向切麵編程,一種編程範式 二、作用 在不改動原始設計(原代碼不改動)的基礎上為方法進行功能增強(即增加功能) 三、核心概念 1、代理(Proxy):SpringAOP的核心本質是採用代理模式實現的 2、連接點(JoinPoint):在SpringAOP中,理解為任意方法的執行 3、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...