翻译: Effective Go (7)

用户头像
申屠鹏会
关注
发布于: 2020 年 08 月 16 日

接口和其他类型



接口



Go的接口提供了一种指定对象行为的方式:如果某个东西可以做到这些行为,则可以在这使用。我们已经见过一些简单的例子。自定义printer可以通过String方法实现,而Fprintf可以使用Write方法生成任意内容的输出。只有一个或两个方法的接口在Go代码中很常见,并且通常会从该方法派生一个名称,例如io.Writer来实现Write。

一个类型可以实现多个接口。例如,如果一个集合实现了sort.Interface,则可以使用sort包的程序进行排序。sort.Interface包含了Len(), Less(i, j int)bool, Swap(i, j int),当然也可以有自定义的格式化器。在这个特别的例子中,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]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}




类型转换



Sequence的String方法重复了Sprint早已为切片完成的工作。(而且性能很差,复杂度 O(N2) )如果在调用Sprint之前将Sequence转换成简单的[]int,那我们可以共享之前的工作(并有性能优化).



func (s Sequence) String() string {
s = s.Copy()
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 {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}




现在我们不必让Sequence实现多个接口(sort, print),而是使用将数据项转换成多种类型(Sequence, sort.IntSlice, []int)的能力,每种类型都能完成部分工作。在实践中,这种情况不常见,但是可以非常高效。



接口类型转换和类型断言



类型选择是类型转换的一种形式:它们接受一个接口,然后在switch表达式的每一个case中,在某种意义上转换成相应case的类型。这里是一个简化版的fmt.Printf,将一个值用类型转换成一个字符串。如果它本身是string类型,我们就输出真实的string值;如果本身带有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()
}




第一种情况是具体的值,第二个将接口转换成另一个接口。这种方式对于混合类型来说非常完美。

如果我们只关心一种类型呢?如果我们知道某值包含一个字符串,而我们只想提取它?一个单一情况的类型选择可以做到,除此之外,类型断言也可以。类型断言接受一个接口值,然后从中提取指定的显式类型的值。语法借鉴了开头的类型选择,但是它需要一个明确的类型,而非type关键字。



value.(typeName)




结果是一个静态类型typeName的新值。该类型必须是接口保留的具体类型,或者是可以将值转换为第二种接口的类型。为了提取其中string,我们可以这么写:



str := value.(string)




但是如果值不包含string,程序就会因为运行时错误而崩溃。为了防止这种情况,请使用", 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语句,它等效于此部分的类型选择。



if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}




通用



如果类型只是为了实现接口存在,并且永远不会有超出该接口的导出方法,则无需导出类型本身。仅导出接口即可清楚地知道该值除了接口中描述的内容外没有其他另类的行为。它还避免了需要在通用方法的每个实例上重复文档说明。

在这种情况下,构造函数应返回接口值而不是实现类型。例如,在哈希库中,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(dst, src []byte)
Decrypt(dst, src []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; 简化不会影响处理程序的设置方式。 这是一个简单但完整的处理程序实现,用于计算访问页面的次数。



// 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)
}




如果你的程序有一些内部状态需要通知已访问的页面怎么办?可以将管道绑定到网页。



// 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")
}




最后,假设我们要在on/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(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}




HandlerFunc是一种带有ServeHTTP方法的类型,因此该类型的值可以处理HTTP请求。 看一下该方法的实现:接收者是一个函数f,该方法调用f。 这可能看起来很奇怪,但是与接收方是管道和在该管道上发送方法没有什么不同。

为了使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服务器将以ArgServer作为接收器调用该类型的ServeHTTP方法,该方法随后将通过HandlerFunc.ServeHTTP内部的调用f(w,req)调用ArgServer。 然后将显示参数。

在本节中,我们通过结构,整数,通道和函数制成了HTTP服务器,这都是因为接口只是方法集,可以(几乎)定义任何类型。



空白标识符



在for range循环和map的背景下,我们已经多次提及空白标识符。 可以使用任何类型的任何值来分配或声明空白标识符,并且可以无害地丢弃该值。 这有点像写入Unix/dev/null文件:它表示只写值,用作需要变量但实际值无关的占位符。 它的用途超出了我们已经看到的用途。



多个赋值中的空白标识符



在for range循环中使用空白标识符是一种特殊情况:多个赋值。

