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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...