Go語言學習 二十 接口

本文最初發表在我的個人博客,查看原文,獲得更好的閱讀體驗


接口是一組方法的集合,接口類型的方法集是其接口。在Go中,接口的實現無需顯示指明,只需要實現類型實現了一個接口的所有方法,就表示該類型實現了該接口,這種實現方式也稱爲隱式實現。隱式接口實現解耦了接口的定義,這樣,可以在任何包中實現一個接口,同時,也使得接口的定義更加明確。

一 接口

1.1 接口聲明

接口的聲明需要使用interface關鍵字:
type 接口名稱 interface

例如,我們可以定義一個USB接口:

type Usb interface {
	Read(s string)

	Write(s string)
}

接口的零值爲nil

1.2 接口類型及實現

接口類型的變量可以保存任何實現了這些方法的值。接口中的每個方法都必須有一個唯一的非空名稱。

package main

import "fmt"

func main() {
	// var device Usb

	c := Computer{"電腦"}
	m := MobilePhone{"手機"}

	content := "Golang is an interesting language."

	Insert(c, content)
	Insert(m, content)

	var player Player

	player = &m
	player.Play("Country Road")

}

type Usb interface {
	Read(s string)

	Write(s string)

	// _(s string) 編譯錯誤:methods must have a unique non-blank name
}

// 接口實現類型1
type Computer struct {
	Name string
}

// 接口實現類型2
type MobilePhone struct {
	Name string
}

func (c Computer) Read(s string) {
	fmt.Printf("%v讀取了%v個字符:%v。\n", c.Name, len(s), s)
}

func (c Computer) Write(s string) {
	fmt.Printf("%v寫入了%v個字符:%v。\n", c.Name, len(s), s)
}

func (m MobilePhone) Read(s string) {
	fmt.Printf("%v讀取了%v個字符:%v。\n", m.Name, len(s), s)
}

func (m MobilePhone) Write(s string) {
	fmt.Printf("%v寫入了%v個字符:%v。\n", m.Name, len(s), s)
}

// 接口類型的參數
func Insert(device Usb, content string) {
	fmt.Printf("\n(設備類型:%T)\n", device)
	device.Read(content)
	device.Write(content)
}

type Player interface {
	Play(s string)
}

func (m MobilePhone) Play(s string) {
	fmt.Printf("%v開始播放歌曲:<<%v>>。\n", m.Name, s)
}

另外,類型可以實現多個接口:

type Player interface {
	Play(s string)
}

func (m MobilePhone) Play(s string) {
	fmt.Printf("%v開始播放歌曲:<<%v>>。\n", m.Name, s)
}

調用其他接口的方法:

var player Player
player = m
player.Play("Country Road")

二 接口值

2.1 接口值

接口也是值。可以像其它值一樣傳遞。接口值可以用作函數的參數或返回值。在內部,接口值可以看做包含值和具體類型的元組:
(value, type)
接口值保存了一個具體底層類型的具體值。

接口值調用方法時會執行其底層類型的同名方法,參考上一個例子的第13,14行調用:

// 接口類型的參數
func Insert(device Usb, content string) {
	fmt.Printf("\n(設備類型:%T)\n", device)
	device.Read(content)
	device.Write(content)
}

2.2 底層值爲 nil 的接口值

即使接口內的具體值爲nil,方法仍然可以被nil接收者調用。
這種情況如果在Java等語言中,會導致空指針異常,但在Go中通常會寫一些方法來優雅地處理(如下面的Play方法)。

注意: 保存了nil具體值的接口其自身並不爲nil

package main

import "fmt"

func main() {

	var p Player
	var m *MobilePhone
	p = m
	fmt.Println(p, m == nil) 
	p.Play("Country Road") // p本身不爲空,所以可以調用Play方法

	m = &MobilePhone{"手機"}
	p = m
	fmt.Println(p, m == nil)

	p.Play("Country Road")
}

type Player interface {
	Play(s string)
}

type MobilePhone struct {
	Name string
}

func (m *MobilePhone) Play(s string) {
	if m == nil {
		fmt.Println("player is now cannot work: <nil>")
		return
	}
	fmt.Printf("%v開始播放歌曲:<<%v>>。\n", m.Name, s)
}

如果接口值本身爲nil,則調用會出現運行時錯誤,因爲接口的元組內並未包含能夠指明該調用哪個 具體 方法的類型:

package main

import "fmt"

