Go语言的接口与反射
go总体而言是一门比较好入门的语言,许多特性都很精简易懂,但是接口与反射除外。他们真的让人头疼,不知道是自身资质问题还是怎么着,总是觉得很多书上写的不够精简明了。。而我,亚楠老猎人,今天就是要受苦试着把它给攻克了。
接口
你可以用很多词语来形容golang,但“传统”肯定不能用。因为,它里面没有类和继承的概念。
你觉得这简直不可思议,怎么可能这样,那不是意味着海量的重复代码。并没有,Go通过很灵活的一个概念,实现了很多面向对象的行为。没错,这个概念就是“接口”。
我们来看看接口的特性。
接口被隐式实现
类型不需要显式声明它实现了某个接口,接口是被隐式地实现的。
什么意思?就是说只要你把接口声明的方法都实现了,那么就认为你实现了这个接口了。无需像其他语言那样在显眼的地方表明 implements 接口名称
,比如php中你可能需要这样子:
<?php
interface Cinema
{
public function show(Order $show,$num);
}
// 显示正常
class Order implements Cinema
{
public $number='0011排';
public function show(Order $show,$num)
{
echo $show->number.$num;
}
}
$face= new Order();
$face->show(new Order,$num='3人');//输出 0011排3人
而在golang中,你只需要这个样子:
// 一个简单的求正方形面积的例子
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 输出形状面积
func PrintArea(shape Shape) {
fmt.Printf("The square has area: %f\n", shape.Area())
}
// 正方形结构体
type Square struct {
side float32
}
// 正方形面积
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func main() {
square := new(Square)
square.side = 5
PrintArea(square)
}
上面的程序定义了一个结构体 Square 和一个接口 Shape,接口有一个方法 Area(),而Square实现了这个方法,虽然没有显示声明。
这时你发现,PrintArea
这个函数居然可以直接接受了Square类型的参数,尽管函数定义里,参数是Shape接口类型的。
也就是说,golang认为你已经用Square结构体实现了Shape接口。
如果,我们对代码稍作修改,给接口定义中增加周长(Perimeter)方法
// 形状接口
type Shape interface {
Area() float32
Perimeter() float32
}
其他不作改动,你就会发现编译器报错了
cannot use square (type *Square) as type Shape in argument to DescArea:
*Square does not implement Shape (missing Perimeter method)
报错信息说的很明了,Shape还有个方法Perimeter,但是Square却未实现它。虽然还没有人去调用这个方法,但是编译器也会提前给出错误。
下面我们准备开始了解继承与多态,在开始之前,我们记住这句话
一个接口可以由多种类型实现,一种类型也可以实现多个接口。
接口实现继承
虽然Go语言没有继承的概念,但为了便于理解,如果一个struct A 实现了 interface B的所有方法时,我们称之为“继承”。
一个接口可以包含一个或者多个其他的接口,这相当于直接把这些内嵌接口的方法列举在外层接口中一样。
比如,还是那个Shape的例子,我们这次增加一个要素,颜色,来生成多彩的正方形。
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 颜色接口
type Color interface {
Colors() []string
}
// 多彩的形状接口
type ColorfulShape interface {
Shape
Color
Name()
}
比如上面的例子,最后的ColorfulShape
就包含了Shape和Color接口,此外还有自身特有的Name()方法。
接口实现多态
我们很容易扩展之前的代码,比如你可以联想到正方形的好兄弟,长方形,于是..
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 输出形状面积
func PrintArea(shape Shape) {
fmt.Printf("The square has area: %f\n", shape.Area())
}
// 正方形结构体
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}
q := &Square{5}
shapes := []Shape{r, q}
fmt.Println("Looping through shapes for area ...")
for key, _ := range shapes {
fmt.Println("Shape details: ", shapes[key])
fmt.Println("Area of this shape is: ", shapes[key].Area())
}
}
在main方法的for循环中,虽然只知道shapes[key]是一个Shape对象,但是它却能自动变成Square或者Rectangle对象,还可以调用各自的Area方法。是不是很厉害?
通过上面的例子,我们可以发现:
- 接口其实像一种契约,实现类型必须满足它(实现其定义的方法)。
- 接口描述了类型的行为,规定类型可以做什么。
- 接口彻底将类型能做什么,以及如何做分离开来。
- 这些特点使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
使用接口使代码更具有普适性。
类型断言
前面用接口实现多态时,在最后main方法的for循环里,接口类型变量
shapes[key]中可以包含任何类型的值,那么如何检测当前的对象是什么类型的呢?
答案就是使用类型断言。比如
v := var.(类型名)
这里的var必需得是接口变量,比如shapes[key]。
如果我们直接这么写
v := shapes[key].(*Square)
那肯是会报错的,因为shapes[key]也可能是Rectangle类型的,为了避免错误发生,我们可以使用更安全的方法进行断言:
if v, ok := shapes[key].(*Square); ok {
// 相关操作
}
如果转换合法,v 是 shapes[key] 转换到类型 Square 的值,ok 会是 true;否则 v 是类型 Square 的零值,ok 是 false,也没有运行时错误发生。
备注: 不要忽略
shapes[key].(*Square)
中的*
号,否则会导致编译错误:impossible type assertion: Square does not implement Shape (Area method has pointer receiver)。
方法集与接口
Go 语言规范定义了接口方法集的调用规则:
- 类型 T 的可调用方法集包含接受者为 T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
举例说明
package main
import (
"fmt"
)
type List []int
func (l List) Len() int {
return len(l)
}
func (l *List) Append(val int) {
*l = append(*l, val)
}
type Appender interface {
Append(int)
}
func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}
type Lener interface {
Len() int
}
func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}
func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID:Identical receiver type
fmt.Printf("- lst is long enough\n")
}
// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。
在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。
反射
Reflection(反射)在计算机中表示 程序能够检查自身结构的能力,尤其是类型。它是元编程的一种形式,也是最容易让人迷惑的一部分。