Files
the-way-to-go_ZH_CN/eBook/11.1.md
Sarlor e837adafa7 Update 11.1.md (#466)
去掉多余的右括号
2018-05-18 17:36:17 -04:00

257 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 11.1 接口是什么
Go 语言不是一种 *“传统”* 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 **接口** 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 **说明** 对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:
```go
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
```
上面的 `Namer` 是一个 **接口类型**
(按照约定,只包含一个方法的)接口的名字由方法名加 `[e]r` 后缀组成,例如 `Printer``Reader``Writer``Logger``Converter` 等等。还有一些不常用的方式(当后缀 `er` 不合适时),比如 `Recoverable`,此时接口名以 `able` 结尾,或者以 `I` 开头(像 `.NET``Java` 中那样)。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 **接口值** `var ai Namer``ai` 是一个多字multiword数据结构它的值是 `nil`。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
![](images/11.1_fig11.1.jpg?raw=true)
此处的方法指针表是通过运行时反射能力构建的。
类型(比如结构体)实现接口方法集中的方法,每一个方法的实现说明了此方法是如何作用于该类型的:**即实现接口**,同时方法集也构成了该类型的接口。实现了 `Namer` 接口类型的变量可以赋值给 `ai` (接收者值),此时方法表中的指针会指向被实现的接口方法。当然如果另一个类型(也实现了该接口)的变量被赋值给 `ai`,这二者(译者注:指针和方法实现)也会随之改变。
**类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口**
**实现某个接口的类型(除了实现接口方法外)可以有其他的方法**
**一个类型可以实现多个接口**
**接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)**
即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。
所有这些特性使得接口具有很大的灵活性。
第一个例子:
示例 11.1 [interfaces.go](examples/chapter_11/interfaces.go)
```go
package main
import "fmt"
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func main() {
sq1 := new(Square)
sq1.side = 5
var areaIntf Shaper
areaIntf = sq1
// shorter,without separate declaration:
// areaIntf := Shaper(sq1)
// or even:
// areaIntf := sq1
fmt.Printf("The square has area: %f\n", areaIntf.Area())
}
```
输出:
The square has area: 25.000000
上面的程序定义了一个结构体 `Square` 和一个接口 `Shaper`,接口有一个方法 `Area()`
`main()` 方法中创建了一个 `Square` 的实例。在主程序外边定义了一个接收者类型是 `Square` 方法的 `Area()`,用来计算正方形的面积:结构体 `Square` 实现了接口 `Shaper`
所以可以将一个 `Square` 类型的变量赋值给一个接口类型的变量:`areaIntf = sq1`
现在接口变量包含一个指向 `Square` 变量的引用,通过它可以调用 `Square` 上的方法 `Area()`。当然也可以直接在 `Square` 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。
这是 **多态** 的 Go 版本,多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。
如果 `Square` 没有实现 `Area()` 方法,编译器将会给出清晰的错误信息:
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)
如果 `Shaper` 有另外一个方法 `Perimeter()`,但是`Square` 没有实现它,即使没有人在 `Square` 实例上调用这个方法,编译器也会给出上面同样的错误。
扩展一下上面的例子,类型 `Rectangle` 也实现了 `Shaper` 接口。接着创建一个 `Shaper` 类型的数组,迭代它的每一个元素并在上面调用 `Area()` 方法,以此来展示多态行为:
示例 11.2 [interfaces_poly.go](examples/chapter_11/interfaces_poly.go)
```go
package main
import "fmt"
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3} // Area() of Rectangle needs a value
q := &Square{5} // Area() of Square needs a pointer
// shapes := []Shaper{Shaper(r), Shaper(q)}
// or shorter
shapes := []Shaper{r, q}
fmt.Println("Looping through shapes for area ...")
for n, _ := range shapes {
fmt.Println("Shape details: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area())
}
}
```
输出:
Looping through shapes for area ...
Shape details: {5 3}
Area of this shape is: 15
Shape details: &{5}
Area of this shape is: 25
在调用 `shapes[n].Area() ` 这个时,只知道 `shapes[n]` 是一个 `Shaper` 对象,最后它摇身一变成为了一个 `Square``Rectangle` 对象,并且表现出了相对应的行为。
也许从现在开始你将看到通过接口如何产生 **更干净**、**更简单** 及 **更具有扩展性** 的代码。在 11.12.3 中将看到在开发中为类型添加新的接口是多么的容易。
下面是一个更具体的例子:有两个类型 `stockPosition``car`,它们都有一个 `getValue()` 方法,我们可以定义一个具有此方法的接口 `valuable`。接着定义一个使用 `valuable` 类型作为参数的函数 `showValue()`,所有实现了 `valuable` 接口的类型都可以用这个函数。
示例 11.3 [valuable.go](examples/chapter_11/valuable.go)
```go
package main
import "fmt"
type stockPosition struct {
ticker string
sharePrice float32
count float32
}
/* method to determine the value of a stock position */
func (s stockPosition) getValue() float32 {
return s.sharePrice * s.count
}
type car struct {
make string
model string
price float32
}
/* method to determine the value of a car */
func (c car) getValue() float32 {
return c.price
}
/* contract that defines different things that have value */
type valuable interface {
getValue() float32
}
func showValue(asset valuable) {
fmt.Printf("Value of the asset is %f\n", asset.getValue())
}
func main() {
var o valuable = stockPosition{"GOOG", 577.20, 4}
showValue(o)
o = car{"BMW", "M3", 66500}
showValue(o)
}
```
输出:
Value of the asset is 2308.800049
Value of the asset is 66500.000000
**一个标准库的例子**
`io` 包里有一个接口类型 `Reader`:
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
```
定义变量 `r`` var r io.Reader`
那么就可以写如下的代码:
```go
var r io.Reader
r = os.Stdin // see 12.1
r = bufio.NewReader(r)
r = new(bytes.Buffer)
f,_ := os.Open("test.txt")
r = bufio.NewReader(f)
```
上面 `r` 右边的类型都实现了 `Read()` 方法,并且有相同的方法签名,`r` 的静态类型是 `io.Reader`
**备注**
有的时候,也会以一种稍微不同的方式来使用接口这个词:从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外定一个接口而已。
**练习 11.1** simple_interface.go
定义一个接口 `Simpler`,它有一个 `Get()` 方法和一个 `Set()``Get()`返回一个整型值,`Set()` 有一个整型参数。创建一个结构体类型 `Simple` 实现这个接口。
接着定一个函数,它有一个 `Simpler` 类型的参数,调用参数的 `Get()``Set()` 方法。在 `main` 函数里调用这个函数,看看它是否可以正确运行。
**练习 11.2** interfaces_poly2.go
a) 扩展 interfaces_poly.go 中的例子,添加一个 `Circle` 类型
b) 使用一个抽象类型 `Shape`(没有字段) 实现同样的功能,它实现接口 `Shaper`,然后在其他类型里内嵌此类型。扩展 10.6.5 中的例子来说明覆写。
## 链接
- [目录](directory.md)
- 上一节:[接口Interfaces与反射reflection](11.0.md)
- 下一节:[接口嵌套接口](11.2.md)