天天看點

Go語言基礎之接口斷言

Go語言基礎之接口斷言

空接口可以存儲任意類型的值,那我們如何擷取其存儲的具體資料呢?

一、接口類型的值

對于像Go語言這種靜态類型的語言,一個變量的類型與具體的值是分開的概念,每個變量都有且隻有一個類型,在編譯時就已經确定不會發生改變,是以Go語言的變量的類型也稱之為靜态類型,比如 int、string、float32、*MyType、[]byte等

type MyInt int
var i int
var j MyInt
           

上面的代碼中,變量 i 的類型是 int,j 的類型是 MyInt。 是以,盡管變量 i 和 j 具有共同的底層類型 int,但它們的靜态類型并不一樣。不經過類型轉換直接互相指派時,編譯器會報錯。

特殊的是,接口類型,通常被人稱之為動态類型,何意

1、首先須知一個接口類型的變量就是專門用于接收具體實作了本接口的變量值的,然後我們展開讨論。在底層,接口類型的變量中存放的内容是一對pair,初始狀态下,接口變量中的pair為(nil,nil)

var w io.Writer // io包内的Writer接口,初始零值/pair是(nil,nil)

// 注意:接口值(一個接口類型的值,簡稱接口值)的類型是type存放的内容,即它所存放的具體變量的類型
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>
           
Go語言基礎之接口斷言

在被指派了一個具體的變量後,pair中會記錄下具體變量的類型和值

w = os.Stdout

// 注意:接口值(一個接口類型的值,簡稱接口值)的類型是type存放的内容,即它所存放的具體變量的類型
fmt.Printf("%T %v\n", w, w)  // *os.File &{0xc0000520c0}
           
Go語言基礎之接口斷言

上述指派過程發生了一個隐式轉換,将具體類型轉換到接口類型,這和顯式的調用接口

io.Writer(os.Stdout)

的轉換是等價的。但這類轉換不管是顯式的還是隐式的,都會将其對應的具體變量的類型和值存入接口type與value中

這個接口值的type被設為

*os.Stdout

指針的類型描述符,它的value持有

os.Stdout

的拷貝(是一個代表處理标準輸出的os.File類型變量的指針)

但凡是實作了io.Writer接口的類型都可以指派給接口變量w,是以我們可以繼續為w指派

w = new(bytes.Buffer)  //給接口值賦了一個*bytes.Buffer類型的值,bytes.Buffer也實作了接口io.Writer

// 注意:接口值(一個接口類型的值,簡稱接口值)的類型是type存放的内容,即它所存放的具體變量的類型
fmt.Printf("%T %v\n", w, w)  // *bytes.Buffer 
           

現在接口值w存放的type是

*bytes.Buffer

并且value是一個指向新配置設定的緩沖區的指針

繼續為w指派

w = nil  // 将nil賦給了接口值

// 注意:接口值(一個接口類型的值,簡稱接口值)的類型是type存放的内容,即它所存放的具體變量的類型
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>
           

此時無論w原來是否存放有内容,這個重置将它所有的部分(動态類型與動态值)都設為nil值,把變量w恢複到和它之前定義時相同的狀态

Go語言基礎之接口斷言

我們把上述讨論所有執行的代碼彙總到一起看一下

var w io.Writer
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>
w = os.Stdout
fmt.Printf("%T %v\n", w, w)  // *os.File &{0xc000052060}
w = new(bytes.Buffer)
fmt.Printf("%T %v\n", w, w)  // *bytes.Buffer
w = nil
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>
           

會得出一個結論,接口值w的類型type是動态的,會随着其被賦予的具體值的類型變化而變化,并且接口存放的value也是動态的,是以才有人會說接口是動态類型,并将接口的pair中的type和value描述為動态類型與動态值

  • 1、動态類型type與動态值value指的是指派給接口的那些具體的變量的類型與值,即便我們把一個被賦予具體值的接口值指派給目前接口變量,目前接口變量中存放的也是對應的具體值的類型與值。
  • 2、接口值(一個接口類型的值,簡稱接口值)的類型是type存放的内容,即它所存放的具體變量的類型
  • 3、type類型部分存放的是與之相關類型的描述符,類型描述符指的是每個類型的詳細資訊,比如類型的名稱和方法。

另外從概念上講,不論接口值多大,動态值總是可以容下它。(這隻是一個概念上的模型;具體的實作可能會非常不同)

d := time.Now()
var x interface{} = d
           
Go語言基礎之接口斷言

二、接口值是否為空的判定依據是type

一個接口類型中的值是基于它的動态類型type被描述為空或非空的,與其動态value無關

