第七章:接口
Golang 当中接口类型的独特之处在于它是满足隐式实现的。即:没必要对于给定的具体类型定义所有满足的接口类型,简单地拥有一些必要的方法即可。这种设计使得我们可以创建一个新的接口类型来满足已经存在的具体类型,却不会改变这些类型的定义。
7.1 接口约定
Golang 当中存在一种名为“接口”(interface)的抽象类型。它不会暴露它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它只会表现出它们自己的方法。也就是说,当我们看到一个接口类型时,我们不知道它具体是什么,但是我们知道可以通过它的方法来做什么。
例如,我们经常使用的fmt.Printf
和fmt.Sprintf
都是通过fmt.Fprintf
来进行封装的,fmt.Fprintf
这个函数对于它的计算结果将会被怎么使用是不知道的:
package fmtfunc 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.BufferFprintf(&buf, format, args...)return buf.String()
}
对于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
和这个函数调用者(比如Printf
和Sprintf
)之间的约定。一方面这个约定需要调用者提供具体类型的值,比如*os.File
和*bytes.Buffer
,这些类型都有一个Write
方法。另一方面这个约定确保Fprintf
接受任何满足io.Writer
接口的值都可以工作。
Fprintf
函数可能没有假定写入的是一个文件或一段内存,而是写入了一个可以调用Write
函数的值。
由于fmt.Fprintf
没有对具体操作的值做任何假设,而仅仅通过io.Writer
接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足io.Writer
接口的任意具体类型的值(意思是传入的值只要具备Write
方法,这个函数调用就是安全的)。一个类型可以自由地被另一个满足相同接口的类型替换,称为可替换性,这是一个面向对象的特征。
下例设计了一个新的类型来检验上述理论,其声明的*ByteCounter
类型里有一个Write
方法:
type ByteCounter intfunc (c *ByteCounter) Write(p []byte) (int, error) {*c += ByteCounter(len(p))return len(p), nil
}
由于*ByteCounter
满足io.Writer
的约定,可以把它传入Fprintf
函数当中:
var c ByteCounter
c.Write([]byte("hello"))
fmt.Println(c) // 5
c = 0
var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name)
fmt.Println(c) // 12
7.2 接口类型
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
io.Writer
是最广泛使用的接口之一,因为它提供了所有类型的写入 bytes 的抽象。io 包中定义了许多其它有用的接口类型。Reader 可以代表任意可以读取 bytes 的类型,Closer 可以是任意可以关闭的值,例如一个文件或网络连接:
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 {ReaderWriterCloser
}
上面用到的语法与结构定义当中的内嵌类似,我们当然可以将内嵌的方法展开,但略失简洁。可以混合使用展开定义和内嵌定义。
type ReadWriter interface {Read(p []byte) (n int, err error)Write(p []byte) (n int, err error)
}
// OR
type ReadWriter interface {Read(p []byte) (n int, err error)Writer
}
7.3 实现接口的条件
一个类型如果拥有一个接口所需的所有方法,那么这个类型就实现了这个接口。例如*os.File
实现了io.Reader
、Writer
、Closer
、ReadWriter
等接口。Golang 程序员通常会简要地把一个具体的类型描述成一个特定的接口类型。
接口的指定规则很简单,正如前面反复提到的,表达一个类型属于某个接口只需要这个类型实现了这个接口:
var w io.Writer
w = os.Stdout // OK
w = new(bytes.Buffer) // OK
w = time.Second // compile error: time.Duration lacks Write methodvar rwc io.ReadWriteCloser
rwc = os.Stdout // OK
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method// 上述规则适用于等式右侧本身也是一个接口的情况
w = rwc // OK
rwc = w // compile error: io.Writer lacks Close method
接口类型封装和隐藏了具体类型和它的值,即使具体类型有其它的方法,也只有接口暴露出来的方法可以被调用:
os.Stdout.Write([]byte("hello")) // OK
os.Stdout.Close() // OKvar w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // Ok
w.Close() // compile error: io.Writer lacks Close method
对于没有定义方法的接口interface{}
,它被称为空接口类型,它是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型(甚至,一个接受空接口类型的切片可以存储任意值):
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
对于创建的一个interface{}
,我们不能直接对它持有的值进行操作,因为interface{}
没有任何方法。
由于接口和实现只依赖于判断两个类型的方法是否相同,所以没必要定义一个具体类型和它实现的接口之间的关系。即:有意地在文档里说明或者程序上断言这种关系偶尔是有用的,但程序不强制这么做。下面的定义在编译器断言一个*bytes.Buffer
的值实现了io.Writer
接口类型:
var w io.Writer = new(bytes.Buffer)
因为任意*bytes.Buffer
的值,甚至 nil 通过 (*bytes.Buffer)(nil)
进行显式的转换都能实现这个接口,所以我们不必为其分配一个新的变量。
一个具体的类型可能实现了很多不相关的接口。例如,下例是一个数字文化产品销售的例子,以下列出了产品具有的具体类型:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
可以把每个抽象的特点用接口来表示:
type Artifact interface {Title() stringCreators() []stringCreated() time.Time
}
一些特性只有特定的产品才具有:
type Text interface {Pages() intWords() intPageSize() int
}
type Audio interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // e.g., "MP3", "WAV"
}
type Video interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string // e.g., "MP4", "WMV"Resolution() (x, y int)
}
可以定义一个 Streamer 接口来代表它们之间相同的部分而不必对已经存在的类型进行改变:
type Streamer interface {Stream() (io.ReadCloser, error)RunningTime() time.DurationFormat() string
}
7.4 flag.Value 接口
使用flag.Duration
可以完成时间周期的标记,这个特性被构建到了 flag 包中。如果我们想要为自己的数据定义新的标记符号,只需要定义一个实现flag.Value
接口的类型:
package flagtype Value interface {String() stringSet(string) error
}
String 方法格式化标记的值用在命令行帮助消息中;这样每一个 flag.Value
也是一个 fmt.Stringer
。Set
方法解析它的字符串参数并且更新标记变量的值。
下例定义了一个允许通过摄氏度或华氏度变换的形式指定温度的 celsiusFlag 类型。由于 celsiusFlag 内嵌了 Celsius 类型,因此它本身已经具有 String 方法了:
type celsiusFlag struct{ Celsius }func (f *celsisuFlag) Set(s string) error {var unit stringvar value float64fmt.Sscanf(s, "%f%s", &value, &unit) // no error check neededswitch unit {case "C", "°C":f.Celsius = Celsius(value)return nilcase "F", "°F":f.Celsius = FToC(Fahrenheit(value))return nil}return fmt.Errorf("invalid temperature %q", s)
}
下面的 CelsiusFlag 函数将所有逻辑封装在一起:
// CelsiusFlag defines a Celsius flag with the specified name,
// default value, and usage, and returns the address of the flag variable.
// The flag argument must have a quantity and a unit, e.g., "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {f := celsiusFlag{value}flag.CommandLine.Var(&f, name, usage)return &f.Celsius
}
现在我们可以在程序中使用新的标记:
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")func main() {flag.Parse()fmt.Println(*temp)
}
7.5 接口值
接口值由两部分组成,分别是一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于 Golang 这种静态语言,类型是编译器的概念,因此一个类型不是一个值。
下面的四个语句中,变量 w 得到了 3 个值:
var w io.Writer // 此时默认为零值 nil
w = os.Stdout
w = new(bytes.Buffer)
w = nil
第二个语句将*os.File
类型的值赋给了变量w:
w = os.Stdout
这个赋值过程调用了一个具体类型到接口类型的隐式转换,它和显式地使用io.Writer(os.Stdout)
是等价的。这种转换不管是显式还是隐式的,都会刻画出操作到的类型和值。接口值的动态类型(type)被设为*os.File
指针的类型描述符,它的动态值(value)持有os.Stdout
的拷贝:
7.5.1 警告:一个包含 nil 指针的接口不是 nil 接口
一个不包含任何值的 nil 接口值和一个刚好包含 nil 指针的接口值是不同的。
7.6 sort.Interface 接口
Golang 的 sort.Sort
函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface
来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定。
一个内置的排序算法需要知道三样东西:序列的长度、表示两个元素比较的结果、一种交换两个元素的方式,这就是sort.Interface
的三个方法:
package sorttype Interface interface {Len() intLess(i, j int) boolswap(i, j int)
}
为了对序列进行排序,我们需要定义一个实现了上述三个方法的类型,然后对这个类型的一个实例应用sort.Sort
函数:
type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
可以通过下面这样的方式对一个 string 切片排序:
sort.Sort(StringSlice(names))
7.7 http.Handler 接口
本小节会对那些基于 http.Handler
接口的服务器 API 进行进一步的学习:
package httptype Handler interface {ServeHTTP(w ResponseWriter, r *Request)
}func ListenAndServe(address string, h Handler) error
ListenAndServe
函数需要一个例如"localhost:8080"
这样的服务器地址,和一个所有请求都可以分派的Handler
接口实例。它会一直运行直到这个服务因为一个错误而失败,它的返回值是一个非空错误。
下例实现了一个简单的电子商务网站,它将库存清单模型化为一个命名为 database 的 map 类型,我们给这个类型一个 ServeHttp 方法,这样它就可以满足 http.Handler
接口,这个 handler 会遍历整个 map 并输出物品信息:
func main() {db := database{"shoes": 50, "socks": 5}log.Fatal(http.ListenAndServe("localhost:8000", db))
}type dollars float32func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }type database map[string]dollarsfunc (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {for item, price := range db {fmt.Fprintf(w, "%s: %s\n", item, price)}
}
目前为止这个服务器不考虑 URL,只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的 URL,每一个都会触发一个不同的行为:
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) // 404fmt.Fprintf(w, "no such item: %q\n", item)return}fmt.Fprintf(w, "%s\n", price)default:w.WriteHeader(http.StatusNotFound) // 404fmt.Fprintf(w, "no such page: %s\n", req.URL)}
}
现在的 handler 会根据 URL 路径部分来决定执行什么逻辑。
7.8 error 接口
在学习这本书的过程中已经大量接触过 error 类型。它实际上是一个 interface 类型,这个类型有一个返回错误信息的单一方法:
type error interface {Error() string
}
创建一个 error 最简单的方法就是调用 errors.New
函数,它会根据传入的错误信息返回一个新的 error。整个 errors 包只有四行:
package errorsfunc New(text string) error { return &errorString{text} }type errorString struct { text string }func (e *errorString) Error() string { return e.text }
调用 errors.New
的机会很少,因为有一个封装好的 fmt.Errorf
:
package fmtimport "errors"func Errorf(format string, args ...interface{}) error {return errors.New(Sprintf(format, args...)
}
7.9 示例:表达式求值
本节会构建一个简单的算术表达式求值器。我们将使用一个接口 Expr 来表示 Golang 中的任意表达式:
type Expr interface{} // 现在这个接口不需要方法, 稍后我们会为它定义一些
表达式的例子如下:
sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
下面是五个具体的表达式类型。Var 类型表示对一个变量的引用。literal 类型表示一个浮点型常量。unary 和 binary 表示一到两个运算对象的运算符表达式,这些操作数可以是任意的 Expr 类型。call 类型表示对一个函数的调用:
type Var stringtype literal float64type unary struct {op rune // + / -x Expr
}type binary struct {op rune // + / - / × / ÷x, y Expr
}type call struct {fn string // pow / sin / sqrtargs []Expr
}
为了计算包含变量的表达式,需要一个 environment 变量将变量的名字映射为对应的值:
type Env map[Var]float64
需要每个表达式定义一个 Eval 方法,这个方法会根据 environment 变量返回表达式的值。因为每个表达式都提供这个方法,因此将它加入到 Expr 接口中。这个包只会对外公开 Expr、Env、Var 类型:
type Expr interface {Eval(env Env) float64
}
具体的 Eval 方法如下:
func (v Var) Eval(env Env) float64 {return env[v]
}func (l literal) Eval(_Env) float64 {return float64(l)
}func (u unary) Eval(env Env) float64 {switch u.op {case '+':return +u.x.Eval(env)case '-':return -u.x.Eval(env)}panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
}func (b binary) Eval(env Env) float64 {switch b.op {case '+':return b.x.Eval(env) + b.y.Eval(env)case '-':return b.x.Eval(env) - b.y.Eval(env)case '*':return b.x.Eval(env) * b.y.Eval(env)case '/':return b.x.Eval(env) / b.y.Eval(env)}panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
}func (c call) Eval(env Env) float64 {switch c.fn {case "pow":return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))case "sin":return math.Sin(c.args[0].Eval(env))case "sqrt":return math.Sqrt(c.args[0].Eval(env))}panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}
现在我们向 Expr 接口添加一个 Check 方法,它会对一个表达式语法树检查静态错误:
type Expr interface {Eval(env Env) float64// Check reports errors in this Expr and adds its Vars to the set.Check(vars map[Var]bool) error
}
具体的 Check 方法;
func (v Var) Check(vars map[Var]bool) error {vars[v] = truereturn nil
}func (literal) Check(vars map[Var]bool) error {return nil
}func (u unary) Check(vars map[Var]bool) error {if !strings.ContainsRune("+-", u.op) {return fmt.Errorf("unexpected unary op %q", u.op)}return u.x.Check(vars)
}func (b binary) Check(vars map[Var]bool) error {if !strings.ContainsRune("+-*/", b.op) {return fmt.Errorf("unexpected binary op %q", b.op)}if err := b.x.Check(vars); err != nil {return err}return b.y.Check(vars)
}func (c call) Check(vars map[Var]bool) error {arity, ok := numParams[c.fn]if !ok {return fmt.Errorf("unknown function %q", c.fn)}if len(c.args) != arity {return fmt.Errorf("call to %s has %d args, want %d",c.fn, len(c.args), arity)}for _, arg := range c.args {if err := arg.Check(vars); err != nil {return err}}return nil
}var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
7.10 类型断言
类型断言是一个使用在接口值上的操作。语法上看起来像x.(T)
,此处的x
表示一个接口类型而T
表示一个类型。类型断言会检查它操作对象的动态类型是否和断言的类型匹配。
类型断言有两种可能。第一种,如果断言的类型T
是一个具体类型,类型断言检查x
的动态类型是否和T
相同。如果检查成功,那么断言的结果是x
的动态值,当然它的类型是T
。检查失败则抛出 panic:
var w io.Writer
w = os.Stdout
f := w.(*os.File)
c := w.(*bytes.Buffer)
一个替代的方案如下,使用 ok 作为类型断言的第二个接收值,此时当类型断言失败时,不会产生 panic:
var w io.Writer = os.Stdout
f, ok := w.(*os.File)
b, ok := w.(*bytes.Buffer)
7.11 基于类型断言区别错误类型
os 包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:
package osfunc IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
一个更可靠的方式是使用一个专门的类型来描述结构化的错误:
package os// PathError records an error and the operation and file path that caused it.
type PathError struct {Op stringPath stringErr error
}func (e *PathError) Error() string {return e.Op + " " + e.Path + ": " + e.Err.Error()
}
7.12 通过类型断言询问行为
7.13 类型分支
7.14 示例:基于标记的 XML 解码
7.15 一些建议
(7.12 ~ 7.14 这部分中文版资源机翻的痕迹太明显,读起来有些晦涩,打算真正需要用到的时候再说)
接口只有当有两个或以上的具体类型必须以相同的方式进行处理时才需要。