接口和其它类型
最后更新于:2022-04-01 04:21:10
[TOC]
## 接口
Go中的接口为指定对象的行为提供了一种方式:如果事情可以*这样*做,那么它就可以在*这里*使用。我们已经看到一些简单的例子;自定义的打印可以通过`String`方法来实现,而`Fprintf`可以通过`Write`方法输出到任意的地方。只有一个或两个方法的接口在Go代码中很常见,并且它的名字通常来自这个方法,例如实现`Write`的`io.Writer`。
类型可以实现多个接口。例如,如果一个集合实现了`sort.Interface`,其包含`Len()`,`Less(i, j int) bool`和`Swap(i, j int)`,那么它就可以通过程序包`sort`中的程序来进行排序,同时它还可以有一个自定义的格式器。在这个人造的例子中,`Sequence`同时符合这些条件。
~~~
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
~~~
## 转换
`Sequence`的`String`方法重复了`Sprint`对切片所做的工作。如果我们在调用`Sprint`之前,将`Sequence`转换为普通的`[]int`,则可以共享所做的工作。
~~~
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
~~~
这个对象方法算是转换技术的另一个例子,从`String`方法中安全地调用`Sprintf`。因为如果我们忽略类型名字,这两个类型(`Sequence`和`[]int`)是相同的,在它们之间进行转换是合法的。该转换并不创建新的值,只不过是暂时使现有的值具有一个新的类型。(有其它的合法转换,像整数到浮点,是会创建新值的。)
将表达式的类型进行转换,来访问不同的方法集合,这在Go程序中是一种常见用法。例如,我们可以使用已有类型`sort.IntSlice`来将整个例子简化成这样:
~~~
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
~~~
现在,`Sequence`没有实现多个接口(排序和打印),相反的,我们利用了能够将数据项转换为多个类型(`Sequence`,`sort.IntSlice`和`[]int`)的能力,每个类型完成工作的一部分。这在实际中不常见,但是却可以很有效。
## 接口转换和类型断言
[类型switch](http://www.hellogcc.org/effective_go.html#type_switch)为一种转换形式:它们接受一个接口,在switch的每个case中,从某种意义上将其转换为那种case的类型。这里有一个简化版本,展示了`fmt.Printf`中的代码如何使用类型switch将一个值转换为字符串。如果其已经是字符串,那么我们想要接口持有的实际字符串值,如果其有一个`String`方法,则我们想要调用该方法的结果。
~~~
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
~~~
第一种情况找到一个具体的值;第二种将接口转换为另一个。使用这种方式进行混合类型完全没有问题。
如果我们只关心一种类型该如何做?如果我们知道值为一个`string`,只是想将它抽取出来该如何做?只有一个case的类型switch是可以的,不过也可以用*类型断言*。类型断言接受一个接口值,从中抽取出显式指定类型的值。其语法借鉴了类型switch子句,不过是使用了显式的类型,而不是`type`关键字:
~~~
value.(typeName)
~~~
结果是一个为静态类型`typeName`的新值。该类型或者是一个接口所持有的具体类型,或者是可以被转换的另一个接口类型。要抽取我们已知值中的字符串,可以写成:
~~~
str := value.(string)
~~~
不过,如果该值不包含一个字符串,则程序会产生一个运行时错误。为了避免这样,可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:
~~~
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
~~~
如果类型断言失败,则`str`将依然存在,并且类型为字符串,不过其为零值,一个空字符串。
这里有一个`if`-`else`语句的实例,其效果等价于这章开始的类型switch例子。
~~~
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
~~~
## 概述
如果一个类型只是用来实现接口,并且除了该接口以外没有其它被导出的方法,那就不需要导出这个类型。只导出接口,清楚地表明了其重要的是行为,而不是实现,并且其它具有不同属性的实现可以反映原始类型的行为。这也避免了对每个公共方法实例进行重复的文档介绍。
这种情况下,构造器应该返回一个接口值,而不是所实现的类型。作为例子,在hash库里,`crc32.NewIEEE`和`adler32.New`都是返回了接口类型`hash.Hash32`。在Go程序中,用CRC-32算法来替换Adler-32,只需要修改构造器调用;其余代码都不受影响。
类似的方式可以使得在不同`crypto`程序包中的流密码算法,可以与链在一起的块密码分离开。`crypto/cipher`程序包中的`Block`接口,指定了块密码的行为,即提供对单个数据块的加密。然后,根据`bufio`程序包类推,实现该接口的加密包可以用于构建由`Stream`接口表示的流密码,而无需知道块加密的细节。
`crypto/cipher`接口看起来是这样的:
~~~
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
~~~
这里有一个计数器模式(CTR)流的定义,其将块密码转换为流密码;注意块密码的细节被抽象掉了:
~~~
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream
~~~
`NewCTR`并不只是用于一个特定的加密算法和数据源,而是用于任何对`Block`接口的实现和任何`Stream`。因为它们返回接口值,所以将CTR加密替换为其它加密模式只是一个局部的改变。构造器调用必须被修改,不过因为上下文代码必须将结果只作为`Stream`来处理,所以其不会注意到差别。
## 接口和方法
由于几乎任何事物都可以附加上方法,所以几乎任何事物都能够满足接口的要求。一个示例是在`http`程序包中,其定义了`Handler`接口。任何实现了`Handler`的对象都可以为HTTP请求提供服务。
~~~
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
~~~
`ResponseWriter`本身是一个接口,提供了对用于向客户端返回响应的方法的访问。这些方法包括了标准的`Write`方法,所以任何可以使用`io.Writer`的地方,都可以使用`http.ResponseWriter`。
简单起见,让我们忽略POST,假设HTTP请求总是GET;这种简化不影响建立处理的方式。这里有一个简单而完整的handler实现,用于计算页面的访问次数。
~~~
// Simple counter server.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
~~~
(题外话,注意`Fprintf`是如何能够打印到`http.ResponseWriter`的。)作为参考,下面给出了如何将该服务附加到URL树上的节点。
~~~
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
~~~
但是为什么`Counter`为一个结构体?只需要一个整数就可以了。(接收者需要为一个指针,这样增量才能对调用者可见。)
~~~
// Simpler counter server.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
~~~
如果你的程序具有某个内部状态,当页面被访问时需要被告知,那么该如何?可以将一个channel绑定到网页上。
~~~
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
~~~
最后,比方说我们想在`/args`上展现我们唤起服务二进制时所使用的参数。这很容易编写一个函数来打印参数。
~~~
func ArgServer() {
fmt.Println(os.Args)
}
~~~
我们怎么将它转换成HTTP服务?我们可以将`ArgServer`创建为某个类型的方法,忽略该类型的值,不过有一种更干净的方式。既然我们可以为除了指针和接口以外的任何类型来定义方法,那么我们可以为函数编写一个方法。`http`程序包包含了这样的代码:
~~~
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
~~~
`HandlerFunc`为一个类型,其具有一个方法,`ServeHTTP`,所以该类型值可以为HTTP请求提供服务。看下该方法的实现:接收者为一个函数,`f`,并且该方法调用了`f`。这看起来可能有些怪异,但是这与接收者为channel,方法在channel上进行发送数据并无差别。
要将`ArgServer`放到HTTP服务中,我们首先将其签名修改正确。
~~~
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
~~~
`ArgServer`现在具有和`HandlerFunc`相同的签名,所以其可以被转换为那个类型,然后访问它的方法,就像我们将`Sequence`转换为`IntSlice`,来访问`IntSlice.Sort`一样。代码实现很简洁:
~~~
http.Handle("/args", http.HandlerFunc(ArgServer))
~~~
当有人访问页面`/args`时,在该页上安装的处理者就具有值`ArgServer`和类型`HandlerFunc`。HTTP服务将会调用该类型的方法`ServeHTTP`,将`ArgServer`作为接收者,其将转而调用`ArgServer`(通过在`HandlerFunc.ServeHTTP`内部调用`f(c, req)`)。然后,参数就被显示出来了。
在这章节,我们分别通过结构体,整数,channel,以及函数创建了HTTP服务,这都是因为接口就是一个方法的集合,其可以针对(几乎)任何类型来定义。