為什麼 Go 不支援 []T 轉換為 []interface

alwaysbeta發表於2023-01-30

在 Go 中,如果 interface{} 作為函式引數的話,是可以傳任意引數的,然後透過型別斷言來轉換。

舉個例子:

package main

import "fmt"

func foo(v interface{}) {
    if v1, ok1 := v.(string); ok1 {
        fmt.Println(v1)
    } else if v2, ok2 := v.(int); ok2 {
        fmt.Println(v2)
    }
}

func main() {
    foo(233)
    foo("666")
}

不管是傳 int 還是 string,最終都能輸出正確結果。

那麼,既然是這樣的話,我就有一個疑問了,拿出我舉一反三的能力。是否可以將 []T 轉換為 []interface 呢?

比如下面這段程式碼:

func foo([]interface{}) { /* do something */ }

func main() {
    var a []string = []string{"hello", "world"}
    foo(a)
}

很遺憾,這段程式碼是不能編譯透過的,如果想直接透過 b := []interface{}(a) 的方式來轉換,還是會報錯:

cannot use a (type []string) as type []interface {} in function argument

正確的轉換方式需要這樣寫:

b := make([]interface{}, len(a), len(a))
for i := range a {
    b[i] = a[i]
}

本來一行程式碼就能搞定的事情,卻非要讓人寫四行,是不是感覺很麻煩?那為什麼 Go 不支援呢?我們接著往下看。

官方解釋

這個問題在官方 Wiki 中是有回答的,我複製出來放在下面:

The first is that a variable with type []interface{} is not an interface! It is a slice whose element type happens to be interface{}. But even given this, one might say that the meaning is clear.
Well, is it? A variable with type []interface{} has a specific memory layout, known at compile time.
Each interface{} takes up two words (one word for the type of what is contained, the other word for either the contained data or a pointer to it). As a consequence, a slice with length N and with type []interface{} is backed by a chunk of data that is N*2 words long.
This is different than the chunk of data backing a slice with type []MyType and the same length. Its chunk of data will be N*sizeof(MyType) words long.
The result is that you cannot quickly assign something of type []MyType to something of type []interface{}; the data behind them just look different.

大概意思就是說,主要有兩方面原因:

  1. []interface{} 型別並不是 interface,它是一個切片,只不過碰巧它的元素是 interface
  2. []interface{} 是有特殊記憶體佈局的,跟 interface 不一樣。

下面就來詳細說說,是怎麼個不一樣。

記憶體佈局

首先來看看 slice 在記憶體中是如何儲存的。在原始碼中,它是這樣定義的:

// src/runtime/slice.go

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array 是指向底層陣列的指標;
  • len 是切片的長度;
  • cap 是切片的容量,也就是 array 陣列的大小。

舉個例子,建立如下一個切片:

is := []int64{0x55, 0x22, 0xab, 0x9}

那麼它的佈局如下圖所示:

假設程式執行在 64 位的機器上,那麼每個「正方形」所佔空間是 8 bytes。上圖中的 ptr 所指向的底層陣列佔用空間就是 4 個「正方形」,也就是 32 bytes。

接下來再看看 []interface{} 在記憶體中是什麼樣的。

回答這個問題之前先看一下 interface{} 的結構,Go 中的介面型別分成兩類:

  1. iface 表示包含方法的介面;
  2. eface 表示不包含方法的空介面。

原始碼中的定義分別如下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

具體細節我們不去深究,但可以明確的是,每個 interface{} 包含兩個指標, 會佔據兩個「正方形」。第一個指標指向 itab 或者 _type;第二個指標指向實際的資料。

所以它在記憶體中的佈局如下圖所示:

因此,不能直接將 []int64 直接傳給 []interface{}

程式執行中的記憶體佈局

接下來換一個更形象的方式,從程式實際執行過程中,看看記憶體的分佈是怎麼樣的?

看下面這樣一段程式碼:

package main

var sum int64

func addUpDirect(s []int64) {
    for i := 0; i < len(s); i++ {
        sum += s[i]
    }
}

func addUpViaInterface(s []interface{}) {
    for i := 0; i < len(s); i++ {
        sum += s[i].(int64)
    }
}

