Golang笔记--interface

接口(interface)

[TOC]

接口类型是对其它类型行为的抽象和概括, 接口把实现分离, 可以此来实现面向对象编程的多态.

Golang接口独特之处在于它是隐式实现的, 具体类型只需实现接口定义的方法,而不用在定义时指明满足的接口类型, 好处之一就是对已经存在的类型, 可根据其方法定义某种接口, 从而让该类型自动满足接口定义而无需改变类型定义.

接口是契约

接口类型是一种抽象类型, 隐藏内部实现而只暴露方法, 这种封装和抽象是面向对象编程思想的精髓之一. 而就算是对面向对象编程的批判, 也说明了这一点. 对对象的抽象, 可能并不在乎它是什么, 而在于它能做什么, 比如我们说到"杯子", 不需要定义内部细节, 只需具有盛水喝水功能, 定义一个用于喝水的方法, 就可以称为杯子, 也许长得像碗.

  • 一个常见的例子就是格式化, 以下两个函数分别格式化输出到标准输出和字符串, 它们调用了同一个函数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()
}
  • 而调用的左边第一个参数分别为os.Stdoutbytes.Buffer,这里借助接口io.Writer实现, 使得fmt.Fprintf仅仅通过io.Writer接口来保证合约约束行为, 因而可以输出到不同类型
package io

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    // Write writes len(p) bytes from p to the underlying data stream.
    // It returns the number of bytes written from p (0 <= n <= len(p))
    // and any error encountered that caused the write to stop early.
    // Write must return a non-nil error if it returns n < len(p).
    // Write must not modify the slice data, even temporarily.
    //
    // Implementations must not retain p.
    Write(p []byte) (n int, err error)
}
  • io.Writer接口定义了函数Fprintf和这个函数调用者之间的约定,无论传递的是何种类型, 它都必须要实现这个接口, 因内部用到该接口Write签名方法

接口类型

接口类型是描述一系列方法的集合,实现了这些方法的类型就是该接口的实例.

  • io.Writer提供了所有的类型写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等
  • io.Reader可以代表任意可以读取bytes的类型
  • io.Closer可以是任意可以关闭的值,例如一个文件或是网络链接
  • 可组合这些接口得到新的接口定义:
// 嵌套的形式
type ReadWriter interface {
    Reader
    Writer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// 非嵌套的形式
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}
  • 赋值指定接口
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 
  • 方法的指针接收者(func(*T) method()*T), 在T类型的参数上调用一个*T的方法是合法的
    • 前提这个参数是一个变量,可以寻址, 编译器隐式的获取了它的地址
    • 只是语法糖: T并不具有 *T的方法, T实现的接口少于 *T
    • godoc -analysis=type tool 分析展示类型方法等
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver

// equals to &s.String()
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // compile error: IntSet lacks String method
  • interface{}, 空接口, 任何类型的对象都可以赋值给空接口类型变量
  • 非空的接口类型比如io.Writer经常被指针类型实现, 特别是改变接收者时
  • flag.Value接口, 定义此接口可用于flag命令行参数解析
package flag

// Value is the interface to the value stored in a flag.
type Value interface {
    String() string
    Set(string) error
}

接口值

  • 接口零值nil, 可比较: if reader !=nil
type nil
value nil
  • 赋值os.File
type *os.File
value pointer
  • 接口值可以使用==!=比较:
    • 两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等
    • 使用fmt包的%T动作可打印动态类型
// 动态类型不可比较会panic
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
  • 陷阱! 一个包含nil指针值的接口不是nil接口
func print(writer io.Writer) {
    if writer != nil {
        writer.Write("done\n")
    }
}

var buf *bytes.Buffer

//panic, buf不是nil interface, 是一个value为nil的Writer, 函数中if writer != nil
print(buf)

sort.Interafce接口

内置的排序算法需要知道以下三点:

  • 序列的长度
  • 元素比较方法
  • 交换两个元素的方式
package sort

type Interface interface {
    Len() int
    Less(i, j int) bool // i, j are indices of sequence elements
    Swap(i, j int)
}

实现以上接口就可以用内置的sor.Sort排序

http.Handler接口

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error
  • 实现该接口
func main() {
    db := database{"shoes": 50, "socks": 5}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}
  • 简单的按照路径路由:
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/list":
        for item, price := range db {
            fmt.Fprintf(w, "%s: %s\n", item, price)
        }
    case "/price":
        item := req.URL.Query().Get("item")
        price, ok := db[item]
        if !ok {
            w.WriteHeader(http.StatusNotFound) // 404, 先写Header
            fmt.Fprintf(w, "no such item: %q\n", item)
            return
        }
        fmt.Fprintf(w, "%s\n", price)
    default:
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such page: %s\n", req.URL)
    }
}
  • 请求多路器ServeMux, 替换以上case更加清晰实用
