加载中...

接口和其它类型


接口

Go中的接口为指定对象的行为提供了一种方式:如果事情可以这样做,那么它就可以在这里使用。我们已经看到一些简单的例子;自定义的打印可以通过String方法来实现,而Fprintf可以通过Write方法输出到任意的地方。只有一个或两个方法的接口在Go代码中很常见,并且它的名字通常来自这个方法,例如实现Writeio.Writer

类型可以实现多个接口。例如,如果一个集合实现了sort.Interface,其包含Len()Less(i, j int) boolSwap(i, j int),那么它就可以通过程序包sort中的程序来进行排序,同时它还可以有一个自定义的格式器。在这个人造的例子中,Sequence同时符合这些条件。

  1. type Sequence []int
  2. // Methods required by sort.Interface.
  3. func (s Sequence) Len() int {
  4. return len(s)
  5. }
  6. func (s Sequence) Less(i, j int) bool {
  7. return s[i] < s[j]
  8. }
  9. func (s Sequence) Swap(i, j int) {
  10. s[i], s[j] = s[j], s[i]
  11. }
  12. // Method for printing - sorts the elements before printing.
  13. func (s Sequence) String() string {
  14. sort.Sort(s)
  15. str := "["
  16. for i, elem := range s {
  17. if i > 0 {
  18. str += " "
  19. }
  20. str += fmt.Sprint(elem)
  21. }
  22. return str + "]"
  23. }

转换

SequenceString方法重复了Sprint对切片所做的工作。如果我们在调用Sprint之前,将Sequence转换为普通的[]int,则可以共享所做的工作。

  1. func (s Sequence) String() string {
  2. sort.Sort(s)
  3. return fmt.Sprint([]int(s))
  4. }

这个对象方法算是转换技术的另一个例子,从String方法中安全地调用Sprintf。因为如果我们忽略类型名字,这两个类型(Sequence[]int)是相同的,在它们之间进行转换是合法的。该转换并不创建新的值,只不过是暂时使现有的值具有一个新的类型。(有其它的合法转换,像整数到浮点,是会创建新值的。)

将表达式的类型进行转换,来访问不同的方法集合,这在Go程序中是一种常见用法。例如,我们可以使用已有类型sort.IntSlice来将整个例子简化成这样:

  1. type Sequence []int
  2. // Method for printing - sorts the elements before printing
  3. func (s Sequence) String() string {
  4. sort.IntSlice(s).Sort()
  5. return fmt.Sprint([]int(s))
  6. }

现在,Sequence没有实现多个接口(排序和打印),相反的,我们利用了能够将数据项转换为多个类型(Sequencesort.IntSlice[]int)的能力,每个类型完成工作的一部分。这在实际中不常见,但是却可以很有效。

接口转换和类型断言

类型switch为一种转换形式:它们接受一个接口,在switch的每个case中,从某种意义上将其转换为那种case的类型。这里有一个简化版本,展示了fmt.Printf中的代码如何使用类型switch将一个值转换为字符串。如果其已经是字符串,那么我们想要接口持有的实际字符串值,如果其有一个String方法,则我们想要调用该方法的结果。

  1. type Stringer interface {
  2. String() string
  3. }
  4. var value interface{} // Value provided by caller.
  5. switch str := value.(type) {
  6. case string:
  7. return str
  8. case Stringer:
  9. return str.String()
  10. }

第一种情况找到一个具体的值;第二种将接口转换为另一个。使用这种方式进行混合类型完全没有问题。

如果我们只关心一种类型该如何做?如果我们知道值为一个string,只是想将它抽取出来该如何做?只有一个case的类型switch是可以的,不过也可以用类型断言。类型断言接受一个接口值,从中抽取出显式指定类型的值。其语法借鉴了类型switch子句,不过是使用了显式的类型,而不是type关键字:

  1. value.(typeName)

结果是一个为静态类型typeName的新值。该类型或者是一个接口所持有的具体类型,或者是可以被转换的另一个接口类型。要抽取我们已知值中的字符串,可以写成:

  1. str := value.(string)

不过,如果该值不包含一个字符串,则程序会产生一个运行时错误。为了避免这样,可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

  1. str, ok := value.(string)
  2. if ok {
  3. fmt.Printf("string value is: %q\n", str)
  4. } else {
  5. fmt.Printf("value is not a string\n")
  6. }

如果类型断言失败,则str将依然存在,并且类型为字符串,不过其为零值,一个空字符串。

这里有一个if-else语句的实例,其效果等价于这章开始的类型switch例子。

  1. if str, ok := value.(string); ok {
  2. return str
  3. } else if str, ok := value.(Stringer); ok {
  4. return str.String()
  5. }

概述

如果一个类型只是用来实现接口,并且除了该接口以外没有其它被导出的方法,那就不需要导出这个类型。只导出接口,清楚地表明了其重要的是行为,而不是实现,并且其它具有不同属性的实现可以反映原始类型的行为。这也避免了对每个公共方法实例进行重复的文档介绍。

这种情况下,构造器应该返回一个接口值,而不是所实现的类型。作为例子,在hash库里,crc32.NewIEEEadler32.New都是返回了接口类型hash.Hash32。在Go程序中,用CRC-32算法来替换Adler-32,只需要修改构造器调用;其余代码都不受影响。

