《Go 語言程序設計》讀書筆記(四)接口

接口概述

  • 一個具體的類型能夠準確的描述它所表明的值而且展現出對類型自己的一些操做方式就像數字類型的算術操做,切片類型的索引、附加和取範圍操做。總的來講,當你拿到一個具體的類型時你就知道它的自己是什麼和你能夠用它來作什麼。
  • 在Go語言中還存在着另一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露出它所表明的對象的內部結構和這個對象支持的基礎操做的集合;它只會展現出本身的方法。也就是說當你有看到一個接口類型的值時,你不知道它是什麼,惟一知道的就是能夠經過它的方法來作什麼。
  • fmt.Printf它會把結果寫到標準輸出和fmt.Sprintf它會把結果以字符串的形式返回,實際上,這兩個函數都使用了另外一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麼使用是徹底不知道的。
package fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)
    return buf.String()
}

​ Fprintf函數中的第一個參數也不是一個文件類型。它是io.Writer類型這是一個接口類型定義以下:程序員

package io

type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定,只要是實現了io.Writer接口的類型均可以做爲 Fprintf 函數的第一個參數。sql

  • 一個類型能夠自由的使用另外一個知足相同接口的類型來進行替換被稱做可替換性(LSP里氏替換)。這是一個面向對象的特徵。

接口定義

  • io.Writer類型是用的最普遍的接口之一,由於它提供了全部的類型寫入bytes的抽象,包括文件類型,內存緩衝區,網絡連接,HTTP客戶端,壓縮工具,哈希等等。io包中定義了不少其它有用的接口類型。Reader能夠表明任意能夠讀取bytes的類型,Closer能夠是任意能夠關閉的值,例如一個文件或是網絡連接。
package io
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
  • 能夠經過組合已有接口類型來定義新的接口類型,好比 io 包中的
type ReadWriter interface {
      Reader
      Writer
  }
  type ReadWriteCloser interface {
      Reader
      Writer
      Closer
  }

上面用到的語法和結構內嵌類似,咱們能夠用這種方式命名另外一個接口,而不用聲明它全部的方法。這種方式稱爲接口內嵌,咱們能夠像下面這樣,不使用內嵌來聲明io.ReadWriter接口。網絡

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

或者甚至使用種混合的風格:函數

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Writer
}

這三種方式定義的io.ReadWriter是徹底同樣的。工具

接口實現

  • 一個類型若是擁有一個接口須要的全部方法,那麼這個類型就實現了這個接口。例如,os.File類型實現了io.Reader,Writer,Closer,和ReadWriter接口。bytes.Buffer實現了Reader,Writer,和ReadWriter這些接口,可是它沒有實現Closer接口由於它不具備Close方法。Go的程序員常常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子,bytes.Buffer是io.Writer;os.Files是io.ReadWriter。
  • 接口實現的規則很是簡單:表達一個類型屬於某個接口只要這個類型實現這個接口。
var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
  • 這個規則甚至適用於等式右邊自己也是一個接口類型
w = rwc                 // OK: io.ReadWriteCloser has Write method
rwc = w                 // compile error: io.Writer lacks Close method
  • 由於ReadWriter和ReadWriteCloser包含Writer的方法,因此任何實現了ReadWriter和ReadWriteCloser的類型一定也實現了Writer接口
  • 對於一些命名的具體類型T;它一些方法的接收者是類型T自己然而另外一些則是一個*T的指針。在T類型的變量上調用一個*T的方法是合法的,編譯器隱式的獲取了它的地址。但這僅僅是一個語法糖:T類型的值不擁有全部*T指針的方法。
  • interface{}類型,它沒有任何方法,但實際上interface{}被稱爲空接口類型是不可或缺的。由於空接口類型對實現它的類型沒有要求,因此全部類型都實現了interface{},咱們能夠將任意一個值賦給空接口類型。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

接口值

  • 接口值由兩個部分組成,一個具體的類型和那個類型的值。它們被稱爲接口的動態類型和動態值。
  • 像Go語言這種靜態類型的語言,類型是編譯期的概念;所以一個類型不是一個值,提供每一個類型信息的值被稱爲類型描述符。
  • 在Go語言中,變量老是被一個定義明確的值初始化,一個接口的零值就是它的類型和值的部分都是nil。

    img

  • 在你很是肯定接口值的動態類型是可比較類型時(好比基本類型)纔可使用==!=對兩個接口值進行比較。若是兩個接口值的動態類型相同,可是這個動態類型是不可比較的(好比切片),將它們進行比較就會失敗而且panic:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
  • 下面4個語句中,變量w獲得了3個不一樣的值。(開始和最後的值是相同的)
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

第一個語句定義了變量w:ui

var w io.Writer

