最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go基礎教程系列之Go接口使用詳解

 更新時間:2022年04月16日 17:16:19   作者:駿馬金龍  
這篇文章主要介紹了Go基礎教程系列之Go接口使用詳解,需要的朋友可以參考下

接口用法簡介

接口(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的接口可以實現面向對象中的多態(tài):可以按需調用名稱相同、功能不同的方法

接口實例中存的是什么

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

接口類型的數據結構是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保存的內容則如下圖:

實際上接口實例中保存的內容,在反射(reflect)中體現的淋漓盡致,reflect所有的一切都離不開接口實例保存的內容。

方法集(Method Set)規(guī)則

官方手冊對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是指針類型的方法只可能存在于指針類型的實例方法集中
  • receiver是值類型的方法既存在于值類型的實例方法集中,也存在于指針類型的方法集中

從實現接口方法的角度上看:

  • 如果某類型實現接口的方法的receiver是(T *Type)類型的,那么只有指針類型的實例*T才算是實現了這個接口,因為這個方法不在值類型的實例T方法集中
  • 如果某類型實現接口的方法的receiver是(T Type)類型的,那么值類型的實例T和指針類型的實例*T都算實現了這個接口,因為這個方法既在值類型的實例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())
}

所以,從struct類型定義的方法的角度去看,如果這個類型的方法有指針類型的receiver方法,則只能使用指針類型的實例賦值給接口變量,才算是實現了接口。如果這個類型的方法全是值類型的receiver方法,則可以隨意使用值類型或指針類型的實例賦值給接口變量。下面這兩個對應關系,對于理解很有幫助:

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

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

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

之所以能調用,是因為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。

普通方法和實現接口方法的區(qū)別

對于普通方法,無論是值類型還是指針類型的實例,都能正常調用,且調用時拷貝的內容都由receiver的類型決定。

func (T Type) method1   // 值類型receiver
func (T *Type) method2  // 指針類型receiver

指針類型的receiver決定了無論是值類型還是指針類型的實例,都拷貝實例的指針。值類型的receiver決定了無論是值類型還是指針類型的實例,都拷貝實例本身。

所以,對于person數據結構:

type person struct {}
p1 := person{}       // 值類型的實例
p2 := new(person)    // 指針類型的實例

p1.method1()p2.method1()都是拷貝整個person實例,只不過Go對待p2.method1()時多一個"步驟":將其解除引用。所以p2.method1()等價于(*p2).method1()。

p1.method2()p2.method2()都拷貝person實例的指針,只不過Go對待p1.method2()時多一個"步驟":創(chuàng)建一個額外的引用。所以,p1.method2()等價于(&p1).method2()。

而類型實現接口方法時,method set規(guī)則決定了類型實例是否實現了接口。

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

對于接口abc、接口方法method1()、method2()和結構person:

type abc interface {
	method1
	method2
}

type person struct {}
func (T person) method1   // 值類型receiver
func (T *person) method2  // 指針類型receiver

p1 := abc(person)  // 接口變量保存值類型實例
p2 := abc(&person) // 接口變量保存指針類型實例

p2.method1()p2.method2()以及p1.method1()都是允許的,都會通過接口實例去調用具體person實例的方法。

p1.method2()是錯誤的,因為method2()的receiver是指針類型的,導致p1沒有實現接口abc的method2()方法。

接口類型作為參數

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

例如,下面的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()中來,至于每個東西怎么輸出,那就要看具體情況:由類型的實現的String()方法決定。

接口類型的嵌套

接口可以嵌套,嵌套的內部接口將屬于外部接口,內部接口的方法也將屬于外部接口。

例如,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()
}

除此之外,類型嵌套時,如果內部類型實現了接口,那么外部類型也會自動實現接口,因為內部屬性是屬于外部屬性的。

更多關于Go基礎教程系列之Go接口的使用方法請查看下面的相關鏈接

相關文章

  • Golang 使用Map實現去重與set的功能操作

    Golang 使用Map實現去重與set的功能操作

    這篇文章主要介紹了Golang 使用 Map 實現去重與 set 的功能操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Go設計模式之訪問者模式講解和代碼示例

    Go設計模式之訪問者模式講解和代碼示例

    訪問者是一種行為設計模式, 允許你在不修改已有代碼的情況下向已有類層次結構中增加新的行為,本文將通過代碼示例給大家詳細的介紹一下Go設計模式之訪問者模式,需要的朋友可以參考下
    2023-08-08
  • golang gorm多條件篩選查詢操作

    golang gorm多條件篩選查詢操作

    這篇文章主要介紹了golang gorm多條件篩選查詢操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • 詳解Go語言中Goroutine退出機制的原理及使用

    詳解Go語言中Goroutine退出機制的原理及使用

    goroutine是Go語言提供的語言級別的輕量級線程,在我們需要使用并發(fā)時,我們只需要通過?go?關鍵字來開啟?goroutine?即可。本文就來詳細講講Goroutine退出機制的原理及使用,感興趣的可以了解一下
    2022-07-07
  • Golang中日志使用詳解

    Golang中日志使用詳解

    這篇文章記錄了Golang項目中日志使用,以及結合Gin框架記錄請求日志,文中通過代碼示例介紹的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下
    2024-01-01
  • 詳解Golang中使用map時的注意問題

    詳解Golang中使用map時的注意問題

    Golang中的map是一種數據結構,它允許你使用鍵值對的形式存儲和訪問數據,map在Go中是非排序的,提供了高效查找、插入和刪除元素的能力,特別是當鍵是不可變類型,本文給大家詳細介紹了Golang中使用map時的注意問題,需要的朋友可以參考下
    2024-06-06
  • 簡單易用的Go逗號comma ok模式使用詳解

    簡單易用的Go逗號comma ok模式使用詳解

    這篇文章主要為大家介紹了簡單易用的Go逗號comma ok模式使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-11-11
  • 使用goland調試遠程代碼的操作步驟

    使用goland調試遠程代碼的操作步驟

    大家都知道如何在goland調試遠程代碼嗎?今天小編給大家分享一篇教程幫助大家學習goland調試遠程代碼的操作步驟,感興趣的朋友跟隨小編一起看看吧
    2021-06-06
  • vscode插件設置之Golang開發(fā)環(huán)境配置全過程

    vscode插件設置之Golang開發(fā)環(huán)境配置全過程

    go語言開發(fā)選擇vscode作為IDE工具也是一個不錯的選擇,下面這篇文章主要給大家介紹了關于vscode插件設置之Golang開發(fā)環(huán)境配置的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下
    2022-12-12
  • golang MarshalJson的實現

    golang MarshalJson的實現

    本文主要介紹了golang MarshalJson的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-03-03

最新評論

临海市| 乌鲁木齐市| 伊宁市| 乌拉特中旗| 炎陵县| 陆良县| 清水县| 诸暨市| 菏泽市| 克东县| 孟连| 南郑县| 临沧市| 高密市| 罗定市| 辽中县| 和静县| 大悟县| 肇东市| 峨山| 揭东县| 五常市| 疏勒县| 顺昌县| 北辰区| 乐清市| 永顺县| 松桃| 丹寨县| 赣榆县| 襄樊市| 赤峰市| 亳州市| 广灵县| 沂源县| 专栏| 富裕县| 东台市| 交口县| 苏尼特右旗| 马鞍山市|