如果赋值在左侧需要多个值,但是程序不会使用其中一个值,则赋值左侧的空白标识符可以避免创建虚拟变量的需要,并明确说明: 该值将被丢弃。 例如,当调用返回一个值和一个错误但仅错误重要的函数时,请使用空白标识符丢弃不相关的值。



if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}




有时,你会看到丢弃该错误值以忽略该错误的代码。 这是可怕的做法。 始终检查错误返回; 提供它们是有原因的。



// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}




未使用的包和变量



导入包或声明变量而不使用它是错误的。 未使用的导入会使程序臃肿,并且编译缓慢,而已初始化但未使用的变量至少会浪费计算量,并且可能表明有一个较大的错误。 然而,当程序正在积极开发中时,经常会出现未使用的导入和变量,并且为了继续进行编译而删除它们,而稍后又需要它们,可能会很烦人。 空白标识符提供了一种解决方法。

这个程序有两个未使用的导入包(fmt和io)和一个未使用的变量(fd),因此它不会编译,尽管目前为止的代码本身没有错误。



package main

import (
"fmt"
"io"
"log"
"os"
)

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}




要使对未使用包的错误进行消除,请使用空白标识符来引用导入包中的符号。 类似地,将未使用的变量fd分配给空白标识符将。这个版本的程序可以编译。



package main

import (
"fmt"
"io"
"log"
"os"
)

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader // For debugging; delete when done.

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}




按照惯例,为消除未导入包的错误的全局声明应在导入语句之后并加以注释,以使它们易于查找,并提醒以后进行清理。



导入的副作用



在之前的例子中,fmt或io的导入最终还是需要被使用或者被删除:空白标识符终归还是有作用的。但是有时候仅出于导入包的副作用而导入一个包,而不用任何使用。例如net/http/pprof包在其init函数期间注册提供调试信息的HTTP处理程序。它具有导出的API,但是大多数只需要注册的处理程序即可访问Web数据。要仅出于副作用导入软件包,可以将包重命名为空白标识符。



import _ "net/http/pprof"




这样导入的形式可以清晰的表示只用于包的副作用而导入一个包,因为没有任何可能可以使用这个包:在这个源文件中,它甚至没有一个名字。(如果它有名字,但是我们不使用的话,编译器就无法编译了。)



接口检查



正如上面对接口的一些讨论,一个类型无需显性说明它实现了某个接口。只要一个类型实现了某个接口的所有方法则代表实现了这个接口。在实际中,大多数接口转换都是静态的,因此可以在编译时进行检查。例如,除非*os.File实现了io.Reader接口,否则将*os.File传递给io.reader不会被编译。

但是,某些接口检查的确在运行时进行。encoding/json包中又一个例子,它定义了Marshaler接口。当JSON编码器接收到实现了该接口的值后,编码器将调用该值的编码方法,将其转换为JSON,而不是执行标准转换。编码器在运行时使用以下类型断言检查此属性:



m, ok := val.(json.Marshaler)




如果仅需要询问某个类型是否实现了一个接口,而不实际使用该接口本身(也许作为错误检查的一部分),则可以使用空白标识符忽略类型声明的值:



if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}




出现这种情况的一个地方是,有必要在实现该类型的包中保证它实际上满足接口的情况。 如果某个类型(例如json.RawMessage)需要自定义JSON表示形式,则应实现json.Marshaler,但是没有静态转换会导致编译器自动对此进行验证。 如果类型意外地不满足该接口,则JSON编码器仍将起作用,但将不使用自定义实现。 为了确保实现正确,可以在包中使用使用空白标识符的全局声明:



var _ json.Marshaler = (*RawMessage)(nil)




在此声明中,涉及将*RawMessage转换为Marshaler的赋值要求*RawMessage实现Marshaler,并且将在编译时检查该属性。如果json.Marshaler接口发生更改,则此程序包将不再编译,我们将注意到需要对其进行更新。

在此构造中出现空白标识符表示该声明仅存在于类型检查中,而不用于创建变量。但是,请不要对满足接口的每种类型执行这样的操作。通常,只有在代码中不存在静态转换这种罕见的情况下才使用此类声明。



发布于: 2020 年 08 月 16 日 阅读数: 100
用户头像

申屠鹏会

关注

enjoy~ 2018.11.08 加入

https://xabc.site

评论

发布
暂无评论
翻译: Effective Go (7)