《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

相关文章
相关标签/搜索