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(可识别联合)