Golang之interface

siwluxuefeng發表於2020-12-08

介面型別 是由一組方法簽名定義的集合。

介面型別的變數可以儲存任何實現了這些方法的值。

注意: 示例程式碼的 22 行存在一個錯誤。由於 Abs 方法只為 *Vertex (指標型別)定義,因此 Vertex(值型別)並未實現 Abser

package main

import (
	"fmt"
	"math"
)

type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // a MyFloat 實現了 Abser
	a = &v // a *Vertex 實現了 Abser

	// 下面一行,v 是一個 Vertex(而不是 *Vertex)
	// 所以沒有實現 Abser。
	a = v

	fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

型別通過實現一個介面的所有方法來實現該介面。既然無需專門顯式宣告,也就沒有“implements”關鍵字。

隱式介面從介面的實現中解耦了定義,這樣介面的實現可以出現在任何包中,無需提前準備。

因此,也就無需在每一個實現上增加新的介面名稱,這樣同時也鼓勵了明確的介面定義。

 

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

// 此方法表示型別 T 實現了介面 I,但我們無需顯式宣告此事。
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

介面也是值。它們可以像其它值一樣傳遞。

介面值可以用作函式的引數或返回值。

在內部,介面值可以看做包含值和具體型別的元組:

(value, type)

介面值儲存了一個具體底層型別的具體值。

介面值呼叫方法時會執行其底層型別的同名方法。

package main

import (
	"fmt"
	"math"
)

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func main() {
	var i I

	i = &T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

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

底層值為 nil 的介面值

即便介面內的具體值為 nil,方法仍然會被 nil 接收者呼叫。

在一些語言中,這會觸發一個空指標異常,但在 Go 中通常會寫一些方法來優雅地處理它(如本例中的 M 方法)。

注意: 儲存了 nil 具體值的介面其自身並不為 nil。

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

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

nil 介面值

nil 介面值既不儲存值也不儲存具體型別。

為 nil 介面呼叫方法會產生執行時錯誤,因為介面的元組內並未包含能夠指明該呼叫哪個 具體 方法的型別。

package main

import "fmt"

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

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

空介面

指定了零個方法的介面值被稱為 *空介面:*

interface{}

空介面可儲存任何型別的值。(因為每個型別都至少實現了零個方法。)

空介面被用來處理未知型別的值。例如,fmt.Print 可接受型別為 interface{} 的任意數量的引數。

package main

import "fmt"

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

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

型別斷言 提供了訪問介面值底層具體值的方式。

t := i.(T)

該語句斷言介面值 i 儲存了具體型別 T,並將其底層型別為 T 的值賦予變數 t

若 i 並未儲存 T 型別的值,該語句就會觸發一個恐慌。

為了 判斷 一個介面值是否儲存了一個特定的型別,型別斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布林值。

t, ok := i.(T)

若 i 儲存了一個 T,那麼 t 將會是其底層值,而 ok 為 true

否則,ok 將為 false 而 t 將為 T 型別的零值,程式並不會產生恐慌。

請注意這種語法和讀取一個對映時的相同之處。

package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // 報錯(panic)
	fmt.Println(f)
}

型別選擇

型別選擇 是一種按順序從幾個型別斷言中選擇分支的結構。

型別選擇與一般的 switch 語句相似,不過型別選擇中的 case 為型別(而非值), 它們針對給定介面值所儲存的值的型別進行比較。

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

型別選擇中的宣告與型別斷言 i.(T) 的語法相同,只是具體型別 T 被替換成了關鍵字 type

此選擇語句判斷介面值 i 儲存的值型別是 T 還是 S。在 T 或 S 的情況下,變數 v 會分別按 T 或 S 型別儲存 i 擁有的值。在預設(即沒有匹配)的情況下,變數 v 與 i 的介面型別和值相同。

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

Stringer

fmt 包中定義的 Stringer 是最普遍的介面之一。

type Stringer interface {
    String() string
}

Stringer 是一個可以用字串描述自己的型別。fmt 包(還有很多包)都通過此介面來列印值。

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}

錯誤

Go 程式使用 error 值來表示錯誤狀態。

與 fmt.Stringer 類似,error 型別是一個內建介面:

type error interface {
    Error() string
}

(與 fmt.Stringer 類似,fmt 包在列印值時也會滿足 error。)

通常函式會返回一個 error 值,呼叫的它的程式碼應當判斷這個錯誤是否等於 nil 來進行錯誤處理。

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

error 為 nil 時表示成功;非 nil 的 error 表示失敗。

package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Reader

io 包指定了 io.Reader 介面,它表示從資料流的末尾進行讀取。

Go 標準庫包含了該介面的許多實現,包括檔案、網路連線、壓縮和加密等等。

io.Reader 介面有一個 Read 方法:

func (T) Read(b []byte) (n int, err error)

Read 用資料填充給定的位元組切片並返回填充的位元組數和錯誤值。在遇到資料流的結尾時,它會返回一個 io.EOF 錯誤。

示例程式碼建立了一個 strings.Reader 並以每次 8 位元組的速度讀取它的輸出。

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

 

相關文章