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

一文帶你理解Go語言中方法的本質(zhì)

 更新時(shí)間:2023年11月05日 09:07:56   作者:賈維斯Echo  
我們知道,Go語言從設(shè)計(jì)伊始,就不支持經(jīng)典的面向?qū)ο笳Z法元素,但?Go?語言仍保留了名為“方法(method)”的語法元素,下面我們就來帶大家深入了解一下Go語言中的方法吧

一、認(rèn)識(shí) Go 方法

1.1 基本介紹

我們知道,Go 語言從設(shè)計(jì)伊始,就不支持經(jīng)典的面向?qū)ο笳Z法元素,比如類、對(duì)象、繼承,等等,但 Go 語言仍保留了名為“方法(method)”的語法元素。當(dāng)然,Go 語言中的方法和面向?qū)ο笾械姆椒ú⒉皇且粯拥?。Go 引入方法這一元素,并不是要支持面向?qū)ο缶幊谭妒?,而?Go 踐行組合設(shè)計(jì)哲學(xué)的一種實(shí)現(xiàn)層面的需要。

在 Go 編程語言中,方法是與特定類型相關(guān)聯(lián)的函數(shù)。它們?cè)试S您在自定義類型上定義行為,這個(gè)自定義類型可以是結(jié)構(gòu)體(struct)或任何用戶定義的類型。方法本質(zhì)上是一種函數(shù),但它們具有一個(gè)特定的接收者(receiver),也就是方法所附加到的類型。這個(gè)接收者可以是指針類型或值類型。方法與函數(shù)的區(qū)別是,函數(shù)不屬于任何類型,方法屬于特定的類型。

1.2 聲明

1.2.1 引入

首先我們這里以 Go 標(biāo)準(zhǔn)庫 net/http 包中 *Server 類型的方法 ListenAndServeTLS 為例,講解一下 Go 方法的一般形式:

和 Go 函數(shù)一樣,Go 的方法也是以 func 關(guān)鍵字修飾的,并且和函數(shù)一樣,也包含方法名(對(duì)應(yīng)函數(shù)名)、參數(shù)列表、返回值列表與方法體(對(duì)應(yīng)函數(shù)體)。

而且,方法中的這幾個(gè)部分和函數(shù)聲明中對(duì)應(yīng)的部分,在形式與語義方面都是一致的,比如:方法名字首字母大小寫決定該方法是否是導(dǎo)出方法;方法參數(shù)列表支持變長(zhǎng)參數(shù);方法的返回值列表也支持具名返回值等。

不過,它們也有不同的地方。從上面這張圖我們可以看到,和由五個(gè)部分組成的函數(shù)聲明不同,Go 方法的聲明有六個(gè)組成部分,多的一個(gè)就是圖中的 receiver 部分。在 receiver 部分聲明的參數(shù),Go 稱之為 receiver 參數(shù),這個(gè) receiver 參數(shù)也是方法與類型之間的紐帶,也是方法與函數(shù)的最大不同。

Go 中的方法必須是歸屬于一個(gè)類型的,而 receiver 參數(shù)的類型就是這個(gè)方法歸屬的類型,或者說這個(gè)方法就是這個(gè)類型的一個(gè)方法。以圖中的 ListenAndServeTLS 為例,這里的 receiver 參數(shù) srv 的類型為 *Server,那么我們可以說,這個(gè)方法就是 *Server 類型的方法。

注意!這里說的是 ListenAndServeTLS 是 *Server 類型的方法,而不是 Server 類型的方法。

1.2.2 一般聲明形式

方法的聲明形式如下:

func (t *T或T) MethodName(參數(shù)列表) (返回值列表) {
    // 方法體
}

