这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

接口

Golang 接口的语言特性

1 - 接口概述

Golang 接口的语言特性概述

学习资料

2 - 接口定义

Golang 的接口定义

go语言实战

接口定义和实现

接口类型是由一组方法定义的集合。

// 定义一个interface和它的方法
type Abser interface {
	Abs() float64
}

type Vertex struct {
	X, Y float64
}

// 让结构体实现interface要求的方法
func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

接口类型的值可以存放实现这些方法的任何值。

var a Abser
v := Vertex{3, 4}
a = &v // *Vertex 实现了 Abser

任意接口类型

在go中,如果要表示类型为任意类型,包括基础类型,可以这样做:

type Value struct {
	v interface{}
}

有点类似java中的Object,但是go没有对象继承,也没有Object这种单根继承的root对象,为了表示所有类型,就需要使用interface关键字,而interface在go中是关键字,不是类型,因此要加{} 后缀。这个语法相对java有点特别。

Effective Go

接口命名

https://golang.org/doc/effective_go.html#interface-names

按照惯例,单方法接口的命名是由方法名加上 -er 后缀或类似的修饰来构造一个代理名词:Reader, Writer, Formatter, CloseNotifier等等。

这样的名字有很多,尊重它们和它们所使用的函数名是很有成效的。Read、Write、Close、Flush、String等都有规范的标志和含义。为了避免混淆,不要给你的方法起这些名字,除非它有相同的签名和含义。反过来说,如果你的类型实现了一个与一个著名类型上的方法具有相同含义的方法,就给它相同的名称和签名;调用你的字符串转换方法String而不是ToString。

Generality/通用性

https://golang.org/doc/effective_go.html#generality

如果一个类型只是为了实现一个接口而存在,并且永远不会有超出该接口的导出方法,那么就没有必要导出类型本身。只导出接口,就可以清楚地知道该值除了接口中描述的内容之外没有其他需要注意的行为。这也避免了在一个普通方法的每个实例上重复文档的需要。

在这种情况下,构造函数应该返回一个接口值而不是实现类型。举个例子,在哈希库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。在Go程序中用CRC-32算法代替Adler-32算法,只需要改变构造函数调用,其余代码不受算法改变的影响。

类似的方法使得各种加密包中的流式密码算法与它们链在一起的块密码算法分离。crypto/cipher包中的Block接口指定了块密码的行为,它提供了单个数据块的加密。那么,通过与bufio包类比,实现这个接口的密码包可以用来构造流密码,用Stream接口表示,而不知道块加密的细节。

加密/密文接口是这样的。

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,所以它不会注意到这种差异。

接口与方法

https://golang.org/doc/effective_go.html#interface_methods

由于几乎任何东西都可以附加方法,所以几乎任何东西都可以满足接口。一个说明性的例子是在http包中,它定义了Handler接口。任何实现Handler的对象都可以服务于HTTP请求。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter 本身就是一个接口,它提供了对返回响应给客户端所需方法的访问。这些方法包括标准的 Write 方法,所以 http.ResponseWriter 可以在任何可以使用 io.Writer 的地方使用。Request是一个包含客户端请求的解析表示的结构。

为了简洁起见,我们忽略 POSTs,并假设 HTTP 请求总是 GETs;这种简化并不影响处理程序的设置方式。下面是一个琐碎但完整的处理程序的实现,用来统计页面被访问的次数。

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

最后,假设我们想在/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服务器将调用该类型的ServeHTTP方法,ArgServer作为接收方,而接收方又会调用ArgServer(通过HandlerFunc.ServeHTTP内部的调用f(w, req))。然后,参数将被显示出来。

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

3 - 接口实现

Golang 中的接口实现

go语言实战

隐式接口

类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。

隐式接口解藕了实现接口的包和定义接口的包:互不依赖。

因此,也就无需在每一个实现上增加新的接口名称。

常见的接口:

  1. fmt 包中定义的 Stringer
type Stringer interface {
    String() string
}

Stringer 是一个可以用字符串描述自己的类型。fmt包 (还有许多其他包)使用这个来进行输出。

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

Effective Go

https://golang.org/doc/effective_go.html#interfaces

Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。

一个类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 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 + "]"
}

4 - 接口检查

Golang 中的接口检查

Effective Go

接口检查

https://golang.org/doc/effective_go.html#blank_implements

正如我们在上面关于接口的讨论中所看到的,类型不需要明确声明它实现了接口。相反,类型只要实现了接口的方法就实现了接口。在实践中,大多数接口转换都是静态的,因此在编译时进行检查。例如,将一个 *os.File 传给一个期望有 io.Reader 的函数,除非 *os.File 实现了 io.Reader 接口,否则不会被编译。

但有些接口检查确实是在运行时进行的。其中一个例子是在 encoding/json 包中,它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现该接口的值时,编码器会调用该值的 marshaling 方法将其转换为JSON,而不是进行标准转换。编码器在运行时用一个类型断言(type assertion)检查这个属性,比如。

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.RawMessag–需要自定义的JSON表示,它应该实现 json.Marshaler,但没有静态转换会导致编译器自动验证这一点。如果该类型无意中没有满足接口,JSON编码器仍然会工作,但不会使用自定义的实现。为了保证实现的正确性,可以在包中使用空白标识符的全局声明。

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

在这个声明中,涉及到将RawMessage转换为Marshaler的赋值,需要RawMessage实现Marshaler,并且在编译时将检查该属性。如果json.Marshaler接口发生变化,这个包将不再编译,我们会被通知需要更新。

