Go基礎系列:Go介面

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/11/02/9898038.html
-Advertisement-
Play Games

介面用法簡介 介面(interface)是一種類型,用來定義行為(方法)。 但這些行為不會在介面上直接實現,而是需要用戶自定義的方法來實現。所以,在上面的Namer介面類型中的方法 都是沒有實際方法體的,僅僅只是在介面Namer中存放這些方法的簽名( )。 當用戶自定義的類型實現了介面上定義的這些方 ...


介面用法簡介

介面(interface)是一種類型,用來定義行為(方法)。

type Namer interface {
    my_method1()
    my_method2(para)
    my_method3(para) return_type
    ...
}

但這些行為不會在介面上直接實現,而是需要用戶自定義的方法來實現。所以,在上面的Namer介面類型中的方法my_methodN都是沒有實際方法體的,僅僅只是在介面Namer中存放這些方法的簽名(簽名 = 函數名+參數(類型)+返回值(類型))。

當用戶自定義的類型實現了介面上定義的這些方法,那麼自定義類型的值(也就是實例)可以賦值給介面類型的值(也就是介面實例)。這個賦值過程使得介面實例中保存了用戶自定義類型實例。

例如:

package main

import (
    "fmt"
)

// Shaper 介面類型
type Shaper interface {
    Area() float64
}

// Circle struct類型
type Circle struct {
    radius float64
}

// Circle類型實現Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

// Square struct類型
type Square struct {
    length float64
}

// Square類型實現Shaper中的方法Area()
func (s *Square) Area() float64 {
    return s.length * s.length
}

func main() {
    // Circle類型的指針類型實例
    c := new(Circle)
    c.radius = 2.5

    // Square類型的值類型實例
    s := Square{3.2}

    // Sharpe介面實例ins1,它只能是值類型的
    var ins1 Shaper
    // 將Circle實例c賦值給介面實例ins1
    // 那麼ins1中就保存了實例c
    ins1 = c
    fmt.Println(ins1)

    // 使用類型推斷將Square實例s賦值給介面實例
    ins2 := s
    fmt.Println(ins2)
}

上面將輸出:

&{2.5}
{3.2}

從上面輸出結果中可以看出,兩個介面實例ins1和ins2被分別賦值後,分別保存了指針類型的Circle實例c和值類型的Square實例s

另外,從上面賦值ins1和ins2的賦值語句上看:

ins1 = c
ins2 := s

是否說明介面實例ins就是自定義類型的實例?實際上介面是指針類型(指向什麼見下文)。這個時候,自定義類型的實例c、s稱為具體實例,ins實例是抽象實例,因為ins介面中定義的行為(方法)並沒有具體的行為模式,而c、s中的行為是具體的。

因為介面實例ins也是自定義類型的實例,所以當介面實例中保存了自定義類型的實例後,就可以直接從介面上調用它所保存的實例的方法。例如:

fmt.Println(ins1.Area())   // 輸出19.625
fmt.Println(ins2.Area())   // 輸出10.24

這裡ins1.Area()調用的是Circle類型上的方法Area(),ins2.Area()調用的則是Square類型上的方法Area()。這說明Go的介面可以實現面向對象中的多態:可以按需調用名稱相同、功能不同的方法

介面實例中存的是什麼

前面說了,介面類型是指針類型,但是它到底存放了什麼東西?

介面類型的數據結構是2個指針,占用2個機器字長。

當將類型實例c賦值給介面實例ins1後,用println()函數輸出ins1和c,比較它們的地址:

println(ins1)
println(c)

輸出結果:

(0x4ceb00,0xc042068058)
0xc042068058

從結果中可以看出,介面實例中包含了兩個地址,其中第二個地址和類型實例c的地址是完全相同的。而第二個地址c是Circle的指針類型實例,所以ins中的第二個值也是指針。

ins中的第一個是指針是什麼?它所指向的是一個內部表結構iTable,這個Table中包含兩部分:第一部分是實例c的類型信息,也就是*Circle,第二部分是這個類型(Circle)的方法集,也就是Circle類型的所有方法(此示例中Circle只定義了一個方法Area())。

所以,如圖所示:

註意,上圖中的實例c是指針,是指針類型的Circle實例。

對於值類型的Square實例s,ins2保存的內容則如下圖:

方法集(Method Set)規則

官方手冊對Method Set的解釋:https://golang.org/ref/spec#Method_sets

實例的method set決定了它所實現的介面,以及通過receiver可以調用的方法。

方法集是類型的方法集合,對於非介面類型,每個類型都分兩個Method Set:值類型實例是一個Method Set,指針類型的實例是另一個Method Set。兩個Method Set由不同receiver類型的方法組成:

實例的類型       receiver
--------------------------------------
 值類型:T       (T Type)
 指針類型:*T    (T Type)或(T *Type)

也就是說:

  • 值類型的實例的Method Set只由值類型的receiver(T Type)組成
  • 指針類型的實例的Method Set由值類型和指針類型的receiver共同組成,即(T Type)(T *Type)

這是什麼意思呢?從receiver的角度去考慮:

receiver        實例的類型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T

上面的意思是:

  • 如果某類型實現介面的方法的receiver是(T *Type)類型的,那麼只有指針類型的實例*T才算是實現了這個介面
  • 如果某類型實現介面的方法的receiver是(T Type)類型的,那麼值類型的實例T和指針類型的實例*T都算實現了這個介面

舉個例子。介面方法Area(),自定義類型Circle有一個receiver類型為(c *Circle)的Area()方法時,說明實現了介面的方法,但只有Circle實例的類型為指針類型時,這個實例才算是實現了介面,才能賦值給介面實例,才能當作一個介面參數。如下:

package main

import "fmt"

// Shaper 介面類型
type Shaper interface {
    Area() float64
}

// Circle struct類型
type Circle struct {
    radius float64
}

// Circle類型實現Shaper中的方法Area()
// receiver類型為指針類型
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // 聲明2個介面實例
    var ins1, ins2 Shaper

    // Circle的指針類型實例
    c1 := new(Circle)
    c1.radius = 2.5
    ins1 = c1
    fmt.Println(ins1.Area())

    // Circle的值類型實例
    c2 := Circle{3.0}
    // 下麵的將報錯
    ins2 = c2
    fmt.Println(ins2.Area())
}

報錯結果:

cannot use c2 (type Circle) as type Shaper
in assignment:
        Circle does not implement Shaper (Area method has
pointer receiver)

它的意思是,Circle值類型的實例c2沒有實現Share介面的Area()方法,它的Area()方法是指針類型的receiver。換句話說,值類型的c2實例的Method Set中沒有receiver類型為指針的Area()方法

所以,上面應該改成:

ins2 = &c2

再聲明一個方法,它的receiver是值類型的。下麵的代碼一切正常。

type Square struct{
    length float64
}

// 實現方法Area(),receiver為值類型
func (s Square) Area() float64{
    return s.length * s.length
}

func main() {
    var ins3,ins4 Shaper

    // 值類型的Square實例s1
    s1 := Square{3.0}
    ins3 = s1
    fmt.Println(ins3.Area())

    // 指針類型的Square實例s2
    s2 := new(Square)
    s2.length=4.0
    ins4 = s2
    fmt.Println(ins4.Area())
}

很經常的,我們會直接使用推斷類型的賦值方式(如ins2 := c2)將實例賦值給一個變數,我們以為這個變數是介面的實例,但實際上並不一定。正如上面值類型的c2賦值給ins2,這個ins2將是從c2數據結構拷貝而來的另一個副本數據結構,並非介面實例,但這時通過ins2也能調用Area()方法:

c2 = Circle{3.2}
ins2 := c2
fmt.Println(ins2.Area())  // 正常執行

之所以能調用,是因為Circle類型中有Area()方法,但這不是通過介面去調用的。

所以,在使用介面的時候,應當儘量使用var先聲明介面類型的實例,再將類型的實例賦值給介面實例(如var ins1,ins2 Shaper),或者使用ins1 := Shaper(c1)的方式。這樣,如果賦值給介面實例的類型實例沒有實現該介面,將會報錯。

但是,為什麼要限制指針類型的receiver只能是指針類型的實例的Method Set呢?

看下圖,假如指針類型的receiver可以組成值類型實例的Method Set,那麼介面實例的第二個指針就必須找到值類型的實例的地址。但實際上,並非所有值類型的實例都能獲取到它們的地址。

哪些值類型的實例找不到地址?最常見的是那些簡單數據類型的別名類型,如果匿名生成它們的實例,它們的地址就會被Go徹底隱藏,外界找不到這個實例的地址。

例如:

package main

import "fmt"

type myint int

func (m *myint) add() myint {
    return *m + 1
}
func main() {
    fmt.Println(myint(3).add())
}

以下是報錯信息:找不到myint(3)的地址

abc\abc.go:11:22: cannot call pointer method on myint(3)
abc\abc.go:11:22: cannot take the address of myint(3)

這裡的myint(3)是匿名的myint實例,它的底層是簡單數據類型int,myint(3)的地址會被徹底隱藏,只會提供它的值對象3。