其中各部分的含義如下:

  • (t *T或T):括號(hào)中的部分是方法的接收者,用于指定方法將附加到的類型。t 是接收者的名稱,T 是接收者的類型。接收者可以是值類型(T)或指針類型(*T)。如果使用值類型作為接收者,方法操作的是接收者的副本,而指針類型允許方法修改接收者的原始值。無論 receiver 參數(shù)的類型為 *T 還是 T,我們都把一般聲明形式中的 T 叫做 receiver 參數(shù) t 的基類型。如果 t 的類型為 T,那么說這個(gè)方法是類型 T 的一個(gè)方法;如果 t 的類型為 *T,那么就說這個(gè)方法是類型 *T 的一個(gè)方法。而且,要注意的是,每個(gè)方法只能有一個(gè) receiver 參數(shù),Go 不支持在方法的 receiver 部分放置包含多個(gè) receiver 參數(shù)的參數(shù)列表,或者變長(zhǎng) receiver 參數(shù)。
  • MethodName:這是方法的名稱,用于在調(diào)用方法時(shí)引用它。
  • (參數(shù)列表):這是方法的參數(shù)列表,定義了方法可以接受的參數(shù)。如果方法不需要參數(shù),此部分為空。
  • (返回值列表):這是方法的返回值列表,定義了方法返回的結(jié)果。如果方法不返回任何值,此部分為空。
  • 方法體:方法體包含了方法的具體實(shí)現(xiàn),這里可以編寫方法的功能代碼。

1.2.3 receiver 參數(shù)作用域

方法接收器(receiver)參數(shù)、函數(shù) / 方法參數(shù),以及返回值變量對(duì)應(yīng)的作用域范圍,都是函數(shù) / 方法體對(duì)應(yīng)的顯式代碼塊。

這就意味著,receiver 部分的參數(shù)名不能與方法參數(shù)列表中的形參名,以及具名返回值中的變量名存在沖突,必須在這個(gè)方法的作用域中具有唯一性。如果不唯一,比如下面的例子中那樣,Go 編譯器就會(huì)報(bào)錯(cuò):

type T struct{}

func (t T) M(t string) { // 編譯器報(bào)錯(cuò):duplicate argument t (重復(fù)聲明參數(shù)t)
    ... ...
}

不過,如果在方法體中沒有使用 receiver 參數(shù),我們也可以省略 receiver 的參數(shù)名,就像下面這樣:

type T struct{}

func (T) M(t string) { 
    ... ...
}

僅當(dāng)方法體中的實(shí)現(xiàn)不需要 receiver 參數(shù)參與時(shí),我們才會(huì)省略 receiver 參數(shù)名,不過這一情況很少使用,了解一下即可。

1.2.4 receiver 參數(shù)的基類型約束

Go 語言對(duì) receiver 參數(shù)的基類型也有約束,那就是 receiver 參數(shù)的基類型本身不能為指針類型或接口類型。

下面的例子分別演示了基類型為指針類型和接口類型時(shí),Go 編譯器報(bào)錯(cuò)的情況:

type MyInt *int
func (r MyInt) String() string { // r的基類型為MyInt,編譯器報(bào)錯(cuò):invalid receiver type MyInt (MyInt is a pointer type)
    return fmt.Sprintf("%d", *(*int)(r))
}

type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // r的基類型為MyReader,編譯器報(bào)錯(cuò):invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

1.2.5 方法聲明的位置約束

Go 要求,方法聲明要與 receiver 參數(shù)的基類型聲明放在同一個(gè)包內(nèi)?;谶@個(gè)約束,我們還可以得到兩個(gè)推論。

第一個(gè)推論:我們不能為原生類型(例如 int、float64、map 等)添加方法。例如,下面的代碼試圖為 Go 原生類型 int 增加新方法 Foo,這是不允許的,Go 編譯器會(huì)報(bào)錯(cuò):

func (i int) Foo() string { // 編譯器報(bào)錯(cuò):cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i) 
}

第二個(gè)推論:不能跨越 Go 包為其他包的類型聲明新方法。例如,下面的代碼試圖跨越包邊界,為 Go 標(biāo)準(zhǔn)庫中的 http.Server 類型添加新方法 Foo,這是不允許的,Go 編譯器同樣會(huì)報(bào)錯(cuò):

import "net/http"

