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
package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error }
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
是徹底同樣的。工具
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
*T
的指針。在T
類型的變量上調用一個*T
的方法是合法的,編譯器隱式的獲取了它的地址。但這僅僅是一個語法糖:T類型的值不擁有全部*T指針的方法。var any interface{} any = true any = 12.34 any = "hello" any = map[string]int{"one": 1} any = new(bytes.Buffer)
==
和!=
對兩個接口值進行比較。若是兩個接口值的動態類型相同,可是這個動態類型是不可比較的(好比切片),將它們進行比較就會失敗而且panic:var x interface{} = []int{1, 2, 3} fmt.Println(x == x) // panic: comparing uncomparable type []int
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類型變量的指針。指針
調用一個包含*os.File
類型指針的接口值的Write方法,使得(*os.File).Write
方法被調用。這個調用輸出「hello」。
w.Write([]byte("hello")) // "hello"
第三個語句給接口值賦了一個*bytes.Buffer類型的值
w = new(bytes.Buffer)
如今動態類型是*bytes.Buffer而且動態值是一個指向新分配的緩衝區的指針(圖7.3)。
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指針的接口值是不一樣的。這個細微區別產生了一個容易絆倒每一個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。
動態分配機制依然決定(*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
type error interface { Error() string }
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...)) }
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
*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
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
接口被以兩種不一樣的方式使用。在第一個方式中,以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(可識別聯合)