func main() {
    is := []int64{0x55, 0x22, 0xab, 0x9}

    addUpDirect(is)

    iis := make([]interface{}, len(is))
    for i := 0; i < len(is); i++ {
        iis[i] = is[i]
    }

    addUpViaInterface(iis)
}

我們使用 Delve 來進行除錯,可以點選這裡進行安裝。

dlv debug slice-layout.go
Type 'help' for list of commands.
(dlv) break slice-layout.go:27
Breakpoint 1 set at 0x105a3fe for main.main() ./slice-layout.go:27
(dlv) c
> main.main() ./slice-layout.go:27 (hits goroutine(1):1 total:1) (PC: 0x105a3fe)
    22:        iis := make([]interface{}, len(is))
    23:        for i := 0; i < len(is); i++ {
    24:            iis[i] = is[i]
    25:        }
    26:
=>  27:        addUpViaInterface(iis)
    28:    }

列印 is 的地址:

(dlv) p &is
(*[]int64)(0xc00003a740)

接下來看看 slice 在記憶體中都包含了哪些內容:

(dlv) x -fmt hex -len 32 0xc00003a740
0xc00003a740:   0x10   0xa7   0x03   0x00   0xc0   0x00   0x00   0x00
0xc00003a748:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a750:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x00

每行有 8 個位元組,也就是上文說的一個「正方形」。第一行是指向資料的地址;第二行是 4,表示切片長度;第三行也是 4,表示切片容量。

再來看看指向的資料到底是怎麼存的:

(dlv) x -fmt hex -len 32 0xc00003a710
0xc00003a710:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a718:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a720:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a728:   0x09   0x00   0x00   0x00   0x00   0x00   0x00   0x00

這就是一片連續的儲存空間,儲存著實際資料。

接下來用同樣的方式,再來看看 iis 的記憶體佈局。

(dlv) p &iis
(*[]interface {})(0xc00003a758)
(dlv) x -fmt hex -len 32 0xc00003a758
0xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x00
0xc00003a760:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a768:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a770:   0xd0   0xa7   0x03   0x00   0xc0   0x00   0x00   0x00

切片的佈局和 is 是一樣的,主要的不同是所指向的資料:

(dlv) x -fmt hex -len 64 0xc000090000
0xc000090000:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090008:   0xa8   0xee   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090010:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090018:   0x10   0xed   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090020:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090028:   0x58   0xf1   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090030:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090038:   0x48   0xec   0x0a   0x01   0x00   0x00   0x00   0x00

仔細觀察上面的資料,偶數行內容都是相同的,這個是 interface{}itab 地址。奇數行內容是不同的,指向實際的資料。

列印地址內容:

(dlv) x -fmt hex -len 8 0x010aeea8
0x10aeea8:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010aed10
0x10aed10:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010af158
0x10af158:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010aec48
0x10aec48:   0x09   0x00   0x00   0x00   0x00   0x00   0x00   0x00

很明顯,透過列印程式執行中的狀態,和我們的理論分析是一致的。

通用方法

透過以上分析,我們知道了不能轉換的原因,那有沒有一個通用方法呢?因為我實在是不想每次多寫那幾行程式碼。

也是有的,用反射 reflect,但是缺點也很明顯,效率會差一些,不建議使用。

func InterfaceSlice(slice interface{}) []interface{} {
    s := reflect.ValueOf(slice)
    if s.Kind() != reflect.Slice {
        panic("InterfaceSlice() given a non-slice type")
    }

    // Keep the distinction between nil and empty slice input
    if s.IsNil() {
        return nil
    }

    ret := make([]interface{}, s.Len())

    for i := 0; i < s.Len(); i++ {
        ret[i] = s.Index(i).Interface()
    }

    return ret
}

還有其他方式嗎?答案就是 Go 1.18 支援的泛型,這裡就不過多介紹了,大家有興趣的話可以繼續研究。

以上就是本文的全部內容,如果覺得還不錯的話歡迎點贊轉發關注,感謝支援。

微信搜尋「AlwaysBeta」,第一時間獲取文章更新。


參考文章:

推薦閱讀:

相關文章