func main() {
    db := database{"shoes": 50, "socks": 5}
    mux := http.NewServeMux()
    mux.Handle("/list", http.HandlerFunc(db.list))
    mux.Handle("/price", http.HandlerFunc(db.price))
    log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

type database map[string]dollars

func (db database) list(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func (db database) price(w http.ResponseWriter, req *http.Request) {
    item := req.URL.Query().Get("item")
    price, ok := db[item]
    if !ok {
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such item: %q\n", item)
        return
    }
    fmt.Fprintf(w, "%s\n", price)
}    

pricelist类似于http.Handler的方法, 但本身不能直接作为Handler接口参数调用, http.HandlerFunc(db.list)是强制转换成http.HandlerFunc, 其中http.HandleFunc定义如下:

package http

type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

精髓来了: 这是个实现了http.Handler接口的函数类型, ServeHTTP 调用它本身的函数, HandleFunc是一个适配器, 将与接口方法具有相同函数签名的函数转换成一个接口实例, database也得以用两种方式满足http.Handler接口

另外, ServeMux可用HandleFunc方法简化调用:

mux.HandleFunc("/price", db.price)

error接口

type error interface {
    Error() string
}

创建error的方法, errors.Newfmt.Errorf:

package fmt

import "errors"

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

syscall的Errno

package syscall

type Errno uintptr // operating system error code

var errors = [...]string{
    1:   "operation not permitted",   // EPERM
    2:   "no such file or directory", // ENOENT
    3:   "no such process",           // ESRCH
    // ...
}

func (e Errno) Error() string {
    if 0 <= int(e) && int(e) < len(errors) {
        return errors[e]
    }
    return fmt.Sprintf("errno %d", e)
}

类型断言

语法: x.(T)被称为断言类型

  • 断言接口是否某种动态类型, 检查接口的动态类型
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
  • 断言是否满足某接口类型
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
  • bool返回代替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

基于类型断言判断错误类型

  • 大部分情况不要根据error.String()去判断错误类型, 而应该使用专门的类型来描述结构化的错误。如os.PathError
import (
    "errors"
    "syscall"
)

var ErrNotExist = errors.New("file does not exist")

// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
    if pe, ok := err.(*PathError); ok {
        err = pe.Err
    }
    return err == syscall.ENOENT || err == ErrNotExist
}

通过类型断言查询行为

以下Write时强制类型转换发生临时copy行为

func writeHeader(w io.Writer, contentType string) error {
    if _, err := w.Write([]byte("Content-Type: ")); err != nil {
        return err
    }
    if _, err := w.Write([]byte(contentType)); err != nil {
        return err
    }
    // ...
}

而某些io.Writer类型具有WriteString()方法会避免这种拷贝, 但不能假设所有Writer都具有这个方法, 这时候可以加入一个接口类型断言判断 (标准库有io.WriteString)

// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
    type stringWriter interface {
        WriteString(string) (n int, err error)
    }
    if sw, ok := w.(stringWriter); ok {
        return sw.WriteString(s) // avoid a copy
    }
    return w.Write([]byte(s)) // allocate temporary copy
}

func writeHeader(w io.Writer, contentType string) error {
    if _, err := writeString(w, "Content-Type: "); err != nil {
        return err
    }
    if _, err := writeString(w, contentType); err != nil {
        return err
    }
    // ...
}

另外一个例子, fmt.Fprintf区分errorStringer

package fmt

func formatOneValue(x interface{}) string {
    if err, ok := x.(error); ok {
        return err.Error()
    }
    if str, ok := x.(Stringer); ok {
        return str.String()
    }
    // ...all other types...
}

type switch

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

或者扩展形式, 这时候通过类型断言访问提取的值:

// x可判断类型, 且通过此变量访问实际类型的值
// switch x := x.(type) { /* ... */ }
func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case string:
        return sqlQuoteString(x) // (not shown)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

建议

  • 不做不必要的抽象, 大部分情况下, 只有一种实现时无需定义接口
  • 例外: 需要解耦具体实现的依赖时, 一种实现也可定义接口(实现不能存在于定义该接口的包中时)
  • 一个原则, 不定义过多方法, 小接口更容易实现, 只要需要的

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。