Go语言的接口与反射

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(反射)在计算机中表示 程序能够检查自身结构的能力,尤其是类型。它是元编程的一种形式,也是最容易让人迷惑的一部分。

猜你喜欢

转载自www.cnblogs.com/laolieren/p/an_easy_entry_to_golang_interface_and_reflect.html