在这个构造中出现空白标识符,说明声明的存在只是为了类型检查,而不是为了创建一个变量。不过不要对每个满足接口的类型都这样做。按照惯例,这种声明只有在代码中已经没有静态转换时才会使用,这是很罕见的事件。

5 - type switch语句

Golang 中的type switch语句

type switch语句的语法详情见:switch 语句

Effective Go

转换

https://golang.org/doc/effective_go.html#conversions

Sequence的String方法重现了Sprint已经为slices做的工作。(它的复杂度也是O(N²),这很差。) 如果我们在调用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实现多个接口(排序和打印),而是利用一个数据项转换为多种类型(Sequence、sort.IntSlice和[]int)的能力,每种类型都能完成一部分工作。这在实践中比较少见,但可以很有效。

接口转换和类型断言

https://golang.org/doc/effective_go.html#interface_conversions

type switch 是一种转换形式:它们接受一个接口,并在某种意义上,对于switch中的每一个case,将其转换为该case的类型。下面是fmt.Printf下的代码如何使用类型转换将一个值变成一个字符串的简化版本。如果它已经是一个字符串,我们要的是接口所持有的实际字符串值,而如果它有一个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()
}

第一个case是找到一个具体的值;第二个case是将接口转换成另一个接口。这样混合类型是完全可以的。

如果我们只关心一种类型呢?如果我们知道这个值持有一个字符串,而我们只想提取它?一个one-case类型switch就可以了,但类型断言(type assertion)也可以。类型断言接受一个接口值,并从中提取一个指定显式类型的值。语法借鉴了打开类型转换的子句,但用的是显式类型而不是类型关键字。

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仍将存在,并且类型为string,但它的值为零,是一个空字符串。

为了说明这种能力,这里有一个if-else语句,相当于本节开头的类型切换。

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

6 - 接口嵌入

Golang 中的接口嵌入

Effective Go

https://golang.org/doc/effective_go.html#embedding

Go并没有提供典型的、类型驱动的子类概念,但它确实可以通过在结构体或接口中嵌入类型来 “借用"实现的一部分。

接口嵌入非常简单。我们之前已经提到了 io.Reader 和 io.Writer 接口,下面是它们的定义。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

io包还导出了其他一些接口,这些接口指定了可以实现若干这样方法的对象。例如,有io.ReadWriter,一个包含Read和Write的接口。我们可以通过显式列出这两个方法来指定io.ReadWriter,但像这样把这两个接口嵌入形成新的接口,会更容易,也更有感召力。

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

如代码所示: ReadWriter可以做Reader做的事情,也可以做Writer做的事情;它是一个嵌入接口(必须是不相干的方法集)的联合体。只有接口可以嵌入到接口中。

同样的基本思想也适用于结构体,但其影响更为深远。bufio包有两个结构类型,bufio.Reader和bufio.Writer,当然每个结构都实现了包io中的类似接口。而且bufio还实现了一个缓冲的读/写器,它是通过使用嵌入的方式将一个读器和一个写器合并到一个结构中来实现的:它列出了结构中的类型,但没有给它们起字段名:

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

嵌入的元素是指向结构体的指针,当然在使用之前必须初始化为指向有效的结构。ReadWriter结构可以写成:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

但这样一来,为了提升字段的方法以满足io接口,我们还需要提供转发方法,比如这样:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

通过直接嵌入结构,我们避免了这种方式。嵌入类型的方法直接附加,这意味着bufio.ReadWriter不仅拥有bufio.Reader和bufio.Writer的方法,还满足了所有三个接口:io.Reader、io.Writer和io.ReadWriter。

嵌入和子类有一个重要的区别。当我们嵌入一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当调用一个bufio.ReadWriter的Read方法时,它的效果和上面写出来的转发方法完全一样,接收者是ReadWriter的reader字段,而不是ReadWriter本身。

嵌入也可以是一种简单的方便。这个例子显示了一个嵌入字段与一个常规的、命名的字段并列。

type Job struct {
    Command string
    *log.Logger
}

现在,Job类型有了*log.Logger的Print、Printf、Println等方法。当然,我们可以给Logger取一个字段名,但没有必要这么做。而现在,一旦初始化,我们就可以将日志记录到Job中。

job.Println("starting now...")

Logger是Job结构的一个常规字段,所以我们可以在Job的构造函数中以通常的方式初始化它,像这样:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

或用组合字面量:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

如果我们需要直接引用一个嵌入的字段,那么字段的类型名,忽略包的限定符,作为字段名,就像在我们的ReadWriter结构的Read方法中一样。在这里,如果我们需要访问一个Job变量job的*log.Logger,我们会写job.Logger,如果我们想完善Logger的方法,这将是非常有用的。

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

嵌入类型引入了名称冲突的问题,但解决这些问题的规则很简单。首先,一个字段或方法X将任何其他项目X隐藏在类型的更深嵌套部分。如果log.Logger包含一个名为Command的字段或方法,那么Job的Command字段就会支配它。

其次,如果相同的名称出现在相同的嵌套层次,通常是一个错误;如果Job结构包含另一个名为Logger的字段或方法,那么嵌入log.Logger将是错误的。但是,如果重复的名称在程序中从未在类型定义之外提及,则是可以的。这个限定提供了一些保护,防止从外部对嵌入的类型进行修改;如果添加的字段与另一个子类型中的另一个字段发生冲突,如果两个字段都没有使用过,那么就没有问题。