类似的方式可以使得在不同crypto程序包中的流密码算法,可以与链在一起的块密码分离开。crypto/cipher程序包中的Block接口,指定了块密码的行为,即提供对单个数据块的加密。然后,根据bufio程序包类推,实现该接口的加密包可以用于构建由Stream接口表示的流密码,而无需知道块加密的细节。

crypto/cipher接口看起来是这样的:

  1. type Block interface {
  2. BlockSize() int
  3. Encrypt(src, dst []byte)
  4. Decrypt(src, dst []byte)
  5. }
  6. type Stream interface {
  7. XORKeyStream(dst, src []byte)
  8. }

这里有一个计数器模式(CTR)流的定义,其将块密码转换为流密码;注意块密码的细节被抽象掉了:

  1. // NewCTR returns a Stream that encrypts/decrypts using the given Block in
  2. // counter mode. The length of iv must be the same as the Block's block size.
  3. func NewCTR(block Block, iv []byte) Stream

NewCTR并不只是用于一个特定的加密算法和数据源,而是用于任何对Block接口的实现和任何Stream。因为它们返回接口值,所以将CTR加密替换为其它加密模式只是一个局部的改变。构造器调用必须被修改,不过因为上下文代码必须将结果只作为Stream来处理,所以其不会注意到差别。

接口和方法

由于几乎任何事物都可以附加上方法,所以几乎任何事物都能够满足接口的要求。一个示例是在http程序包中,其定义了Handler接口。任何实现了Handler的对象都可以为HTTP请求提供服务。

  1. type Handler interface {
  2. ServeHTTP(ResponseWriter, *Request)
  3. }

ResponseWriter本身是一个接口,提供了对用于向客户端返回响应的方法的访问。这些方法包括了标准的Write方法,所以任何可以使用io.Writer的地方,都可以使用http.ResponseWriter

简单起见,让我们忽略POST,假设HTTP请求总是GET;这种简化不影响建立处理的方式。这里有一个简单而完整的handler实现,用于计算页面的访问次数。

  1. // Simple counter server.
  2. type Counter struct {
  3. n int
  4. }
  5. func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  6. ctr.n++
  7. fmt.Fprintf(w, "counter = %d\n", ctr.n)
  8. }

(题外话,注意Fprintf是如何能够打印到http.ResponseWriter的。)作为参考,下面给出了如何将该服务附加到URL树上的节点。

  1. import "net/http"
  2. ...
  3. ctr := new(Counter)
  4. http.Handle("/counter", ctr)

但是为什么Counter为一个结构体?只需要一个整数就可以了。(接收者需要为一个指针,这样增量才能对调用者可见。)

  1. // Simpler counter server.
  2. type Counter int
  3. func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  4. *ctr++
  5. fmt.Fprintf(w, "counter = %d\n", *ctr)
  6. }

如果你的程序具有某个内部状态,当页面被访问时需要被告知,那么该如何?可以将一个channel绑定到网页上。

  1. // A channel that sends a notification on each visit.
  2. // (Probably want the channel to be buffered.)
  3. type Chan chan *http.Request
  4. func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  5. ch <- req
  6. fmt.Fprint(w, "notification sent")
  7. }

最后,比方说我们想在/args上展现我们唤起服务二进制时所使用的参数。这很容易编写一个函数来打印参数。

  1. func ArgServer() {
  2. fmt.Println(os.Args)
  3. }

我们怎么将它转换成HTTP服务?我们可以将ArgServer创建为某个类型的方法,忽略该类型的值,不过有一种更干净的方式。既然我们可以为除了指针和接口以外的任何类型来定义方法,那么我们可以为函数编写一个方法。http程序包包含了这样的代码:

  1. // The HandlerFunc type is an adapter to allow the use of
  2. // ordinary functions as HTTP handlers. If f is a function
  3. // with the appropriate signature, HandlerFunc(f) is a
  4. // Handler object that calls f.
  5. type HandlerFunc func(ResponseWriter, *Request)
  6. // ServeHTTP calls f(c, req).
  7. func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
  8. f(w, req)
  9. }

HandlerFunc为一个类型,其具有一个方法,ServeHTTP,所以该类型值可以为HTTP请求提供服务。看下该方法的实现:接收者为一个函数,f,并且该方法调用了f。这看起来可能有些怪异,但是这与接收者为channel,方法在channel上进行发送数据并无差别。

要将ArgServer放到HTTP服务中,我们首先将其签名修改正确。

  1. // Argument server.
  2. func ArgServer(w http.ResponseWriter, req *http.Request) {
  3. fmt.Fprintln(w, os.Args)
  4. }

ArgServer现在具有和HandlerFunc相同的签名,所以其可以被转换为那个类型,然后访问它的方法,就像我们将Sequence转换为IntSlice,来访问IntSlice.Sort一样。代码实现很简洁:

  1. http.Handle("/args", http.HandlerFunc(ArgServer))

当有人访问页面/args时,在该页上安装的处理者就具有值ArgServer和类型HandlerFunc。HTTP服务将会调用该类型的方法ServeHTTP,将ArgServer作为接收者,其将转而调用ArgServer(通过在HandlerFunc.ServeHTTP内部调用f(c, req))。然后,参数就被显示出来了。

在这章节,我们分别通过结构体,整数,channel,以及函数创建了HTTP服务,这都是因为接口就是一个方法的集合,其可以针对(几乎)任何类型来定义。


还没有评论.