在Go語言中,變量老是被一個定義明確的值初始化,即便接口類型也不例外。對於一個接口的零值就是它的類型和值的部分都是nil,如圖 7.1。spa

一個接口值基於它的動態類型被描述爲空或非空,因此這是一個空的接口值。你能夠經過使用w==nil或者w!=nil來判讀接口值是否爲空。調用一個空接口值上的任意方法都會產生panic:debug

w.Write([]byte("hello")) // panic: nil pointer dereference

第二個語句將一個*os.File類型的值賦給變量w:設計

w = os.Stdout

這個賦值過程調用了一個具體類型到接口類型的隱式轉換,這和顯式的使用io.Writer(os.Stdout)是等價的。這類轉換不論是顯式的仍是隱式的,都會刻畫出操做到的類型和值。這個接口值的動態類型被設爲*os.File指針的類型描述符(os.Stdout 是指向 os.File 的指針),它的動態值持有os.Stdout的拷貝;這是一個指向處理標準輸出的os.File類型變量的指針。指針

img

調用一個包含*os.File類型指針的接口值的Write方法,使得(*os.File).Write方法被調用。這個調用輸出「hello」。

w.Write([]byte("hello")) // "hello"

第三個語句給接口值賦了一個*bytes.Buffer類型的值

w = new(bytes.Buffer)

如今動態類型是*bytes.Buffer而且動態值是一個指向新分配的緩衝區的指針(圖7.3)。

img

Write方法的調用也使用了和以前同樣的機制:

w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers

此次類型描述符是*bytes.Buffer,因此調用了(*bytes.Buffer).Write方法,而且接收者是該緩衝區的地址。這個調用把字符串「hello」添加到緩衝區中。

最後,第四個語句將nil賦給了接口值:

w = nil

這個重置將它全部的部分都設爲nil值,把變量w恢復到和它以前定義時相同的狀態圖,在圖7.1中能夠看到。

一個包含nil指針的接口不是nil接口

一個不包含任何值的nil接口值和一個恰好包含nil指針的接口值是不一樣的。這個細微區別產生了一個容易絆倒每一個Go程序員的陷阱。

思考下面的程序。當debug變量設置爲true時,main函數會將f函數的輸出收集到一個bytes.Buffer類型中。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // enable collection of output
    }
    f(buf) // NOTE: subtly incorrect!
    if debug {
        // ...use buf...
    }
}