var w io.Writer  // 動态類型與動态值分别為:<nil> <nil>
fmt.Println(w == nil)  // 動态類型為nil,是以結果為:true

w = os.Stdout  // 動态類型與動态值分别為:*os.File &{0xc000052060}
fmt.Println(w == nil)  // 動态類型不為nil,是以結果為:false

w = new(bytes.Buffer)  // 動态類型與動态值分别為:*bytes.Buffer
fmt.Println(w == nil)  // 動态類型不為nil,是以結果為:false

w = nil  // 動态類型與動态值分别為:<nil> <nil>
fmt.Println(w == nil)  // 動态類型為nil,是以結果為:true
           

三、nil接口與動态值為nil指針的接口

nil接口指的是動态類型與動态值均為nil的接口值,此時,該接口值與nil相等,如

var w io.Writer  // 動态類型與動态值分别為:<nil> <nil>
fmt.Println(w == nil) // true
           

而一個接口值有可能動态類型不為nil,而動态值為一個指向nil的指針,因為接口值是否為空的判定依據是其動态類型,是以此時該接口值不等于nil

先準備好一個指針類型的變量x和一個接口類型的變量out

// x是一個指針類型,該指針類型指向接口,注意日常開發不要使用指針指向接口類型
var x *bytes.Buffer  // x采用其零值,未開辟記憶體空間
fmt.Printf("%T %v\n", x, x)  // *bytes.Buffer <nil>,變量x是一個空指針,類型為指針

// 因為x不是一個接口類型,對于非接口類型,判定是否與nil相等的依據就是它的值,是以下述結果為true
fmt.Println(x == nil)  // true

// out是一個接口類型,再次強調,針對接口這種類型的值,判斷是否為nil的依據是其存放的動态類型而非動态值
var out io.Writer  
fmt.Printf("%T %v\n", out,out)  // 動态值與動态類型:<nil> <nil>
fmt.Println(out == nil)  // true,因為接口類型out的動态類型為nil
           

然後再把x指派給接口類型out,out存放的動态類型為x的類型即指針類型,而out存放的動态值為nil,如下

Go語言基礎之接口斷言

再次強調,對于接口類型來說,判定其是否為空的依據是其動态類型而非動态值,也就是此時的out,雖然存的動态值為nil,但因為其存放的動态類型不為空,是以此時接口類型的變量out不為空

out=x  // 
fmt.Printf("%T %v\n", out,out)  // *bytes.Buffer <nil>
fmt.Println(out != nil)  // true
           

至此,得出一個重要結論,nil接口與動态值為nil的接口不是一回事

四、 接口值的相等性判斷

接口值可以使用

==

!=

來進行比較。兩個接口值相等僅當它們都是nil值或者它們的動态類型相同并且動态值也相同(前提是動态值是可以比較的)。因為接口值是可比較的,是以它們可以用在map的鍵或者作為switch語句的操作數。

如果兩個接口值的動态類型相同,但是這個動态類型是不可比較的(比如切片),将它們進行比較就會失敗并且panic:

var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
           

五、接口類型斷言

一個接口的值(簡稱接口值)是由

一個具體類型

具體類型的值

兩部分組成的。這兩部分分别稱為接口的

動态類型

動态值

我們來看一個具體的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
           

請看下圖分解:

Go語言基礎之接口斷言

想要判斷空接口中的值這個時候就可以使用類型斷言,其文法格式:

x.(T)
           

其中:

  • x:表示類型為

    interface{}

    的變量
  • T:表示斷言

    x

    可能是的類型。

該文法傳回兩個參數,第一個參數是

x

轉化為

T

類型後的變量,第二個值是一個布爾值,若為

true

則表示斷言成功,為

false

則表示斷言失敗。

舉個例子:

func main() {
    var x interface{}
    x = "Hello word"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("類型斷言失敗")
    }
}
           

上面的示例中如果要斷言多次就需要寫多個

if

判斷,這個時候我們可以使用

switch

語句來實作:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}
           

因為空接口可以存儲任意類型值的特點,是以空接口在Go語言中的使用十分廣泛。

關于接口需要注意的是,隻有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義接口。不要為了接口而寫接口,那樣隻會增加不必要的抽象,導緻不必要的運作時損耗。

七、總結

  1. 接口類型底層存放的是兩個值,一個是類型

    type

    ,一個是值

    value

  2. switch x.(type) {
    case *Square:
        // TODO
    case *Circle:
        // TODO
    ...
    default:
        // TODO
    }
               

在當下的階段,必将由程式員來主導,甚至比以往更甚。