接口和其它类型

最后更新于: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服务,这都是因为接口就是一个方法的集合,其可以针对(几乎)任何类型来定义。
';