日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Go 方法集合与选择receiver类型

發布時間:2023/11/16 windows 84 coder
生活随笔 收集整理的這篇文章主要介紹了 Go 方法集合与选择receiver类型 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Go 方法集合與選擇receiver類型

目錄
  • Go 方法集合與選擇receiver類型
    • 一、receiver 參數類型對 Go 方法的影響
    • 二、選擇 receiver 參數類型原則
      • 2.1 選擇 receiver 參數類型的第一個原則
      • 2.2 選擇 receiver 參數類型的第二個原則
    • 三、方法集合(Method Set)
      • 3.1 引入
      • 3.2 類型的方法集合
    • 四、選擇 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)

這個例子中有方法 M1M2。M1 方法是 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 參數類型為 *T。M1M2 方法體都通過 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 以及一個自定義類型 T。Interface 接口類型包含了兩個方法 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?;蛘哒f,方法集合這個概念在 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 語言中的主要用途就是判斷某個類型是否實現了某個接口。方法集合像“膠水”一樣,將自定義類型與接口隱式地“粘結”在一起,

總結

以上是生活随笔為你收集整理的Go 方法集合与选择receiver类型的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。