答讀者問(wèn):為什么 Go 里面不能把任意切片轉(zhuǎn)換為 []interface{} ?
本文來(lái)源于一個(gè)朋友的提問(wèn)。
數(shù)組怎么樣展開(kāi)?
問(wèn)題描述比較模糊,進(jìn)一步溝通之后得知,他需要的是將一個(gè)數(shù)組(其實(shí)是切片)展開(kāi),來(lái)作為函數(shù)的可變參數(shù)。
可變參數(shù)
關(guān)于可變參數(shù),之前在這里(函數(shù)簽名部分)介紹過(guò)??紤]到那篇文章內(nèi)容比較多,這里再介紹一下。
簡(jiǎn)單來(lái)說(shuō),可變參數(shù)就是函數(shù)里以 x ...T 這種形式聲明的參數(shù)。舉例說(shuō) f(s ...int),參數(shù) s 接受零到多個(gè) int 型的參數(shù),可以像這樣調(diào)用 f(a, b, c) (a,b,c 都是 int 型的值)。逐個(gè)傳入的參數(shù)(實(shí)參)會(huì)裝包成一個(gè)切片 s,傳遞給函數(shù)。

從函數(shù)內(nèi)部的角度,這跟 f(s []int) 是等價(jià)的。而從調(diào)用方的角度看則有差別:可變參數(shù)接受多個(gè) int 型參數(shù),而后者只能接受一個(gè) []int 類(lèi)型參數(shù)。
如果有多個(gè)同類(lèi)型參數(shù),遇到第二種函數(shù)定義(參數(shù)類(lèi)型是切片),就只能自己先創(chuàng)建一個(gè)切片,再直接傳遞切片。不過(guò)相信你也明白了,可變參數(shù)不過(guò)是把創(chuàng)建切片過(guò)程省略的語(yǔ)法糖:
//?函數(shù)簽名:f(s []int)
a?:=?[]int{x,?y,?z}
f(a)
反過(guò)來(lái),有一個(gè) []int 變量 b ,需要傳遞給可變參數(shù)怎么辦?難道要 f(b[0], b[1], b[2]) 這樣一個(gè)個(gè)用下標(biāo)訪(fǎng)問(wèn)?如果切片很長(zhǎng),又或者直接不確定長(zhǎng)度怎么辦?
在其它語(yǔ)言,例如 Python 里,對(duì)于可迭代類(lèi)型對(duì)象(Iterator Types),可以用裝包和拆包(解包)解決這個(gè)問(wèn)題,使用上非常靈活。
Go (看起來(lái))也可以解包:
//?函數(shù)簽名:f(s ...int)
f(b...)
注意 ... 的位置,聲明時(shí)在前,調(diào)用時(shí)在后。
但,這是一個(gè)假的解包。這只是又一個(gè)語(yǔ)法糖,背后把 b 直接賦值給 s 。把 b 拆分成逐個(gè)參數(shù)傳遞,然后重新打包成切片 s 這件事,根本沒(méi)有發(fā)生。
你以為的解包:
(圖中的細(xì)箭頭表示指針,粗箭頭表示拷貝)

或者至少是這樣的:

其實(shí)是這樣的:

切片是引用類(lèi)型,變量本身保存的是頭信息(元數(shù)據(jù)),里面有一個(gè)指向底層數(shù)組的指針,元素?cái)?shù)據(jù)保存在數(shù)組里。在賦值和傳參時(shí),拷貝的只是切片頭(slice header),底層數(shù)組并不會(huì)遞歸拷貝。新舊切片共享同一個(gè)底層數(shù)組。
... 只是表示 b 是一組參數(shù),而不是一個(gè)參數(shù)。如果缺少 ... ,直接 f(b) ,會(huì)把 b 直接當(dāng)成一個(gè)參數(shù)(也就是 s 切片的一個(gè)元素),參數(shù)的 []int 類(lèi)型和元素的 int 不匹配。
好消息是,沒(méi)有額外開(kāi)銷(xiāo)。壞消息是,因此使用上多了很多限制。
b必須是相同類(lèi)型的切片。[]string傳遞給[]int固然不行;因?yàn)闆](méi)有經(jīng)過(guò)解包后重新裝包,數(shù)組傳遞給切片也不行。...(姑且還是叫解包)不能跟其它參數(shù)或者其它解包參數(shù)一起使用。f(x, b...)或者f(b..., c...)都會(huì)報(bào)錯(cuò)。因?yàn)闆](méi)有經(jīng)過(guò)解包后重新裝包,元素x和切片b,或者b和c兩個(gè)切片,都不會(huì)組成一個(gè)新切片。修改 s的元素,會(huì)影響到b。(因?yàn)樗鼈児蚕硪粋€(gè)底層數(shù)組)
類(lèi)型轉(zhuǎn)換
由于沒(méi)有看到具體代碼,根據(jù)對(duì)方的描述,猜測(cè)問(wèn)題出在沒(méi)有理解『偽解包』上。所以我對(duì)這部分進(jìn)行了解釋。
然而問(wèn)題并沒(méi)有解決,第二天提問(wèn)者又來(lái)了。
這次提問(wèn)者給了更詳細(xì)的信息。
他需要調(diào)用 gorm 包的 Having 方法,方法簽名是:
func?(s?*DB)?Having(query?interface{},?values?...interface{})?*DB
看起來(lái)跟我的猜測(cè)差不多。還有什么該注意的我忘了說(shuō)?
我正想要代碼和具體的報(bào)錯(cuò)信息,對(duì)方說(shuō)了一句:
為什么 []string 不能轉(zhuǎn)為 []interface{}?
我一下子明白了問(wèn)題所在:解包的實(shí)參是一個(gè) []string 而不是 []interface{} 。
如果是多個(gè) string 變量作為 values 參數(shù),反而沒(méi)有問(wèn)題。但是把 []string 解包,就報(bào)錯(cuò)了。
當(dāng)然,提問(wèn)者自己也意識(shí)到問(wèn)題出在這里了,只是不明白原因。而我過(guò)分關(guān)注可變參數(shù),忘了留意類(lèi)型。
這個(gè)現(xiàn)象很容易重現(xiàn),完全沒(méi)必要用到 gorm 包。下面的代碼就報(bào)同樣的錯(cuò)誤:
package?main
import?(
????"fmt"
)
func?main()?{
????fmt.Print("this",?1,?"is",?"fine")
????ifaces?:=?[]interface{}{1,?"good",?"case",?"here"}
????//?OK
????fmt.Print(ifaces...)
????strs?:=?[]string{"bad",?"case",?"here"}
????//?cannot?use?strs?(variable?of?type?[]string)?as?[]interface{}?value?in?argument?to?fmt.Print
????fmt.Print(strs...)
????ifaces2?:=?make([]interface{},?0)
????for?_,?str?:=?range?strs?{
????????ifaces2?=?append(ifaces2,?str)
????}
????//?OK?now
????fmt.Print(ifaces2...)
}
注意是 fmt.Print(...interface{}) ,內(nèi)置函數(shù) print(...Type) ?的原理不在今天的討論范圍。
當(dāng)然理解可變參數(shù)也很必要。我們還是需要先理解(偽)解包,知道解包的背后是直接傳遞切片。如果是語(yǔ)言做了真實(shí)的解包和重新裝包,這個(gè)問(wèn)題也就不存在了(見(jiàn) ifaces2 部分代碼)。
一旦了解這些,提問(wèn)者很自然地發(fā)現(xiàn)問(wèn)題變成了:既然任意類(lèi)型都可以轉(zhuǎn)換為空接口 interface{},為什么 []string (或者任意別的類(lèi)型的切片)不能轉(zhuǎn)為空接口切片 []interface{}?
是的,不可以。其它強(qiáng)類(lèi)型語(yǔ)言也不可以。其它容器也不可以。
簡(jiǎn)單粗暴的結(jié)論就是:
子類(lèi)型變量可以向父類(lèi)型變量轉(zhuǎn)換;但存放子類(lèi)型的容器跟存放父類(lèi)型的容器沒(méi)有關(guān)系,不能轉(zhuǎn)換。(為了方便理解,父子類(lèi)型借用的 Java 的概念,Go 沒(méi)有繼承機(jī)制。)
Go 里面沒(méi)有繼承,只有接口和實(shí)現(xiàn);同時(shí)(暫時(shí))沒(méi)有泛型,只有內(nèi)置派生類(lèi)型(slice, map, chan 等)可以指定元素的類(lèi)型。Go 版本的表述是,即使類(lèi)型
T滿(mǎn)足接口I,各自的派生類(lèi)型也沒(méi)有任何關(guān)系(例如[]T和[]I)。
在 Java 里,Integer 是 Number 的子類(lèi),ArrayList 是 List 的子類(lèi)。但是,List 跟 List 沒(méi)有繼承關(guān)系,不能轉(zhuǎn)換,只能創(chuàng)建新容器,然后拷貝元素。
對(duì)應(yīng)到 Go 里,string 滿(mǎn)足 interface{} ,string 變量可以轉(zhuǎn)換為 interface{} 變量;但對(duì)應(yīng)的切片 []string 卻不能轉(zhuǎn)換為 []interface{} 。map 和 chan 同理。
原因
設(shè)計(jì)成這樣的理由,稍微解釋就很容易理解。
無(wú)論 Java 的類(lèi)繼承和接口實(shí)現(xiàn),還是 Go 的鴨子類(lèi)型接口,都是為了實(shí)現(xiàn)多態(tài)。
關(guān)于多態(tài)(特別是不同語(yǔ)言下的多態(tài))這里不展開(kāi)。一句話(huà)來(lái)形容的話(huà),Java 的多態(tài)是『代父從軍』,『龍生九子,各有不同』;Go 的多態(tài)則是『如果它跑起來(lái)像鴨子,叫起來(lái)像鴨子,那它就是一只鴨子』,但是每一只『鴨子』可以有自己不同的行為。
具體的實(shí)現(xiàn)只要滿(mǎn)足相同的約束,就可以賦值給上層抽象類(lèi)型(父類(lèi)型或者接口),當(dāng)作該類(lèi)型使用;與此同時(shí),不同的實(shí)現(xiàn)有不同的行為。調(diào)用代碼只需要認(rèn)準(zhǔn)上層類(lèi)型的約束,不必關(guān)心具體實(shí)現(xiàn)的行為,達(dá)到調(diào)用和實(shí)現(xiàn)的松耦合。這樣可以做到在不修改調(diào)用的情況下,替換掉具體實(shí)現(xiàn)。
Integer 完全可以當(dāng)作 Number 使用,因?yàn)?Number 有的行為 Integer 都有;日后也可以根據(jù)需要替換成 Float 或者 Double。ArrayList 和 List 也類(lèi)似(注意,T 是同一個(gè)類(lèi)型)。Go 的空接口 interface{} 對(duì)類(lèi)型沒(méi)有任何約束,可以接受任何類(lèi)型。
可一旦涉及容器,情況就變了。如果一個(gè) ArrayList 可以當(dāng)作 ArrayList ,意味著調(diào)用方可以往里面添加任何 Number 類(lèi)型(及子類(lèi)型),有可能是 Integer ,也可能是 Float 或者 Double 。
背后的具體實(shí)現(xiàn) ArrayList 可以放別的 Number 類(lèi)型嗎?不行。
同樣的,[]string 不能存放 string 以外的元素。如果允許 []string 轉(zhuǎn)換成 []interface{} 變量,意味著需要接受任意類(lèi)型的元素。
總結(jié):
父類(lèi)或者接口作為上層抽象類(lèi)型,在運(yùn)行時(shí)可能會(huì)被替換為任意子類(lèi)型,其可接受的行為應(yīng)該是子類(lèi)型的子集 。(父親會(huì)的技能,孩子們都要會(huì)。父親不能接孩子們不會(huì)的活,否則這個(gè)活就無(wú)法在運(yùn)行時(shí)分派給孩子們干。)
[]interface{} 可以接受的元素類(lèi)型,比任意具體類(lèi)型的切片都要多,顯然不滿(mǎn)足上述條件。從『空接口是任意類(lèi)型的抽象』,得出空接口切片(或者其它容器)也是上層抽象,就屬于想當(dāng)然了。
推薦閱讀
