Go?不支持?[]T轉(zhuǎn)換為[]interface類型詳解
正文
在 Go 中,如果 interface{}
作為函數(shù)參數(shù)的話,是可以傳任意參數(shù)的,然后通過類型斷言來轉(zhuǎn)換。
舉個例子:
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
,最終都能輸出正確結(jié)果。
那么,既然是這樣的話,我就有一個疑問了,拿出我舉一反三的能力。是否可以將 []T
轉(zhuǎn)換為 []interface
呢?
比如下面這段代碼:
func foo([]interface{}) { /* do something */ } func main() { var a []string = []string{"hello", "world"} foo(a) }
很遺憾,這段代碼是不能編譯通過的,如果想直接通過 b := []interface{}(a)
的方式來轉(zhuǎn)換,還是會報錯:
cannot use a (type []string) as type []interface {} in function argument
正確的轉(zhuǎn)換方式需要這樣寫:
b := make([]interface{}, len(a), len(a)) for i := range a { b[i] = a[i] }
本來一行代碼就能搞定的事情,卻非要讓人寫四行,是不是感覺很麻煩?那為什么 Go 不支持呢?我們接著往下看。
官方解釋
這個問題在官方 Wiki 中是有回答的,我復(fù)制出來放在下面:
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.
大概意思就是說,主要有兩方面原因:
[]interface{}
類型并不是interface
,它是一個切片,只不過碰巧它的元素是interface
;[]interface{}
是有特殊內(nèi)存布局的,跟interface
不一樣。
下面就來詳細(xì)說說,是怎么個不一樣。
內(nèi)存布局
首先來看看 slice 在內(nèi)存中是如何存儲的。在源碼中,它是這樣定義的:
// src/runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }
array
是指向底層數(shù)組的指針;len
是切片的長度;cap
是切片的容量,也就是array
數(shù)組的大小。
舉個例子,創(chuàng)建如下一個切片:
is := []int64{0x55, 0x22, 0xab, 0x9}
那么它的布局如下圖所示:
假設(shè)程序運行在 64 位的機器上,那么每個「正方形」所占空間是 8 bytes。上圖中的 ptr
所指向的底層數(shù)組占用空間就是 4 個「正方形」,也就是 32 bytes。
接下來再看看 []interface{}
在內(nèi)存中是什么樣的。
回答這個問題之前先看一下 interface{}
的結(jié)構(gòu),Go 中的接口類型分成兩類:
iface
表示包含方法的接口;eface
表示不包含方法的空接口。
源碼中的定義分別如下:
type iface struct { tab *itab data unsafe.Pointer }
type eface struct { _type *_type data unsafe.Pointer }
具體細(xì)節(jié)我們不去深究,但可以明確的是,每個 interface{}
包含兩個指針, 會占據(jù)兩個「正方形」。第一個指針指向 itab
或者 _type
;第二個指針指向?qū)嶋H的數(shù)據(jù)。
所以它在內(nèi)存中的布局如下圖所示:
因此,不能直接將 []int64
直接傳給 []interface{}
。
程序運行中的內(nèi)存布局
接下來換一個更形象的方式,從程序?qū)嶋H運行過程中,看看內(nèi)存的分布是怎么樣的?
看下面這樣一段代碼:
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 來進(jìn)行調(diào)試,可以點擊這里進(jìn)行安裝。
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 在內(nèi)存中都包含了哪些內(nèi)容:
(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 個字節(jié),也就是上文說的一個「正方形」。第一行是指向數(shù)據(jù)的地址;第二行是 4,表示切片長度;第三行也是 4,表示切片容量。
再來看看指向的數(shù)據(jù)到底是怎么存的:
(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
這就是一片連續(xù)的存儲空間,保存著實際數(shù)據(jù)。
接下來用同樣的方式,再來看看 iis
的內(nèi)存布局。
(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
是一樣的,主要的不同是所指向的數(shù)據(jù):
(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
仔細(xì)觀察上面的數(shù)據(jù),偶數(shù)行內(nèi)容都是相同的,這個是 interface{}
的 itab
地址。奇數(shù)行內(nèi)容是不同的,指向?qū)嶋H的數(shù)據(jù)。
打印地址內(nèi)容:
(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
很明顯,通過打印程序運行中的狀態(tài),和我們的理論分析是一致的。
通用方法
通過以上分析,我們知道了不能轉(zhuǎn)換的原因,那有沒有一個通用方法呢?因為我實在是不想每次多寫那幾行代碼。
也是有的,用反射 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 支持的泛型,這里就不過多介紹了,大家有興趣的話可以繼續(xù)研究。
參考文章:
以上就是Go 不支持 []T 轉(zhuǎn)換為 []interface詳解的詳細(xì)內(nèi)容,更多關(guān)于Go不支持[]T轉(zhuǎn)換[]interface的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go實現(xiàn)自動解壓縮包以及讀取docx/doc文件內(nèi)容詳解
在開發(fā)過程中,我們常常需要處理壓縮包和文檔文件。本文將介紹如何使用Go語言自動解壓縮包和讀取docx/doc文件,需要的可以參考一下2023-03-03Go實現(xiàn)map轉(zhuǎn)json的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Go語言實現(xiàn)map轉(zhuǎn)json的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09Go語言使用Cobra實現(xiàn)強大命令行應(yīng)用
Cobra是一個強大的開源工具,能夠幫助我們快速構(gòu)建出優(yōu)雅且功能豐富的命令行應(yīng)用,本文為大家介紹了如何使用Cobra打造強大命令行應(yīng)用,感興趣的小伙伴可以了解一下2023-07-07