// If out is non-nil, output will be written to it.
func f(out io.Writer) {
    // ...do something...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

咱們可能會預計當把變量debug設置爲false時能夠禁止對輸出的收集,可是實際上在out.Write方法調用時程序發生了panic:

if out != nil {
    out.Write([]byte("done!\n")) // panic: nil pointer dereference
}

當main函數調用函數f時,它給f函數的out參數賦了一個*bytes.Buffer的空指針,因此out的動值是nil。然而,它的動態類型是*bytes.Buffer,意思就是out變量是一個包含空指針值的非空接口(如圖7.5),因此防護性檢查out!=nil的結果依然是true。

img

動態分配機制依然決定(*bytes.Buffer).Write的方法會被調用,可是此次的接收者的值是nil。對於一些如*os.File的類型,nil是一個有效的接收者(§6.2.1),可是*bytes.Buffer類型不在這些類型中。這個方法會被調用,可是當它嘗試去獲取緩衝區時會發生panic。

問題在於儘管一個nil的*bytes.Buffer指針有實現這個接口的方法,它也不知足這個接口具體的行爲上的要求。特別是這個調用違反了(*bytes.Buffer).Write方法的接收者非空的隱含先覺條件,因此將nil指針賦給這個接口是錯誤的。解決方案就是將main函數中的變量buf聲明的類型改成io.Writer,(它的零值動態類型和動態值都爲 nil)所以能夠避免一開始就將一個不徹底的值賦值給這個接口:

var buf io.Writer
if debug {
    buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK

error 接口

  • 預約義的error類型實際上就是interface類型,這個類型有一個返回錯誤信息的單一方法:
type error interface {
      Error() string
}
  • 建立一個error最簡單的方法就是調用errors.New函數,它會根據傳入的錯誤信息返回一個新的error。整個errors包僅只有4行:
package errors

func New(text string) error { return &errorString{text} }

type errorString struct { text string }

func (e *errorString) Error() string { return e.text }

每一個New函數的調用都分配了一個獨特的和其餘錯誤不相同的實例。咱們也不想要重要的error例如io.EOF和一個恰好有相同錯誤消息的error比較後相等。

fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"

調用errors.New函數是很是稀少的,由於有一個方便的封裝函數fmt.Errorf,它還會處理字符串格式化。

package fmt

import "errors"

func Errorf(format string, args ...interface{}) error {
    return errors.New(Sprintf(format, args...))
}

類型斷言

  • 類型斷言是一個使用在接口值上的操做。語法上它看起來像x.(T)被稱爲斷言類型。這裏x表示一個接口值,T表示一個類型(接口類型或者具體類型)。一個類型斷言會檢查操做對象的動態類型是否和斷言類型匹配。
  • x.(T)中若是斷言的類型T是一個具體類型,類型斷言檢查x的動態類型是否和T相同。若是是,類型斷言的結果是x的動態值,固然它的類型是T。換句話說,具體類型的類型斷言從它的操做對象中得到具體的值。若是x 的動態類型與 T 不相同,會拋出panic。
var w io.Writer
w = os.Stdout
f := w.(*os.File)      // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
  • 相反斷言的類型T是一個接口類型,而後類型斷言檢查是否x的動態類型知足T。若是這個檢查成功了,這個結果仍然是一個有相同類型和值部分的接口值,可是結果接口值的動態類型爲T。換句話說,對一個接口類型的類型斷言改變了類型的表述方式,改變了能夠獲取的方法集合(一般更大),可是它保護了接口值內部的動態類型和值的部分。
  • 在下面的第一個類型斷言後,w和rw都持有os.Stdout由於它們每一個值的動態類型都是*os.File,可是變量的類型是io.Writer只對外公開出文件的Write方法,變量rw的類型爲 io.ReadWriter,只對外公開文件的Read方法。
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
  • 若是斷言操做的對象是一個nil接口值,那麼不論被斷言的類型是什麼這個類型斷言都會失敗。
  • 常常地咱們對一個接口值的動態類型是不肯定的,而且咱們更願意去檢驗它是不是一些特定的類型。若是類型斷言出如今一個有兩個結果的賦值表達式中,例如以下的定義,這個類型斷言不會在失敗的時候發生panic,代替地返回的第二個返回值是一個標識類型斷言是否成功的布爾值:
var w io.Writer = os.Stdout
f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

type switch

接口被以兩種不一樣的方式使用。在第一個方式中,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler,和error爲典型,一個接口的方法表達了實現這個接口的具體類型間的類似性,可是隱藏了表明的細節和這些具體類型自己的操做。重點在於方法上,而不是具體的類型上。

第二個方式利用一個接口值能夠持有各類具體類型值的能力而且將這個接口認爲是這些類型的union(聯合)。類型斷言用來動態地區別這些類型。在這個方式中,重點在於具體的類型知足這個接口,而不是在於接口的方法(若是它確實有一些的話),而且沒有任何的信息隱藏。咱們將以這種方式使用的接口描述爲discriminated unions(可辨識聯合)。

一個類型開關像普通的switch語句同樣,它的運算對象是x.(type)-它使用了關鍵詞字面量type-而且每一個case有一到多個類型。一個類型開關基於這個接口值的動態類型使一個多路分支有效。這個nil的case和if x == nil匹配,而且這個default的case和若是其它case都不匹配的狀況匹配。一個對sqlQuote的類型開關可能會有這些case

switch x.(type) {
    case nil:       // ...
    case int, uint: // ...
    case bool:      // ...
    case string:    // ...
    default:        // ...
}

類型開關語句有一個擴展的形式,它能夠將提取的值綁定到一個在每一個case範圍內的新變量上。

switch x := x.(type) { /* ... */ }

使用類型開關的擴展形式來重寫sqlQuote函數會讓這個函數更加的清晰:

func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x) // x has type interface{} here.
    case bool:
        if x {
            return "TRUE"
        }
        return "FALSE"
    case string:
        return sqlQuoteString(x) // (not shown)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

儘管sqlQuote接受一個任意類型的參數,可是這個函數只會在它的參數匹配類型開關中的一個case時運行到結束;其它狀況的它會panic出「unexpected type」消息。雖然x的類型是interface{},可是咱們把它認爲是一個int,uint,bool,string,和nil值的discriminated union(可識別聯合)

使用建議

  • 接口只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才須要。
  • 當一個接口只被一個單一的具體類型實現時有一個例外,就是因爲它的依賴,這個具體類型不能和這個接口存在在一個相同的包中。這種狀況下,一個接口是解耦這兩個包的一個好的方式。
  • 由於在Go語言中只有當兩個或更多的類型須以相同的方式進行處理時纔有必要使用接口,它們一定會從任意特定的實現細節中抽象出來。結果就是有更少和更簡單方法(常常和io.Writer或 fmt.Stringer同樣只有一個)的更小的接口。當新的類型出現時,小的接口更容易知足。對於接口設計的一個好的標準就是 ask only for what you need(只考慮你須要的東西)。

tWbHIMFsM3.png

相關文章
相關標籤/搜索