1. 程式人生 > >Go語言的介面與反射

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