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