func main() {

	var p Player
	fmt.Println(p)

	p.Play("Country Road") // 該行會報錯:panic: runtime error: invalid memory address or nil pointer dereference
}

type Player interface {
	Play(s string)
}

三 空接口

沒有任何一個方法的接口值被稱爲 空接口

interface{}

空接口可保存任何類型的值。(因爲每個類型都至少實現了零個方法。)

空接口被用來處理未知類型的值。例如,fmt.Print可接受類型爲interface{}的任意數量的參數。

這有點類似Java中的Object超類

package main

import "fmt"

func main() {
	x := 5
	Display(x) // (5, int)

	s := "Country Road"
	Display(s) // (Country Road, string)

	var m = MobilePhone{"手機"}
	Display(m) // ({手機}, main.MobilePhone)
}

type Player interface {
	Play(s string)
}

type MobilePhone struct {
	Name string
}

func Display(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

四 方法集

類型可以具有與其關聯的方法集。接口類型的方法集是其接口。任何其他類型T的方法集由用接收器類型T聲明的所有方法組成。指針類型*T的方法集包含接收器爲*TT聲明的所有方法(也就是說,它包含T的方法集)。任何其他類型都有一個空方法集。在方法集中,每個方法必須具有唯一的非空方法名稱。

類型的方法集確定類型實現的接口以及可以使用該類型的接收器調用的方法。

五 轉換

我們已經知道,類型可以實現多個接口。例如下面的Sequence類型實現了標準庫中的接口sort.Interface,其中包含Len(), Less(i, j int) bool, 和 Swap(i, j int)。它現在就可以按照sort包中的例程進行排序,另外,它還有一個自定義的格式化器String()

type Sequence []int

// 實現sort.Interface中的方法
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// 複製一個序列
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// 打印序列前先排序
func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    str := "["
    for i, elem := range s { // 循環的複雜度是 O(N²);稍後優化這裏
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

SequenceString方法重新實現了Sprint爲切片實現的功能。(它的複雜度爲O(N²),很差。)如果我們在調用Sprint之前將Sequence轉換爲普通的[]int,就能共享已實現的功能(並加快速度)。

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

此方法是從String方法安全地調用Sprintf轉換技術的一個示例(關於如何安全地調用Sprintf轉換,可以參考後續打印一文)。因爲如果忽略類型名稱,兩種類型(Sequence[] int)其實是一樣的,在它們之間進行轉換是合法的。
轉換不會創建新值,只是暫時表現爲現有值有個新類型而已。(還有些其他合法轉換則會創建新值,例如從整數到浮點數等)
參考:Go語言學習 十三 類型轉換和類型推導

Go的一個習慣用法是轉換表達式的類型以訪問不同的方法集。例如,我們可以再將Sequence轉換爲現有類型sort.IntSlice,並直接使用其排序方法,整個示例可以縮減爲這樣:

type Sequence []int

// 打印前先排序
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

現在,我們不必讓Sequence實現多個接口(排序和打印),而是將它轉換爲多種類型(Sequencesort.IntSlice[]int)來使用相應的已有功能,每次轉換都完成一部分工作,這在實踐中很有用。

5.1 類型選擇

類型選擇可以根據接口值的具體類型進行選擇分支。前文提到過,類型選擇的語法與switch一般用法類似,只不過在括號中使用一個type關鍵字:

switch v := i.(type) {
case T:
    // v 的類型爲 T
case S:
    // v 的類型爲 S
default:
    // 沒有匹配,v 與 i 的類型相同
}

上述選擇語句判斷接口值i保存的值類型是T還是S。在TS的情況下,變量v會分別按TS類型保存i擁有的值。如果沒有匹配到,變量vi的接口類型和值相同。

package main

import "fmt"

func main() {
	x := 5
	check(x)

	y := Counter(3)
	check(y)

	s := "Country Road"
	check(s)

}

func check(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("int類型:(%v, %T)\n", v, v)
	case string:
		fmt.Printf("string類型:(%v, %T)\n", v, v)
	default:
		fmt.Println("未知類型:", v)

	}
}

type Counter int

另外,還可以將類型選擇理解爲一種轉換形式:其接受一個接口值,對於switch中的每個case,在某種意義上將其轉換爲該case的類型所對應的值。例如以下示例我們統一將值轉換爲int類型

package main

import "fmt"

func main() {
	x := 5
	v := convert(x)
	fmt.Printf("(%v, %T)\n", v, v)

	y := Counter(3)
	v = convert(y)
	fmt.Printf("(%v, %T)\n", v, v)
}

type Counter int

func convert(i interface{}) int {
	switch v := i.(type) {
	case int:
		return v
	case Counter:
		return int(v)
	}

	return 0
}

5.2 類型斷言

有時候我們只想關心一種類型,並且希望提取該類型的值。當然,類型選擇可以做到,例如:

package main

import "fmt"

func main() {

	var y interface{} = 3
	v := convert(y)
	fmt.Printf("(%v, %T)\n", v, v)
}

func convert(i interface{}) int {
	switch v := i.(type) {
	case int:
		return v
	}

	return 0
}

我們將類型選擇簡化到了只有一個case完成了int值的提取,但是還有另外一種更爲簡潔的方法,即類型斷言

類型斷言 提供了訪問接口值底層具體值的方式。它接受一個接口值,並從中提取指定的明確類型的值。其語法借鑑自類型選擇開頭的子句,但它需要一個明確的類型,而非type關鍵字:
v := i.(typeName)
其結果則是擁有靜態類型typeName的新值。該類型必須爲該接口所擁有的具體類型,或者該值可轉換成的第二種接口類型。要提取我們知道在該值中的int,可以這樣:

v := i.(int)

但若它所轉換的值中不包含字符串,程序就會以運行時錯誤崩潰。

package main

import "fmt"

func main() {

	var y interface{} = 3
	v := convert(y)
	fmt.Printf("(%v, %T)\n", v, v)

	var x uint = 3
	v = convert(x) // 轉換失敗,panic: interface conversion: interface {} is uint, not int
	fmt.Printf("(%v, %T)\n", v, v)
}

func convert(i interface{}) int {
	v := i.(int)
	return v
}

爲避免這種情況,需使用“逗號ok”慣用語法。爲了判斷一個接口值是否保存了一個特定的類型,類型斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布爾值。

package main

import "fmt"

func main() {

	var y interface{} = 3
	v := convert(y)
	fmt.Printf("(%v, %T)\n", v, v)

	var x uint = 3
	v = convert(x) // 轉換失敗,但不會報錯
	fmt.Printf("(%v, %T)\n", v, v)
}

func convert(i interface{}) int {
	v, ok := i.(int)
	if !ok {
		fmt.Println("轉換異常:", v)
	}

	return v
}

如果類型斷言失敗,v仍然存在,且爲int類型,但它將擁有零值,即0。

六 接口的通用性

若某種現有的類型僅實現了一個接口,且除此之外並無可導出的方法,則該類型本身就無需導出。僅導出該接口能讓我們更專注於其行爲而非實現。這也能夠避免爲每個通用接口的實例重複編寫文檔。

在這種情況下,構造函數應當返回一個接口值而非實現的類型。例如在hash庫中,crc32.NewIEEEadler32.New都返回接口類型 hash.Hash32。要在Go程序中用Adler-32算法替代CRC-32,只需修改構造函數調用即可,其餘代碼則不受算法改變的影響。

同樣的方式能將crypto包中多種聯繫在一起的流密碼算法與塊密碼算法分開。crypto/cipher包中的Block接口指定了塊密碼算法的行爲,它爲單獨的數據塊提供加密。接着,和bufio包類似,任何實現了該接口的密碼包都能被用於構造以Stream爲接口表示的流密碼,而無需知道塊加密的細節。

crypto/cipher 中的部分接口如下:

type Block interface {
    BlockSize() int
    Encrypt(src, dst []byte)
    Decrypt(src, dst []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

這是計數器模式CTR流的定義,它將塊加密改爲流加密,注意塊加密的細節已被抽象化了。

// NewCTR 返回一個 Stream,其加密/解密使用計數器模式中給定的 Block 進行。
// iv 的長度必須與 Block 的塊大小相同。
func NewCTR(block Block, iv []byte) Stream

NewCTR的應用並不僅限於特定的加密算法和數據源,它適用於任何對Block接口和Stream的實現。因爲它們返回接口值,所以用其它加密模式來代替CTR只需做局部的更改。構造函數的調用過程必須被修改,但由於其周圍的代碼只能將它看做Stream,因此它們不會注意到其中的區別。

參考:
https://golang.org/doc/effective_go.html#interfaces_and_types
https://golang.org/ref/spec#Interface_types
https://tour.go-zh.org/methods/9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章