介面類型作為參數

將介面類型作為參數很常見。這時,那些實現介面的實例都能作為介面類型參數傳遞給函數/方法。

例如,下麵的myArea()函數的參數是n Shaper,是介面類型。

package main

import (
    "fmt"
)

// Shaper 介面類型
type Shaper interface {
    Area() float64
}

// Circle struct類型
type Circle struct {
    radius float64
}

// Circle類型實現Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // Circle的指針類型實例
    c1 := new(Circle)
    c1.radius = 2.5
    myArea(c1)
}

func myArea(n Shaper) {
    fmt.Println(n.Area())
}

上面myArea(c1)是將c1作為介面類型參數傳遞給n,然後調用c1.Area(),因為實現了介面方法,所以調用的是Circle的Area()。

如果實現介面方法的receiver是指針類型的,但卻是值類型的實例,將沒法作為介面參數傳遞給函數,原因前面已經解釋過了,這種類型的實例沒有實現介面。

以介面作為方法或函數的參數,將使得一切都變得靈活且通用,只要是實現了介面的類型實例,都可以去調用它。

用的非常多的fmt.Println(),它的參數也是介面,而且是變長的介面參數:

$ go doc fmt Println
func Println(a ...interface{}) (n int, err error)

每一個參數都會放進一個名為a的Slice中,Slice中的元素是介面類型,而且是空介面,這使得無需實現任何方法,任何東西都可以丟帶fmt.Println()中來,至於每個東西怎麼輸出,那就要看具體情況。

介面類型的嵌套

介面可以嵌套,嵌套的內部介面將屬於外部介面,內部介面的方法也將屬於外部介面。

例如,File介面內部嵌套了ReadWrite介面和Lock介面。

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}
type Lock interface {
    Lock()
    Unlock()
}
type File interface {
    ReadWrite
    Lock
    Close()
}

除此之外,類型嵌套時,如果內部類型實現了介面,那麼外部類型也會自動實現介面,因為內部屬性是屬於外部屬性的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 《Accelerated C++ 中文版通過示例進行編程實踐》系統介紹C++程式設計,是美國斯坦福大學的經典教材。從使用C++標準庫中的高級抽象開始,使讀者很快掌握編程方法。每一章都有很經典獨特的例子以及非常到位的講解,覆蓋了C++更多領域的內容,從標準庫容器、泛型演算法的使用,到類的設計、泛型演算法的 ...
  • 環境安裝 環境安裝 主要包含三個部分 運行環境及開發sdk 系統環境和路徑配置 IDE配置 以mac環境為例,其他環境類似 運行環境及開發sdk 使用 brew 安裝 檢查,得到go基本安裝信息 系統環境和路徑配置 主要是GOROOT和GOPATH GOROOT:就是go的安裝環境 GOPATH:作 ...
  • Spring 是一個非常流行的基於Java語言的開發框架,此框架用來構建web和企業應用程式。與許多其他僅關註一個領域的框架不同,Spring框架提供了廣泛的功能,通過其組合項目滿足現代業務需求。 Spring框架提供了以多種方式配置bean的靈活性,例如XML,註解和JavaConfig。隨著功能 ...
  • 內置函數: continue... 傳送門 ...
  • 1、作用域相關 locals() 功能:返回當作用域中的名字 globals() 功能:返回全局作用域中的名字 2、迭代器/生成器相關 range() 功能:生成數據 iter() 功能:獲取迭代器,內部實際使用的是__iter__()方法來獲取迭代器 next() 功能:迭代器向下執行一次,內部實 ...
  • 介紹 在 C# 程式中嵌入 IronPython 得到了很好的支持。在本教程中,我們將展示如何完成這個項目。 首先,我們將展示兩個非常基本的例子,說明如何執行一個不導入任何模塊的非常簡單的腳本。然後,再展示如何執行使用模塊的腳本。 在 C 中執行 Python 第一個例子 我們來創建一個執行Pyth ...
  • 剛開始接觸 python 的時候,對 python 中的 wargs (可變參數) 和 kwargs (關鍵字參數)的理解不是很透徹,看了一下 《Explore Python》一書,裡面對這一部分的描述相對淺顯易懂, 這裡依據個人理解進行相關總結。 可變參數( args) 對於可變參數可以聯想到 C ...
  • #[每一個元素或者是和元素相關的操作 for 元素 in 可迭代數據類型] #遍歷之後挨個處理 #[滿足條件的元素相關的操作 for 元素 in 可迭代數據類型 if 元素相關的條件] #篩選功能 # #30以內所有能被3整除的數 ret = [i for i in range(30) if i%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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...