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(反射)在計算機中表示 程序能夠檢查自身結構的能力,尤其是類型。它是元編程的一種形式,也是最容易讓人迷惑的一部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章