Golang | 既是介面又是型別,interface是什麼神仙用法?

TechFlow2019發表於2020-08-11

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是golang專題的第12篇文章,我們來繼續聊聊interface的使用。

在上一篇文章當中我們介紹了物件導向的一些基本概念,以及golang當中interface和多型的實現方法。今天我們繼續來介紹interface當中其他的一些方法。

萬能型別interface

在Java以及其他語言當中介面是一種寫法規範,而在golang當中,interface其實也是一種值,它可以像是值一樣傳遞。並且在它的底層,它其實是一個值和型別的元組。

這裡我們來看下golang官方文件當中的一個例子:

package main

import (
 "fmt"
 "math"
)

type I interface {
 M()
}

type T struct {
 S string
}

func (t *T) M() {
 fmt.Println(t.S)
}

type F float64

func (f F) M() {
 fmt.Println(f)
}

func main() {
 var i I

 i = &T{"Hello"}
 describe(i)
 i.M()

 i = F(math.Pi)
 describe(i)
 i.M()
}

func describe(i I) {
 fmt.Printf("(%v, %T)\n", i, i)
}

在上面的程式碼當中定義了一個叫做describe的方法,在這個方法當中我們輸出了兩個值,一個是介面i對應的值,另一個是介面i的型別

我們輸出的結果如下:

image-20200724084346988
image-20200724084346988

可以看到介面當中既儲存了對應的結構體的例項的資訊,也儲存了結構體的型別。因此interface可以理解成一種特殊的型別。

實際上也的確如此,我們可以把interface理解成一種萬能資料型別,它可以接收任何型別的值。我們看下下面這種用法:

var a1 interface{} = 1
var a2 interface{} = "abc"
list := make([]interface{}, 0)
list = append(list, a1)
list = append(list, a2)
fmt.Println(list)

在程式碼當中我們建立了一個interface{}型別的slice,它可以接收任何型別的值和例項。另外我們用interface{}這個型別也可以接收任何結構體的值。這裡可能會有些迷惑,其實很容易想明白。interface表示一種型別,可以接收任何實現了interface當中規定的方法的型別的值。當我們定義inteface{}的時候,其實是定義了空的interface,相當於不需要實現任何方法的空interface,所以任何型別都可以接收,這也就是它成為萬能型別的原因。

我們接收當然沒有問題,問題是我們怎麼使用這些interface型別的值呢?

一種方法是我們可以判斷一個interface的變數型別。判斷的方法非常簡單,我們在interface的變數後面用.(type)的方法來判斷。它和map的key值判斷一樣,會返回一個值和bool型別的標記。我們可以通過這個標記判斷這個型別是否正確。

if v, ok := a1.(int); ok {
    fmt.Println(v)
}

如果型別比較多的話使用switch也是可以的:

switch v := i.(type) {
case int:
    fmt.Println("int")
case string:
    fmt.Println("string")
}

空值nil

interface型別的空值是nil,和Python當中的None是一個意思,表示一個指標指向空。如果我們在Java或者是其他語言當中對一個空指標呼叫方法,那麼會觸發NullPointerMethodError,也就是空指標報錯。這也是我們初學者在程式設計當中最容易遇到的錯誤,往往原因是忘記了對宣告進行初始化導致的。

但是在golang當中不會,即使是nil也可以呼叫interface的方法。舉個例子:

type T struct {
 S string
}

func (t *T) M() {
 fmt.Println(t.S)
}

func main() {
 var i I
 var t *T
 i = t
 i.M()
}

我們將t賦值給了i,問題是t並沒有進行初始化,所以它是一個nil,那麼我們的i也就會是一個nil。我們對nil呼叫M方法,在M方法當中我們列印了t的區域性變數S。由於t此刻是一個nil,它並沒有這個變數,所以會引發一個invalid memory address or nil pointer derefernce的錯誤,也就是對空指標進行定址的錯誤。

要解決這個錯誤,其實很簡單,我們可以在M方法當中對t進行判斷,如果發現t是一個nil,那麼我們則跳過執行的邏輯。當我們把M函式改成這樣之後,就不會觸發空指標的問題了。

func (t *T) M() {
    if t == nil {
        fmt.Println("nil")
        return
    }
 fmt.Println(t.S)
}

nil觸發異常的問題也是初學者經常遇到的問題之一,這也要求我們在實現結構體內方法的時候一定要記得判斷呼叫的物件是否為nil,避免不必要的問題。

賦值的型別選擇

我們都知道golang當中通過interface來實現多型,只要是實現了interface當中定義的函式,那麼我們就可以將對應的例項賦值給這個interface型別。

這看起來沒有問題,但是在實際執行的時候仍然會有一點點小小的問題。比如說我們有這樣一段程式碼:

type Integer int

type Operation interface {
 Less(b Integer) bool
 Add(b Integer)
}


func (a Integer) Less(b Integer) bool {
 return a < b
}

func (a *Integer) Add(b Integer) {
 *a += b
}

這段程式碼非常簡單,我們定義了一個Operation的interface,並且實現了Integer型別的兩個方法。表面上看一切正常,但是有一個細節。Less和Add這兩個方法針對的型別是不同的,Less方法我們不需要修改原值,所以我們傳入的是Integer的值,而Add方法,我們需要修改原值, 所以我們傳入的型別是Integer的指標。

那麼問題來了,這兩個方法的型別不同, 我們還可以將它的值賦值給Operation這個interface嗎?如果可以的話,我們應該傳遞的是值還是指標呢?下面程式碼當中的第二行和第三行究竟哪個是正確的呢?

var a Integer = 1
var b Operation = &a
var b Operation = a

答案是第二行的是正確的,原因也很簡單,因為我們傳入指標之後,golang的編譯器會自動生成一個新的Less方法。在這個轉換了型別的方法當中去呼叫了原本的方法,相當於做了一層中轉。

func (a *Integer) Less(b Integer) bool{
    return (*a).Less(b)
}

那反過來行不行呢?我們也寫出程式碼:

func (a Integer) Add (b Integer) {
    (&a).Add(b)
}

顯然這樣是不行的,因為函式執行之後修改的只能是Add這個方法當中a這個引數的值,而沒辦法修改原值。這和我們想要的不符合,所以golang沒有選擇這種策略。

總結

在今天的文章當中我們介紹了golang當中interface的一些高階用法,比如將它作為萬能型別來接收各種格式的值。比如interface的空指標呼叫問題,以及interface中的兩個函式接收型別不一致的問題。

也就是說在go語言當中,interface既是一種多型實現的規範,又有全能型別這樣衍生的功能,這個設計的確是很驚豔的。對interface的熟練使用可以在一些問題當中大大降低我們編碼的複雜度,以及執行的效率。這也是golang的原生優勢之一。

相關閱讀

物件導向回顧,golang中多型的實現方法

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

- END -

相關文章