func (s http.Server) Foo() { // 編譯器報(bào)錯(cuò):cannot define new methods on non-local type http.Server
}

1.2.6 如何使用方法

我們直接還是通過一個(gè)例子理解一下。如果 receiver 參數(shù)的基類型為 T,那么我們說 receiver 參數(shù)綁定在 T 上,我們可以通過 *T 或 T 的變量實(shí)例調(diào)用該方法:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通過類型T的變量實(shí)例調(diào)用方法M

    p := &T{}
    p.M(2) // 通過類型*T的變量實(shí)例調(diào)用方法M
}

這段代碼中,方法 M 是類型 T 的方法,通過 *T 類型變量也可以調(diào)用 M 方法。

二、方法的本質(zhì)

通過以上,我們知道了 Go 的方法與 Go 中的類型是通過 receiver 聯(lián)系在一起,我們可以為任何非內(nèi)置原生類型定義方法,比如下面的類型 T:

type T struct { 
    a int
}

func (t T) Get() int {  
    return t.a 
}

func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

在Go 中,Go 方法中的原理是將 receiver 參數(shù)以第一個(gè)參數(shù)的身份并入到方法的參數(shù)列表中。按照這個(gè)原理,我們示例中的類型 T 和 *T 的方法,就可以分別等價(jià)轉(zhuǎn)換為下面的普通函數(shù):

// 類型T的方法Get的等價(jià)函數(shù)
func Get(t T) int {  
    return t.a 
}

// 類型*T的方法Set的等價(jià)函數(shù)
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}

這種等價(jià)轉(zhuǎn)換后的函數(shù)的類型就是方法的類型。只不過在 Go 語言中,這種等價(jià)轉(zhuǎn)換是由 Go 編譯器在編譯和生成代碼時(shí)自動(dòng)完成的。Go 語言規(guī)范中還提供了方法表達(dá)式(Method Expression)的概念,可以讓我們更充分地理解上面的等價(jià)轉(zhuǎn)換。

以上面類型 T 以及它的方法為例,結(jié)合前面說過的 Go 方法的調(diào)用方式,我們可以得到下面代碼:

var t T
t.Get()
(&t).Set(1)

我們可以用另一種方式,把上面的方法調(diào)用做一個(gè)等價(jià)替換:

var t T
T.Get(t)
(*T).Set(&t, 1)

這種直接以類型名 T 調(diào)用方法的表達(dá)方式,被稱為Method Expression。通過Method Expression這種形式,類型 T 只能調(diào)用 T 的方法集合(Method Set)中的方法,同理類型 *T 也只能調(diào)用 *T 的方法集合中的方法。

我們看到,Method Expression 有些類似于 C++ 中的靜態(tài)方法(Static Method)。在 C++ 中的靜態(tài)方法使用時(shí),以該 C++ 類的某個(gè)對(duì)象實(shí)例作為第一個(gè)參數(shù)。而 Go 語言的 Method Expression 在使用時(shí),同樣以 receiver 參數(shù)所代表的類型實(shí)例作為第一個(gè)參數(shù)。

這種通過 Method Expression 對(duì)方法進(jìn)行調(diào)用的方式,與我們之前所做的方法到函數(shù)的等價(jià)轉(zhuǎn)換是如出一轍的。所以,Go 語言中的方法的本質(zhì)就是,一個(gè)以方法的 receiver 參數(shù)作為第一個(gè)參數(shù)的普通函數(shù)。

而且,Method Expression 就是 Go 方法本質(zhì)的最好體現(xiàn),因?yàn)榉椒ㄗ陨淼念愋途褪且粋€(gè)普通函數(shù)的類型,我們甚至可以將它作為右值,賦值給一個(gè)函數(shù)類型的變量,比如下面示例:

func main() {
    var t T
    f1 := (*T).Set // f1的類型,也是*T類型Set方法的類型:func (t *T, int)int
    f2 := T.Get    // f2的類型,也是T類型Get方法的類型:func(t T)int
    fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
    fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
    f1(&t, 3)
    fmt.Println(f2(t)) // 3
}

