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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352