三、巧解難題

我們來看一段代碼:

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

這段代碼在我的多核 macOS 上的運(yùn)行結(jié)果是這樣(由于 Goroutine 調(diào)度順序不同,你自己的運(yùn)行結(jié)果中的行序可能與下面的有差異):

one
two
three
six
six
six

為什么對(duì) data2 迭代輸出的結(jié)果是三個(gè)“six”,而不是 four、five、six?

我們來分析一下。首先,我們根據(jù) Go 方法的本質(zhì),也就是一個(gè)以方法的 receiver 參數(shù)作為第一個(gè)參數(shù)的普通函數(shù),對(duì)這個(gè)程序做個(gè)等價(jià)變換。這里我們利用 Method Expression 方式,等價(jià)變換后的源碼如下:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go (*field).print(v)
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go (*field).print(&v)
    }

    time.Sleep(3 * time.Second)
}

這段代碼中,我們把對(duì) field 的方法 print 的調(diào)用,替換為 Method Expression 形式,替換前后的程序輸出結(jié)果是一致的。但變換后,問題是不是豁然開朗了!我們可以很清楚地看到使用 go 關(guān)鍵字啟動(dòng)一個(gè)新 Goroutine 時(shí),Method Expression 形式的 print 函數(shù)是如何綁定參數(shù)的:

  • 迭代 data1 時(shí),由于 data1 中的元素類型是 field 指針 (*field),因此賦值后 v 就是元素地址,與 print 的 receiver 參數(shù)類型相同,每次調(diào)用 (*field).print 函數(shù)時(shí)直接傳入的 v 即可,實(shí)際上傳入的也是各個(gè) field 元素的地址。
  • 迭代 data2 時(shí),由于 data2 中的元素類型是 field(非指針),與 print 的 receiver 參數(shù)類型不同,因此需要將其取地址后再傳入 (*field).print 函數(shù)。這樣每次傳入的 &v 實(shí)際上是變量 v 的地址,而不是切片 data2 中各元素的地址。

《Go 的 for 循環(huán),僅此一種》中,我們學(xué)習(xí)過 for range 使用時(shí)應(yīng)注意的幾個(gè)問題,其中循環(huán)變量復(fù)用是關(guān)鍵的一個(gè)。這里的 v 在整個(gè) for range 過程中只有一個(gè),因此 data2 迭代完成之后,v 是元素 "six" 的拷貝。

這樣,一旦啟動(dòng)的各個(gè)子 goroutine 在 main goroutine 執(zhí)行到 Sleep 時(shí)才被調(diào)度執(zhí)行,那么最后的三個(gè) goroutine 在打印 &v 時(shí),實(shí)際打印的也就是在 v 中存放的值 "six"。而前三個(gè)子 goroutine 各自傳入的是元素 "one"、"two" 和 "three" 的地址,所以打印的就是 "one"、"two" 和 "three" 了。

那么原程序要如何修改,才能讓它按我們期望,輸出“one”、“two”、“three”、“four”、 “five”、“six”呢?

其實(shí),我們只需要將 field 類型 print 方法的 receiver 類型由 *field 改為 field 就可以了。我們直接來看一下修改后的代碼:

type field struct {
    name string
}

func (p field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

修改后的程序的輸出結(jié)果是這樣的(因 Goroutine 調(diào)度順序不同,在你的機(jī)器上的結(jié)果輸出順序可能會(huì)有不同):

one
two
three
four
five
six

以上就是一文帶你理解Go語言中方法的本質(zhì)的詳細(xì)內(nèi)容,更多關(guān)于Go方法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 使用go mod導(dǎo)入本地自定義包問題

    使用go mod導(dǎo)入本地自定義包問題

    這篇文章主要介紹了使用go mod導(dǎo)入本地自定義包問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • Go語言使用字符串的幾個(gè)技巧分享

    Go語言使用字符串的幾個(gè)技巧分享

    這篇文章中小編將給出一些Go語言在處理字符串方面的技巧,對(duì)大家學(xué)習(xí)Go語言具有一定的參考借鑒價(jià)值,下面一起看看吧。
    2016-09-09
  • Go?gin框架加載Html模板文件的方法

    Go?gin框架加載Html模板文件的方法

    這篇文章主要介紹了Go?gin框架加載Html模板文件的方法,Gin框架沒有內(nèi)置靜態(tài)文件服務(wù),但可以使用gin.Static或gin.StaticFS中間件來提供靜態(tài)文件服務(wù),文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下
    2024-03-03
  • Go讀寫鎖操作方法示例詳解

    Go讀寫鎖操作方法示例詳解

    這篇文章主要為大家介紹了Go讀寫鎖方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • 學(xué)會(huì)提升Go語言編碼效率技巧拒絕加班!

    學(xué)會(huì)提升Go語言編碼效率技巧拒絕加班!

    這篇文章主要為大家介紹了Go語言編碼效率提升技巧詳解,學(xué)會(huì)了從此拒絕加班,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Go語言使用Etcd實(shí)現(xiàn)分布式鎖

    Go語言使用Etcd實(shí)現(xiàn)分布式鎖

    etcd是近幾年比較火熱的一個(gè)開源的、分布式的鍵值對(duì)數(shù)據(jù)存儲(chǔ)系統(tǒng),本文將介紹如何利用Etcd實(shí)現(xiàn)分布式鎖,感興趣的小伙伴可以跟隨小編一起了解一下
    2023-05-05
  • GoLang逃逸分析講解

    GoLang逃逸分析講解

    我們都知道go語言中內(nèi)存管理工作都是由Go在底層完成的,這樣我們可以不用過多的關(guān)注底層的內(nèi)存問題。本文主要總結(jié)一下?Golang內(nèi)存逃逸分析,需要的朋友可以參考以下內(nèi)容,希望對(duì)大家有幫助
    2022-12-12
  • Go語言跨平臺(tái)時(shí)字符串中的換行符如何統(tǒng)一?

    Go語言跨平臺(tái)時(shí)字符串中的換行符如何統(tǒng)一?

    本文介紹了Go語言中統(tǒng)一換行符的方法,包括使用`strings.ReplaceAll`函數(shù)將Windows風(fēng)格的換行符`\r\n`替換為Unix風(fēng)格的換行符`\n`,或?qū)\n`替換為`\r\n`,統(tǒng)一換行符可以避免不同平臺(tái)間顯示不一致、傳輸時(shí)出現(xiàn)多余的換行符或丟失換行符,以及解析錯(cuò)誤等問題
    2024-11-11
  • 基于Go實(shí)現(xiàn)TCP長(zhǎng)連接上的請(qǐng)求數(shù)控制

    基于Go實(shí)現(xiàn)TCP長(zhǎng)連接上的請(qǐng)求數(shù)控制

    在服務(wù)端開啟長(zhǎng)連接的情況下,四層負(fù)載均衡轉(zhuǎn)發(fā)請(qǐng)求時(shí),會(huì)出現(xiàn)服務(wù)端收到的請(qǐng)求qps不均勻的情況或是服務(wù)器無法接受到請(qǐng)求,因此需要服務(wù)端定期主動(dòng)斷開一些長(zhǎng)連接,所以本文給大家介紹了基于Go實(shí)現(xiàn)TCP長(zhǎng)連接上的請(qǐng)求數(shù)控制,需要的朋友可以參考下
    2024-05-05
  • go中值傳遞和指針傳遞的使用

    go中值傳遞和指針傳遞的使用

    在Go語言中,使用&和*可以分別取得變量的地址和值,解引用未初始化或?yàn)閚il的指針會(huì)引發(fā)空指針異常,正確的做法是先進(jìn)行nil檢查,此外,nil在Go中用于多種類型的空值表示,值傳遞和指針傳遞各有適用場(chǎng)景,通常小型數(shù)據(jù)結(jié)構(gòu)優(yōu)先考慮值傳遞以減少解引用開銷
    2024-10-10

最新評(píng)論