未知世界總是讓人心生畏懼。古老的地圖上對(duì)于未到達(dá)過(guò)的區(qū)域總會(huì)使用惡龍和獅子進(jìn)行標(biāo)記。在前面的文章中,我們強(qiáng)調(diào)了Go是一門(mén)安全的編程語(yǔ)言,含有類(lèi)型的變量讓我們清楚的知道使用的是哪類(lèi)數(shù)據(jù),還有垃圾回收管理著內(nèi)存。哪怕是指針沒(méi)有C和C++所具備的槽點(diǎn)。
以上這些都沒(méi)錯(cuò),對(duì)于我們所寫(xiě)的大部分Go代碼,Go的運(yùn)行時(shí)都會(huì)守護(hù)我們。但總有例外。有時(shí)Go程序需要探索未知之地。本文中,我們會(huì)學(xué)習(xí)如何處理普通Go代碼無(wú)法解決的問(wèn)題。例如,在編譯期無(wú)法決定數(shù)據(jù)的類(lèi)型時(shí),我們可以使用reflect包所提供的反射支持來(lái)與數(shù)據(jù)交互甚至是構(gòu)造數(shù)據(jù)。在需要在Go中使用數(shù)據(jù)類(lèi)型的內(nèi)存布局時(shí),可以使用unsafe包。而如果某些功能的庫(kù)僅由C語(yǔ)言編寫(xiě)時(shí),我們可以使用cgo來(lái)調(diào)用C代碼。
讀者可能會(huì)想新手為什么要知道這些高級(jí)概念呢?有兩大原因,一是開(kāi)發(fā)者在搜索解決方案時(shí),有時(shí)對(duì)CV大法背后的代碼并不能完全理解。最好在把它們加到代碼庫(kù)中之前能知道一些可能引起問(wèn)題的高級(jí)技術(shù)。二是這些工具也很有意思。因?yàn)樗鼈兛梢詫?shí)現(xiàn)Go中通常無(wú)法實(shí)現(xiàn)的功能,把玩這些工具會(huì)很刺激。
反射使我們可以在運(yùn)行時(shí)操作類(lèi)型
人們喜歡使用Go的一個(gè)原因是它是靜態(tài)語(yǔ)言。通常在Go中聲明變量、類(lèi)型和函數(shù)都不復(fù)雜。在需要使用類(lèi)型、變量或函數(shù)時(shí),直接定義就好了:
type Foo struct {
A int
B string
}
var x Foo
func DoSomething(f Foo) {
fmt.Println(f.A, f.B)
}
我們使用類(lèi)型來(lái)表示編程時(shí)需要用到的數(shù)據(jù)結(jié)構(gòu)。因?yàn)轭?lèi)型是Go的核心部分,編譯器使用它們來(lái)保障代碼的正確性。但有時(shí)僅依賴編譯時(shí)的信息會(huì)產(chǎn)生限制。我們可能需要在編譯時(shí)使用編寫(xiě)程序時(shí)尚不存在的信息操作變量。可能是嘗試將文件或網(wǎng)絡(luò)請(qǐng)求映射到變量上,或是構(gòu)建一個(gè)可處理多種類(lèi)型的函數(shù)。在這些場(chǎng)景中,我們需要用到反射。反射讓我們可以在運(yùn)行時(shí)查看數(shù)據(jù)類(lèi)型。還可以通過(guò)它在運(yùn)行時(shí)查看、修改及創(chuàng)建變量、函數(shù)和結(jié)構(gòu)體。
那么在什么時(shí)候用到這一功能呢?Go的標(biāo)準(zhǔn)庫(kù)里有你要的答案。其用法主要有以下幾類(lèi):
讀取、寫(xiě)入數(shù)據(jù)庫(kù)。database/sql包使用反射向數(shù)據(jù)庫(kù)發(fā)送記錄或回讀數(shù)據(jù)。
Go的內(nèi)置模板庫(kù)text/template及html/template使用反射來(lái)處理傳遞給模板的值。
fmt包重度使用了反射,因?yàn)?code>fmt.Println及相關(guān)函數(shù)調(diào)用時(shí)依賴反射獲取參數(shù)的類(lèi)型。
errors包使用反射實(shí)現(xiàn)errors.Is和errors.As。
sort包使用反射實(shí)現(xiàn)各種類(lèi)型切片排序的函數(shù):sort.Slice、sort.SliceStable及sort.SliceIsSorted。
最后Go標(biāo)準(zhǔn)庫(kù)對(duì)反射的主要用法還有數(shù)據(jù)對(duì)JSON和XML格式及其它編碼包中定義的數(shù)據(jù)格式的序列化和反序列化。結(jié)構(gòu)體標(biāo)簽通過(guò)反射進(jìn)行訪問(wèn),結(jié)構(gòu)體中的字段也是通過(guò)反射進(jìn)行讀取和寫(xiě)入。
這些例子都有一個(gè)共同點(diǎn):需要訪問(wèn)和格式化導(dǎo)入或?qū)С鯣o程序的數(shù)據(jù)。通常看到使用反射的地方都是在程序與外部世界進(jìn)行互通時(shí)。
Go標(biāo)準(zhǔn)庫(kù)中對(duì)
reflect包還有一個(gè)用法:測(cè)試。在講切片時(shí),我們提到reflect中的一個(gè)函數(shù)DeepEqual。它處于reflect包中的原因是需要用到反射。reflect.DeepEqual函數(shù)查看兩值是否“底層相等”。這比使用==比較要更為底層,在標(biāo)準(zhǔn)庫(kù)中使用它來(lái)校驗(yàn)測(cè)試結(jié)果。也可使用它來(lái)比較無(wú)法使用==比較的數(shù)據(jù),如切片和map。大部分時(shí)候都用不到
DeepEqual,但如果希望比較兩個(gè)map來(lái)確定它們的鍵和值是否相同或是看兩個(gè)切片是否相同時(shí),可進(jìn)行使用。
Type、Kind和Value
我們已經(jīng)知道反射是什么以及何時(shí)使用,下面就來(lái)學(xué)習(xí)其原理。標(biāo)準(zhǔn)庫(kù)中的reflect包是Go語(yǔ)言中實(shí)現(xiàn)反射的類(lèi)型和函數(shù)的地方。反射的構(gòu)建有三大核心概念:Type、Kind和Value。
首先我們來(lái)看type。反射中的type就是字面意思。它定義了變量的屬性、存儲(chǔ)的內(nèi)容以及如何使用。借助反射,我們可以使用代碼查詢類(lèi)型并了解這些屬性。
Type和Kind
我們可使用reflect包中的TypeOf函數(shù)獲取到變量的類(lèi)型:
vType := reflect.TypeOf(v)
reflect.TypeOf函數(shù)返回reflect.Type類(lèi)型值,表示傳入TypeOf函數(shù)中變量的類(lèi)型。reflect.Type類(lèi)型定義了有關(guān)變量類(lèi)型的信息。我們無(wú)法窮舉所有方法,下面僅列舉部分。
Name方法不出所料返回的是該類(lèi)型的名稱。下面是一個(gè)快速示例:
var x int
xt := reflect.TypeOf(x)
fmt.Println(xt.Name()) // 返回int
f := Foo{}
ft := reflect.TypeOf(f)
fmt.Println(ft.Name()) // 返回Foo
xpt := reflect.TypeOf(&x)
fmt.Println(xpt.Name()) // 返回空字符串
上例中先定義了一個(gè)類(lèi)型為int的變量x。我們將其傳遞給了reflect.TypeOf獲取一個(gè)reflect.Type實(shí)例。對(duì)于像int這樣的基礎(chǔ)類(lèi)型,Name()返回類(lèi)型的名稱,此處即字符串int。對(duì)于結(jié)構(gòu)體則返回結(jié)構(gòu)體的名稱。切片或指針這種類(lèi)型是沒(méi)有名稱的,此時(shí)Name返回空字符串。
reflect.Type的Kind方法返回類(lèi)型為reflect.Kind的值,這是一個(gè)有關(guān)類(lèi)型組成的常量:切片、map、指針、結(jié)構(gòu)體、接口、字符串、數(shù)組、函數(shù)、整型或其它基礎(chǔ)類(lèi)型。kind和type之間的區(qū)別不太容易理解。記住一個(gè)規(guī)則:如果定義了結(jié)構(gòu)體Foo,kind為reflect.Struct,而type為Foo。
kind非常重要。在使用反射時(shí)需要注意的是reflect包的所有方法都假定使用者知道自己在干什么。reflect.Type和reflect包中定義的一些方法僅適用于特定的kind。例如在reflect.Type中有一個(gè)方法NumIn。如果reflect.Type實(shí)例表示一個(gè)函數(shù),它返回的是函數(shù)的入?yún)?shù)量。而在reflect.Type實(shí)例不是函數(shù)時(shí),NumIn會(huì)讓整個(gè)程序panic。
警告:通常調(diào)用kind對(duì)類(lèi)型無(wú)意義時(shí),方法調(diào)用會(huì)panic。一定要搞清楚反射類(lèi)型使用的哪些方法可運(yùn)行,哪些panic。reflect.Type另一個(gè)重置的方法是Elem。Go語(yǔ)言中一些類(lèi)型引用了其它類(lèi)型,r可用于發(fā)現(xiàn)包含的類(lèi)型。例如,對(duì)int的指針使用reflect.TypeOf:
var x int
xpt := reflect.TypeOf(&x)
fmt.Println(xpt.Name()) // 返回空字符串
fmt.Println(xpt.Kind()) //返回reflect.Ptr
fmt.Println(xpt.Elem().Name()) // 返回"int"
fmt.Println(xpt.Elem().Kind()) // 返回reflect.Int
[v_act]注意雖然以上代碼的返回值不同,但在控制臺(tái)打印時(shí)你看到的仍是ptr或int。這是由于打印時(shí)調(diào)用了func (k Kind) String() string方法。[/v_act]
以上為一個(gè)名稱為空的reflect.Type實(shí)例,kind為reflect.Ptr或者指針。在reflect.Type表示指針時(shí),Elem返回該指針?biāo)赶蝾?lèi)型的reflect.Type。本例中Name返回“int”,Kind返回reflect.Int。Elem方法還可用于切片、map、通道和數(shù)組。
reflect.Type上有針對(duì)結(jié)構(gòu)體的反射方法。使用NumField方法可獲取結(jié)構(gòu)體中字段的數(shù)量,使用Field方法可通過(guò)索引獲取結(jié)構(gòu)體中的字段。它返回reflect.StructField,中所描述的每個(gè)字段的結(jié)構(gòu),有字段的名稱、排序、類(lèi)型和結(jié)構(gòu)體標(biāo)簽。可在Go Playground中快速運(yùn)行如下示例:
type Foo struct {
A int `myTag:"value"`
B string `myTag:"value2"`
}
var f Foo
ft := reflect.TypeOf(f)
for i := 0; i < ft.NumField(); i++ {
curField := ft.Field(i)
fmt.Println(curField.Name, curField.Type.Name(),
curField.Tag.Get("myTag"))
}
這里創(chuàng)建了一個(gè)類(lèi)型Foo的實(shí)例,并使用reflect.TypeOf獲取f的reflect.Type。接著我們使用NumField構(gòu)建了一個(gè)for循環(huán)獲取f中每個(gè)字段的索引。然后使用Field方法獲取表示字段的reflect.StructField結(jié)構(gòu)體,然后我們可以使用reflect.StructField中的字段獲取有關(guān)字段的更多信息。輸出結(jié)果為:
A int value
B string value2
reflect.Type中有很多方法,基本都是同樣的形式,可用于訪問(wèn)描述變量類(lèi)型的一些信息。可以閱讀標(biāo)準(zhǔn)庫(kù)中reflect.Type的文檔了解更多的信息。
Value
除了查看變量的類(lèi)型,還可以使用反射來(lái)讀取變量值、設(shè)置變量值或從頭新建值。
我們使用reflect.ValueOf函數(shù)來(lái)創(chuàng)建代表變量值的reflect.Value實(shí)例:
vValue := reflect.ValueOf(v)
Go語(yǔ)言中的所有變量都擁有類(lèi)型,所以reflect.Value有一個(gè)Type方法返回reflect.Value的reflect.Type。同時(shí)reflect.Type上還有一個(gè)Kind方法。
就像reflect.Type有查看變量類(lèi)型信息的方法一樣,reflect.Value具備查看變量值信息的方法。這里不會(huì)一一列舉,但我們一起來(lái)看看如何使用reflect.Value獲取變量的值。
首先看如何從reflect.Value中讀取值。Interface方法以空接口的形式返回變量值。但丟失了類(lèi)型信息,在將Interface返回的值放入變量時(shí),可以使用類(lèi)型斷言獲取到正確的類(lèi)型:
s := []string{"a", "b", "c"}
sv := reflect.ValueOf(s) // sv類(lèi)型為reflect.Value
s2 := sv.Interface().([]string) // s2類(lèi)型為[]string
雖然包含任意類(lèi)型的reflect.Value實(shí)例均可調(diào)用Interface,還有一些針對(duì)內(nèi)置基礎(chǔ)類(lèi)型的特殊方法:Bool、Complex、Int、Uint、Float和String。另外在變量是一個(gè)字節(jié)切片時(shí)還可以使用Bytes方法。如果使用的方法與reflect.Value的類(lèi)型不相符的話,代碼會(huì)panic。
也可以使用反射來(lái)設(shè)置變量值,但要經(jīng)過(guò)三步。
首先,將變量的指針傳給reflect.ValueOf。這會(huì)返回一個(gè)表示該指針的reflect.Value:
i := 10
iv := reflect.ValueOf(&i)
接下來(lái)我們需要獲取待設(shè)置的實(shí)際值。可以對(duì)reflect.Value使用Elem方法獲取傳遞給reflect.ValueOf的指針?biāo)赶虻闹?。如?code>reflect.Type中的Elem返回的是所包含類(lèi)型所指向的類(lèi)型一樣,reflect.Value中的Elem返回的是指針?biāo)赶虻闹祷蚴谴鎯?chǔ)在接口中的值:
ivv := iv.Elem()
最后用到的是實(shí)現(xiàn)設(shè)置值的方法。在讀取基礎(chǔ)類(lèi)型時(shí)有各自的方法,同樣設(shè)置基礎(chǔ)類(lèi)型也有各自的方法:SetBool、SetInt、SetFloat、SetString和SetUint。本例中調(diào)用ivv.SetInt(20)會(huì)修改i的值。此時(shí)打印i得到的值是20:
ivv.SetInt(20)
fmt.Println(i) // 打印20
對(duì)于所有其它類(lèi)型,需要使用Set方法,它接收一個(gè)類(lèi)型為reflect.Value的變量。所設(shè)置的值無(wú)需是指針,因?yàn)橹灰x取值,而并不涉及修改。正如我們使用Interface()讀取基礎(chǔ)類(lèi)型一樣,我們可以使用Set寫(xiě)入基礎(chǔ)類(lèi)型。
需要傳指針給reflect.ValueOf來(lái)修改入?yún)⒌闹档脑蚝虶o語(yǔ)言中其它函數(shù)是一樣的。在指針表示可談參數(shù)一節(jié)中,我們使用了指針類(lèi)型的參數(shù)來(lái)表示希望修改參數(shù)的值。修改值時(shí),對(duì)指針解引用,然后設(shè)置值。如下兩個(gè)函數(shù)的處理一致:
func changeInt(i *int) {
*i = 20
}
func changeIntReflect(i *int) {
iv := reflect.ValueOf(i)
iv.Elem().SetInt(20)
}
小貼士:如果不向reflect.ValueOf傳遞變量的指針,仍可使用反射讀取變量值。但如若嘗試使用修改變量值的方法,方法調(diào)用會(huì)理所當(dāng)然地panic。
生成新值
在學(xué)習(xí)使用反射的最佳實(shí)踐之前,還有一個(gè)內(nèi)容要講解:如何創(chuàng)建值。reflect.New是反射中與new函數(shù)對(duì)應(yīng)的函數(shù)。它接收reflect.Type然后返回reflect.Value,后者是指定類(lèi)型的reflect.Value的指針。因其是指針,可以使用Interface方法為變量賦修改后的值。
reflect.New創(chuàng)建的是標(biāo)量類(lèi)型指針,我們還可以使用反射完成和make函數(shù)同樣的操作,使用如下的函數(shù):
func MakeChan(typ Type, buffer int) Value
func MakeMap(typ Type) Value
func MakeMapWithSize(typ Type, n int) Value
func MakeSlice(typ Type, len, cap int) Value
這些函數(shù)接收一個(gè)表示復(fù)合類(lèi)型(而非其所包含類(lèi)型)的reflect.Type。
構(gòu)造reflect.Type時(shí)總是需要從一個(gè)值開(kāi)始。但有一個(gè)小技巧可以在沒(méi)有值時(shí)創(chuàng)建表示reflect.Type的變量:
var stringType = reflect.TypeOf((*string)(nil)).Elem()
var stringSliceType = reflect.TypeOf([]string(nil))
變量stringType包含一個(gè)表示字符串的reflect.Type,變量stringSliceType包含一個(gè)表示[]string的reflect.Type。第一行需要花點(diǎn)精力進(jìn)行解碼。這里所做的是將nil轉(zhuǎn)換成字符串指針,使用reflect.TypeOf生成該指針類(lèi)型的reflect.Type,然后對(duì)指針的reflect.Type調(diào)用Elem獲取底層的類(lèi)型。我們?cè)诶ㄌ?hào)里放*string的原因是Go語(yǔ)言中運(yùn)算的順序,不加括號(hào)的話,編譯器會(huì)認(rèn)為我們將nil轉(zhuǎn)換為字符串,這是非法操作。
對(duì)于stringSliceType則更為簡(jiǎn)單,因?yàn)閚il是一個(gè)有效切片值。我們只需要將nil的類(lèi)型轉(zhuǎn)換為[]string,將其傳遞給reflect.Type。
有了這些類(lèi)型,我們就可以學(xué)習(xí)如何使用reflect.New和reflect.MakeSlice:
ssv := reflect.MakeSlice(stringSliceType, 0, 10)
sv := reflect.New(stringType).Elem()
sv.SetString("hello")
ssv = reflect.Append(ssv, sv)
ss := ssv.Interface().([]string)
fmt.Println(ss) // 打印[hello]
可以在Go Playground中自行測(cè)試這段代碼。
使用反射檢查接口值是否為nil
我們?cè)?strong>接口和nil一節(jié)中討論過(guò),如果一個(gè)具體類(lèi)型的nil變量賦值給接口類(lèi)型的變量,接口類(lèi)型的變量就不是nil。這是因?yàn)樵摻涌谧兞坑幸粋€(gè)關(guān)聯(lián)類(lèi)型。如果希望檢查接口所關(guān)聯(lián)的值是否為nil,需要通過(guò)反射使用兩個(gè)方法:IsValid和IsNil:
func hasNoValue(i interface{}) bool {
iv := reflect.ValueOf(i)
if !iv.IsValid() {
return true
}
switch iv.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Func, reflect.Interface:
return iv.IsNil()
default:
return false
}
}
如果reflect.Value存放的不為nil接口IsValid返回true。我們要先進(jìn)行這一檢測(cè),因?yàn)樵?code>IsValid為false時(shí)對(duì)reflect.Value調(diào)用其它方法會(huì)panic。如果reflect.Value的值是nil的話IsNil方法返回true,但僅在reflect.Kind可為nil時(shí)才能進(jìn)行調(diào)用。如果對(duì)零值不為nil的類(lèi)型調(diào)用該方法,也會(huì)panic。
雖然可以監(jiān)測(cè)到接口是否為nil,還是應(yīng)該在編寫(xiě)代碼時(shí)保證與nil接口關(guān)聯(lián)的值也可以正確執(zhí)行。把這段留在別無(wú)選擇時(shí)使用。
使用反射編寫(xiě)數(shù)據(jù)序列化工具
前面已講過(guò),標(biāo)準(zhǔn)庫(kù)用反射實(shí)現(xiàn)序列化和反序列化。我們來(lái)學(xué)習(xí)如何自己構(gòu)建一個(gè)數(shù)據(jù)序列化工具。Go語(yǔ)言提供了csv.NewReader和csv.NewWriter方法用于從將CSV文件讀取到字符切片的切片中以及將字符切片的切片寫(xiě)至CSV文件中,但無(wú)法將該數(shù)據(jù)映射到結(jié)構(gòu)體的字段。我們就能完成這個(gè)功能。
注:本例為了便于講解,簡(jiǎn)化為所支持的類(lèi)型。完成代表請(qǐng)見(jiàn)Go Playground。
我們會(huì)開(kāi)始定義自己的API。和其它序列化工具一樣,我們會(huì)定義結(jié)構(gòu)體標(biāo)簽來(lái)指定結(jié)構(gòu)體中的字段與數(shù)據(jù)中字段之間的映射:
type MyData struct {
Name string `csv:"name"`
Age int `csv:"age"`
HasPet bool `csv:"has_pet"`
}
對(duì)外的API有兩個(gè)函數(shù):
// Unmarshal maps all of the rows of data in a slice of slice of strings
// into a slice of structs.
// The first row is assumed to be the header with the column names.
func Unmarshal(data [][]string, v interface{}) error
// Marshal maps all of the structs in a slice of structs to a slice of slice
// of strings.
// The first row written is the header with the column names.
func Marshal(v interface{}) ([][]string, error)
我們先編寫(xiě)Marshal函數(shù)本身,然后來(lái)看其使用的兩個(gè)幫助函數(shù):
func Marshal(v interface{}) ([][]string, error) {
sliceVal := reflect.ValueOf(v)
if sliceVal.Kind() != reflect.Slice {
return nil, errors.New("must be a slice of structs")
}
structType := sliceVal.Type().Elem()
if structType.Kind() != reflect.Struct {
return nil, errors.New("must be a slice of structs")
}
var out [][]string
header := marshalHeader(structType)
out = append(out, header)
for i := 0; i < sliceVal.Len(); i++ {
row, err := marshalOne(sliceVal.Index(i))
if err != nil {
return nil, err
}
out = append(out, row)
}
return out, nil
}
因?yàn)槲覀円瘮?shù)化任意類(lèi)型的結(jié)構(gòu)體,所以參數(shù)的類(lèi)型需要使用interface{}。這里用的不是結(jié)構(gòu)體切片的指針,因?yàn)槲覀冎灰獜那衅羞M(jìn)行讀取,無(wú)需修改。
我們CSV的第一行是含列名的頭,因此可以通過(guò)結(jié)構(gòu)體中字段標(biāo)簽獲取到這些列名。我們使用Type方法來(lái)從reflect.Value中獲取切片的reflect.Type,然后調(diào)用Elem方法來(lái)獲取切片元素的reflect.Type。之后將其傳遞給marshalHeader并追加到輸出的響應(yīng)中。
接著,我們使用反射遍歷結(jié)構(gòu)體中的每個(gè)元素,將每個(gè)元素的reflect.Value傳遞給marshalOne,追加到輸出結(jié)果中。完成遍歷后,返回字符串切片的切片。
我們來(lái)看第一個(gè)幫助函數(shù)marshalHeader的實(shí)現(xiàn):
func marshalHeader(vt reflect.Type) []string {
var row []string
for i := 0; i < vt.NumField(); i++ {
field := vt.Field(i)
if curTag, ok := field.Tag.Lookup("csv"); ok {
row = append(row, curTag)
}
}
return row
}
這個(gè)函數(shù)遍歷reflect.Type的字段,讀取每個(gè)字段的csv標(biāo)簽追加至字符串切片并返回該切片。
第二個(gè)幫助函數(shù)是marshalOne:
func marshalOne(vv reflect.Value) ([]string, error) {
var row []string
vt := vv.Type()
for i := 0; i < vv.NumField(); i++ {
fieldVal := vv.Field(i)
if _, ok := vt.Field(i).Tag.Lookup("csv"); !ok {
continue
}
switch fieldVal.Kind() {
case reflect.Int:
row = append(row, strconv.FormatInt(fieldVal.Int(), 10))
case reflect.String:
row = append(row, fieldVal.String())
case reflect.Bool:
row = append(row, strconv.FormatBool(fieldVal.Bool()))
default:
return nil, fmt.Errorf("cannot handle field of kind %v",
fieldVal.Kind())
}
}
return row, nil
}
它接收reflect.Value,返回一個(gè)string切片。我們創(chuàng)建了字符串切片,然后對(duì)結(jié)構(gòu)體中的每個(gè)字段,判斷reflect.Kind來(lái)決定如何將其轉(zhuǎn)換為字符串,并追加到輸出中。
至此我們就完成了一個(gè)簡(jiǎn)單的序列化工具。下面來(lái)看如何進(jìn)行反序列化:
func Unmarshal(data [][]string, v interface{}) error {
sliceValPtr := reflect.ValueOf(v)
if sliceValPtr.Kind() != reflect.Ptr {
return errors.New("must be a pointer to a slice of structs")
}
sliceVal := sliceValPtr.Elem()
if sliceVal.Kind() != reflect.Slice {
return errors.New("must be a pointer to a slice of structs")
}
structType := sliceVal.Type().Elem()
if structType.Kind() != reflect.Struct {
return errors.New("must be a pointer to a slice of structs")
}
// assume the first row is a header
header := data[0]
namePos := make(map[string]int, len(header))
for k, v := range header {
namePos[v] = k
}
for _, row := range data[1:] {
newVal := reflect.New(structType).Elem()
err := unmarshalOne(row, namePos, newVal)
if err != nil {
return err
}
sliceVal.Set(reflect.Append(sliceVal, newVal))
}
return nil
}
因?yàn)槲覀円獙?shù)據(jù)拷貝到各類(lèi)結(jié)構(gòu)體,需要使用interface{}類(lèi)型的參數(shù)。此外,因?yàn)橐薷膮?shù)中存儲(chǔ)的值,需要傳遞結(jié)構(gòu)體切片的指針。Unmarshal函數(shù)將結(jié)構(gòu)體指針切片轉(zhuǎn)化為reflect.Value,然后獲取底層的切片,之后得到底層切片中結(jié)構(gòu)體的類(lèi)型。
前面已經(jīng)說(shuō)過(guò),我們會(huì)假定數(shù)據(jù)的第一行是列頭字段名。我們使用這一信息來(lái)構(gòu)造map,因此將csv結(jié)構(gòu)體標(biāo)簽值關(guān)聯(lián)到正確的數(shù)據(jù)元素。
然后我們遍歷剩下的所有string切片,使用結(jié)構(gòu)體的reflect.Type新建reflect.Value,調(diào)用unmarshalOne來(lái)將當(dāng)前string切片中的數(shù)據(jù)復(fù)制到結(jié)構(gòu)體中,然后將結(jié)構(gòu)體添加到切片中。遍歷完所有數(shù)據(jù)行后返回。
剩下的就是unmarshalOne的實(shí)現(xiàn):
func unmarshalOne(row []string, namePos map[string]int, vv reflect.Value) error {
vt := vv.Type()
for i := 0; i < vv.NumField(); i++ {
typeField := vt.Field(i)
pos, ok := namePos[typeField.Tag.Get("csv")]
if !ok {
continue
}
val := row[pos]
field := vv.Field(i)
switch field.Kind() {
case reflect.Int:
i, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
field.SetInt(i)
case reflect.String:
field.SetString(val)
case reflect.Bool:
b, err := strconv.ParseBool(val)
if err != nil {
return err
}
field.SetBool(b)
default:
return fmt.Errorf("cannot handle field of kind %v",
field.Kind())
}
}
return nil
}
這個(gè)函數(shù)遍歷新建的reflect.Value中的每個(gè)字段,使用當(dāng)前字段的csv結(jié)構(gòu)體標(biāo)簽查找其名稱,使用namePosmap查找數(shù)據(jù)切片中的元素,將值由字符串轉(zhuǎn)化為相應(yīng)的類(lèi)型,并為當(dāng)前字段設(shè)置值。完成所有字段后函數(shù)返回。
我們已經(jīng)編寫(xiě)好序列化和反序列化工具,可以與Go標(biāo)準(zhǔn)庫(kù)已有的CSV進(jìn)行集成:
data := `name,age,has_pet
Jon,"100",true
"Fred ""The Hammer"" Smith",42,false
Martha,37,"true"
`
r := csv.NewReader(strings.NewReader(data))
allData, err := r.ReadAll()
if err != nil {
panic(err)
}
var entries []MyData
Unmarshal(allData, &entries)
fmt.Println(entries)
//now to turn entries into output
out, err := Marshal(entries)
if err != nil {
panic(err)
}
sb := &strings.Builder{}
w := csv.NewWriter(sb)
w.WriteAll(out)
fmt.Println(sb)
使用反射構(gòu)建函數(shù)自動(dòng)完成重復(fù)任務(wù)
通過(guò)Go的反射還可以實(shí)現(xiàn)函數(shù)的創(chuàng)建。我們可以使用這一技術(shù)對(duì)已有的函數(shù)封裝通用功能來(lái)避免編寫(xiě)重復(fù)的代碼。下例為所傳遞函數(shù)添加一個(gè)計(jì)時(shí)的工廠函數(shù):
func MakeTimedFunction(f interface{}) interface{} {
ft := reflect.TypeOf(f)
fv := reflect.ValueOf(f)
wrapperF := reflect.MakeFunc(ft, func(in []reflect.Value) []reflect.Value {
start := time.Now()
out := fv.Call(in)
end := time.Now()
fmt.Println(end.Sub(start))
return out
})
return wrapperF.Interface()
}
該函數(shù)可接收任意函數(shù),因此參數(shù)類(lèi)型為interface{}。然后將表示函數(shù)的reflect.Type傳遞給reflect.MakeFunc,同時(shí)傳遞的還有的閉包,它捕獲開(kāi)始時(shí)間、使用反射調(diào)用原始函數(shù)、捕獲結(jié)束時(shí)間、打印時(shí)間差并返回原函數(shù)所運(yùn)算的值。reflect.MakeFunc所返回的值為一個(gè)reflect.Value,我們調(diào)用Interface方法獲取返回值。使用方式如下:
func timeMe(a int) int {
time.Sleep(time.Duration(a) * time.Second)
result := a * 2
return result
}
func main() {
timed:= MakeTimedFunction(timeMe).(func(int) int)
fmt.Println(timed(2))
}
可通過(guò)Go Playground查看程序的完整版本。
雖然生成函數(shù)很精巧,但請(qǐng)謹(jǐn)慎使用這一功能。確保在使用生成的函數(shù)時(shí)添加的功能足夠清晰。否則程序的數(shù)據(jù)流會(huì)很難理解。此外我們?cè)?strong>僅在值得之處使用反射一節(jié)會(huì)討論到,反射會(huì)拉慢程序,所以使用反射生成或調(diào)用函數(shù)會(huì)影響到性能,除非代碼所生成的函數(shù)原本就很慢,像網(wǎng)絡(luò)調(diào)用。記住反射最佳用途是程序內(nèi)外的數(shù)據(jù)映射。
遵循這一生成函數(shù)規(guī)則的項(xiàng)目有SQL映射庫(kù)Proteus。它通過(guò)SQL查詢和函數(shù)字段或變量生成函數(shù)創(chuàng)建一個(gè)類(lèi)型安全的數(shù)據(jù)庫(kù)API。可以在GopherCon 2017的演講中更多地了解Proteus,演講主題為Runtime Generated, Typesafe, and Declarative: Pick Any Three,源代碼位于GitHub。
可使用反射構(gòu)建結(jié)構(gòu)體,但別這么干
反射還能實(shí)現(xiàn)一個(gè)奇怪的功能。reflect.StructOf函數(shù)接收reflect.StructField切片返回表示新結(jié)構(gòu)體類(lèi)型的reflect.Type。這些結(jié)構(gòu)體僅能使用interface{}類(lèi)型的變量賦值,其字段也僅能使用反射讀取和寫(xiě)入。
大多數(shù)情況這種特性僅具有學(xué)術(shù)意義。如果希望看下如何使用reflect.StructOf,可以在Go Playground查看memoizer函數(shù)。它使用動(dòng)態(tài)生成的結(jié)構(gòu)體作為緩存函數(shù)輸出的map的鍵。
反射無(wú)法生成方法
我們了解了反射可實(shí)現(xiàn)的功能,但有一件事它做不了。雖然我們可以使用反射新建函數(shù)和結(jié)構(gòu)體類(lèi)型,但無(wú)法使用反射為一個(gè)類(lèi)型添加方法。也就是說(shuō)無(wú)法使用反射新建一個(gè)實(shí)現(xiàn)了接口的類(lèi)型。
僅在利大于弊時(shí)使用反射
雖然Go中反射是和外界互換數(shù)據(jù)非常重要,但用于其它場(chǎng)景時(shí)要很謹(jǐn)慎。反射是開(kāi)銷(xiāo)的。為進(jìn)行演示,我們使用反射實(shí)現(xiàn)一個(gè)Filter。這是很多編程語(yǔ)言中的常見(jiàn)函數(shù),接收一個(gè)列表,檢測(cè)列表中的每一項(xiàng),然后僅返回那些通過(guò)檢測(cè)的子項(xiàng)。Go不允許我們編寫(xiě)一個(gè)針對(duì)所有類(lèi)型切片的類(lèi)型安全函數(shù),但可以使用反射來(lái)編寫(xiě)Filter:
func Filter(slice interface{}, filter interface{}) interface{} {
sv := reflect.ValueOf(slice)
fv := reflect.ValueOf(filter)
sliceLen := sv.Len()
out := reflect.MakeSlice(sv.Type(), 0, sliceLen)
for i := 0; i < sliceLen; i++ {
curVal := sv.Index(i)
values := fv.Call([]reflect.Value{curVal})
if values[0].Bool() {
out = reflect.Append(out, curVal)
}
}
return out.Interface()
}
用法如下:
names := []string{"Andrew", "Bob", "Clara", "Hortense"}
longNames := Filter(names, func(s string) bool {
return len(s) > 3
}).([]string)
fmt.Println(longNames)
ages := []int{20, 50, 13}
adults := Filter(ages, func(age int) bool {
return age >= 18
}).([]int)
fmt.Println(adults)
打印出的結(jié)果為:
[Andrew Clara Hortense]
[20 50]
這個(gè)使用了反射的過(guò)濾器函數(shù)并不難理解,但一定比自定義函數(shù)要么。我們來(lái)看i7-8700內(nèi)存32GB的機(jī)器使用Go 1.14過(guò)濾1000個(gè)元素的字符和數(shù)字切片的執(zhí)行性能,并對(duì)比自定義函數(shù):
BenchmarkFilterReflectString-12 4822 229099 ns/op 87361 B/op 2219 allocs/op
BenchmarkFilterString-12 158197 7795 ns/op 16384 B/op 1 allocs/op
BenchmarkFilterReflectInt-12 4962 232885 ns/op 72256 B/op 2503 allocs/op
BenchmarkFilterInt-12 348441 3440 ns/op 8192 B/op 1 allocs/op
示例代碼請(qǐng)見(jiàn)GitHub,讀者可自行測(cè)試。
使用反射比字符串自定義過(guò)濾函數(shù)慢大約30倍,而對(duì)于整數(shù)則慢了近70倍。它使用大量的內(nèi)存并執(zhí)行了幾千次內(nèi)存分配,為垃圾回收增加了大量工作。根據(jù)不同需求,你也許能接受這種折衷做法,但一定要深思熟慮。
另一個(gè)嚴(yán)重的弊端是編譯器無(wú)法防止你向slice或filter參數(shù)傳遞錯(cuò)誤的類(lèi)型。讀者可能不介意幾千納秒的CPU時(shí)間,但如果有人向Filter傳遞了錯(cuò)誤類(lèi)型的函數(shù)或切片,會(huì)導(dǎo)致程序在生產(chǎn)環(huán)境崩潰。這種維護(hù)成本可能會(huì)過(guò)高。為不同類(lèi)型重復(fù)編寫(xiě)相同的函數(shù)雖然很重復(fù),但節(jié)省幾行代碼在大部分時(shí)候都顯得不值得。
unsafe是不安全的
reflect包允許我們操作的是類(lèi)型和值,unsafe包則允許我們操作內(nèi)存。unsafe包很小也很奇怪。它定義了三個(gè)函數(shù)和一個(gè)類(lèi)型,和其它包中的類(lèi)型和函數(shù)行為都不相似。
三個(gè)函數(shù)分別為Sizeof(接收任意類(lèi)型的變量返回其使用的字節(jié)數(shù))、Offsetof(接收結(jié)構(gòu)體字段返回結(jié)構(gòu)體起始至字段起始之間的字節(jié)數(shù))和Alignof(接收字段或變量返回所需要的字節(jié)對(duì)齊系數(shù))。與其它Go中的非內(nèi)置函數(shù)不同,對(duì)這些函數(shù)可以傳遞任意值,返回值為常量,因此可用于常量表達(dá)式。
unsafe.Pointer類(lèi)型是一種特殊類(lèi)型,存在的目的只有一個(gè):任意類(lèi)型的指針可與unsafe.Pointer互轉(zhuǎn)。除指針外,unsafe.Pointer還可與特殊的整型互轉(zhuǎn),稱為uintptr。與其它整數(shù)類(lèi)型一樣,可對(duì)其做數(shù)學(xué)運(yùn)算。這樣我們可以進(jìn)入一種類(lèi)型的實(shí)例,提取單獨(dú)的字節(jié)。我們也可以像C和C++中的指針那樣執(zhí)行指數(shù)運(yùn)算。對(duì)字節(jié)的操作會(huì)修改變量的值。
unsafe代碼中有兩種常見(jiàn)的模式。第一種是實(shí)現(xiàn)兩種通常不可互轉(zhuǎn)的變量類(lèi)型之間的互轉(zhuǎn)。第二種是通過(guò)將變量轉(zhuǎn)化為unsafe.Pointer、將unsafe.Pointer轉(zhuǎn)化為uintptr然后再?gòu)?fù)制或操作底層字節(jié)來(lái)讀取或修改變量中的字節(jié)。我們來(lái)看什么時(shí)候應(yīng)該使用它以及使用時(shí)候不該使用。
使用unsafe 轉(zhuǎn)換外部二進(jìn)制數(shù)據(jù)
既然Go的核心是內(nèi)存安全,你可能會(huì)納悶為什么會(huì)存在unsafe。就像使用reflect來(lái)進(jìn)行外部和Go之間的文本數(shù)據(jù)互通,我們使用unsafe來(lái)轉(zhuǎn)換二進(jìn)制數(shù)據(jù)。使用unsafe有兩個(gè)主要原因。Costa、Mujahid、Abdalkareem和Shihab在2020年發(fā)表了篇名為Breaking Type-Safety in Go: An Empirical Study on the Usage of the unsafe Package的論文,調(diào)查了2438個(gè)流行的開(kāi)源項(xiàng)目,發(fā)現(xiàn):
- 所研究項(xiàng)目中的24%至少在代碼中使用了一次
unsafe。 - 對(duì)
unsafe的使用大部分是為了集成操作系統(tǒng)和C代碼(45.7%)。 - 開(kāi)發(fā)者還經(jīng)常使用
unsafe來(lái)編寫(xiě)效率更高的Go代碼(23.6%)。
大部分對(duì)unsafe的使用都是為了操作系統(tǒng)。Go標(biāo)準(zhǔn)庫(kù)使用unsafe來(lái)向操作系統(tǒng)讀取或?qū)懭霐?shù)據(jù)??梢栽跇?biāo)準(zhǔn)庫(kù)的syscall包或更高階的sys包中看到示例。我們可以在Matt Layher的著名博客文章中學(xué)習(xí)到更多有關(guān)如何使用unsafe來(lái)與操作系統(tǒng)進(jìn)行交互。
人們使用unsafe的第二大原因是為了性能,尤其是從網(wǎng)絡(luò)讀取數(shù)據(jù)的場(chǎng)景。如果希望與Go的數(shù)據(jù)結(jié)構(gòu)進(jìn)行映射,unsafe.Pointer提供了一種非??焖俚姆绞?。我們來(lái)使用一個(gè)假想的示例進(jìn)行探索。假設(shè)有一個(gè)結(jié)構(gòu)如下的線路協(xié)議:
- Value:4字節(jié),表示無(wú)符號(hào)大端字節(jié)序32位整數(shù)
- Label:10字節(jié),值的ASCII名稱
- Active:1字節(jié),表示字段是否為活躍的布爾標(biāo)記
- Padding:1字節(jié),因?yàn)槲覀兿M麅?nèi)容剛好16字節(jié)
注:通過(guò)網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)通過(guò)都是采用大端序格式(大序字節(jié)排前面),通過(guò)稱之為網(wǎng)絡(luò)字節(jié)序。而當(dāng)前大部分CPU都是小端序(或是以小端模式運(yùn)行的雙端序),在對(duì)網(wǎng)絡(luò)讀取或?qū)懭霐?shù)據(jù)時(shí)要格外小心。
可以這樣定義該數(shù)據(jù)結(jié)構(gòu):
type Data struct {
Value uint32 // 4 bytes
Label [10]byte // 10 bytes
Active bool // 1 byte
// Go padded this with 1 byte to make it align
}
假設(shè)我們從網(wǎng)絡(luò)讀取如下字節(jié):
[0 132 95 237 80 104 111 110 101 0 0 0 0 0 1 0]
我們將這些字節(jié)計(jì)入一個(gè)長(zhǎng)16的數(shù)組,然后將該數(shù)組轉(zhuǎn)化為前述的結(jié)構(gòu)體。
注:為什么使用的是數(shù)組而非切片呢?記住數(shù)組和結(jié)構(gòu)體一樣是值類(lèi)弄:字節(jié)直接分配。在下一節(jié)中我們會(huì)學(xué)習(xí)如何對(duì)切片使用unsafe。
通過(guò)安全的Go代碼,可以這樣進(jìn)行映射:
func DataFromBytes(b [16]byte) Data {
d := Data{}
d.Value = binary.BigEndian.Uint32(b[:4])
copy(d.Label[:], b[4:14])
d.Active = b[14] != 0
return d
}
或者可以使用unsafe.Pointer:
func DataFromBytesUnsafe(b [16]byte) Data {
data := *(*Data)(unsafe.Pointer(&b))
if isLE {
data.Value = bits.ReverseBytes32(data.Value)
}
return data
}
第一行代碼看上去有些迷,我們可以拆開(kāi)來(lái)進(jìn)行理解。首先,我們接收一個(gè)字節(jié)數(shù)組的指針,將其轉(zhuǎn)換為unsafe.Pointer。然后中將unsafe.Pointer轉(zhuǎn)換為(*Data)(需要將(*Data)放到括號(hào)里,這是因?yàn)镚o的運(yùn)算順序)。我們希望返回一個(gè)結(jié)構(gòu)體,而不是其指針,因此要解引用該指針。接下來(lái)我們檢查標(biāo)記看是否為小端序平臺(tái)。若是,則反轉(zhuǎn)Value字段中的字節(jié)。最后返回該值。
我們是怎么知道平臺(tái)是否為小端序的呢?所使用的代碼如下:
var isLE bool
func init() {
var x uint16 = 0xFF00
xb := *(*[2]byte)(unsafe.Pointer(&x))
isLE = (xb[0] == 0x00)
}
我們?cè)?strong>init函數(shù):非必要勿使用一節(jié)中討論過(guò),應(yīng)當(dāng)避免使用init函數(shù),除非是初始化包級(jí)的不可變值。因?yàn)樘幚砥鞯拇笮《嗽诔绦蜻\(yùn)行時(shí)不會(huì)發(fā)生改變,這正是可用的場(chǎng)景。
在小端序平臺(tái)上,表示x的字節(jié)存儲(chǔ)為[00 FF]。而在大端序平臺(tái),x在內(nèi)存中存儲(chǔ)為[FF 00]。我們使用unsafe.Pointer將數(shù)字轉(zhuǎn)換為一個(gè)字節(jié)數(shù)組,然后查看第一個(gè)字節(jié)來(lái)決定isLE的值。
同樣,如果希望將數(shù)據(jù)回寫(xiě)至網(wǎng)絡(luò),我們可以使用安全的Go代碼:
func BytesFromData(d Data) [16]byte {
out := [16]byte{}
binary.BigEndian.PutUint32(out[:4], d.Value)
copy(out[4:14], d.Label[:])
if d.Active {
out[14] = 1
}
return out
}
或者使用unsafe:
func BytesFromDataUnsafe(d Data) [16]byte {
if isLE {
d.Value = bits.ReverseBytes32(d.Value)
}
b := *(*[16]byte)(unsafe.Pointer(&d))
return b
}
是否值得這么做呢?在i7-8700的電腦上(小端序),使用unsafe.Pointer的速度大約為2倍:
BenchmarkBytesFromData-12 112741796 10.4 ns/op
BenchmarkBytesFromDataUnsafe-12 298846651 4.01 ns/op
BenchmarkDataFromBytes-12 100000000 10.3 ns/op
BenchmarkDataFromBytesUnsafe-12 235992582 5.95 ns/op
注:本節(jié)中的所有代碼請(qǐng)見(jiàn)GitHub。
如果程序中有大量這類(lèi)轉(zhuǎn)換,則值得使用更低級(jí)的技術(shù)。但對(duì)于大部分程序,請(qǐng)使用安全代碼。
unsafe字符串和切片
我們也可以使用unsafe與切片和字符串進(jìn)行交互。在字符串、符文和字節(jié)一節(jié)中,我們?cè)岬紾o中的字符串使用一組字節(jié)的指針和長(zhǎng)度進(jìn)行表示。reflect包中有一個(gè)reflect.StringHeader類(lèi)型具有這種結(jié)構(gòu),我們使用它來(lái)訪問(wèn)、修改底層的字節(jié)。
s := "hello"
sHdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println(sHdr.Len) // prints 5
我們可以使用指針運(yùn)算讀取字符串中的字節(jié),用的是sHdr中的Data字段,其類(lèi)型為uintptr:
for i := 0; i < sHdr.Len; i++ {
bp := *(*byte)(unsafe.Pointer(sHdr.Data + uintptr(i)))
fmt.Print(string(bp))
}
fmt.Println()
runtime.KeepAlive(s)
reflect.StringHeader中的Data字段類(lèi)型為uintptr,正如我們所討論的,uintptr指向有效內(nèi)存的可靠性可能僅一行。如何防止垃圾回收讓指針失效呢?我們通過(guò)在函數(shù)末尾添加一個(gè)runtime.KeepAlive(s)調(diào)用來(lái)進(jìn)行實(shí)現(xiàn)。這告訴Go運(yùn)行時(shí)在調(diào)用KeepAlive之前不要回收s。
如果想測(cè)試這段代碼,請(qǐng)?jiān)L問(wèn)Go Playground。
就像可以用unsafe從字符串獲取reflect.StringHeader,也可從切片獲取reflect.SliceHeader。它有三個(gè)字段:Len、Cap和Data,分別表示切片的長(zhǎng)度、容量和數(shù)據(jù)指針:
s := []int{10, 20, 30}
sHdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Println(sHdr.Len) // prints 3
fmt.Println(sHdr.Cap) // prints 3
我對(duì)字符串的操作一樣,我們將int切片轉(zhuǎn)換為了unsafe.Pointer。然后將unsafe.Pointer轉(zhuǎn)換為reflect.SliceHeader指針。接著可以通過(guò)Len和Cap字段訪問(wèn)切片的長(zhǎng)度和容量。然后遍歷切片:
intByteSize := unsafe.Sizeof(s[0])
fmt.Println(intByteSize)
for i := 0; i < sHdr.Len; i++ {
intVal := *(*int)(unsafe.Pointer(sHdr.Data + intByteSize*uintptr(i)))
fmt.Println(intVal)
}
runtime.KeepAlive(s)
因?yàn)?code>int的大小可能是32位也可能是64位,我們必須使用unsafe.Sizeof來(lái)確定Data字段所指向的內(nèi)存塊占多少字節(jié)。然后將i轉(zhuǎn)化為uintptr,乘上int的大小,添加至Data字段,由uintptr轉(zhuǎn)化為unsafe.Pointer,然后將指針轉(zhuǎn)化為int,最后對(duì)int進(jìn)行解引用獲取其值。
可以在Go Playground中運(yùn)行這段代碼。
unsafe工具
Go是一種值傳遞的語(yǔ)言,有一個(gè)編譯器標(biāo)記用于發(fā)現(xiàn)uintptr和unsafe.Pointer的誤用。使用-gcflags=-d=checkptr標(biāo)記運(yùn)行代碼來(lái)添加額外的運(yùn)行時(shí)檢測(cè)。和數(shù)據(jù)爭(zhēng)用檢測(cè)一樣,并不能保證找到所有的unsafe問(wèn)題并且會(huì)拖慢程序。但在測(cè)試代碼是一種良好實(shí)踐。
如果希望學(xué)習(xí)更多有關(guān)unsafe的知識(shí),請(qǐng)閱讀該包的官方文檔。
警告??unsafe很強(qiáng)大也很底層。除非知道在干什么并且需要其所提供的性能提升否則請(qǐng)不要使用它。
cgo的作用是集成,而非性能
和反射和unsafe一樣,cgo也多用于處理Go程序與外部世界的跨界問(wèn)題。反射有助于集成外部文本數(shù)據(jù),unsafe用于操作系統(tǒng)和網(wǎng)絡(luò)數(shù)據(jù),而cgo用于集成C語(yǔ)言庫(kù)。
雖然已經(jīng)50多歲了,C仍是編程語(yǔ)言的通用語(yǔ)言(lingua franca)。所有主流操作系統(tǒng)都是由C或C++編寫(xiě)的,也就是說(shuō)打包了C編寫(xiě)的很多庫(kù)。同時(shí)也表明幾乎所有操作語(yǔ)言都提供了與C庫(kù)集成的方式。Go的FFI(語(yǔ)言交互接口)調(diào)用C cgo。
我們已經(jīng)多次看到Go是一種偏好顯示聲明的語(yǔ)言。Go開(kāi)發(fā)者有時(shí)不屑于其它語(yǔ)言稱為“魔法”的自動(dòng)化行為。但是,使用cgo就像是來(lái)到了霍格沃茲。我們來(lái)看看這種魔法膠水代碼。無(wú)法在Go Playground中運(yùn)行cgo代碼,示例代碼請(qǐng)見(jiàn)GitHub。我們先使用一個(gè)簡(jiǎn)單的程序調(diào)用C代碼執(zhí)行數(shù)學(xué)運(yùn)算:
package main
import "fmt"
/*
#cgo LDFLAGS: -lm
#include <stdio.h>
#include <math.h>
#include "mylib.h"
int add(int a, int b) {
int sum = a + b;
printf("a: %d, b: %d, sum %d\n", a, b, sum);
return sum;
}
*/
import "C"
func main() {
sum := C.add(3, 2)
fmt.Println(sum)
fmt.Println(C.sqrt(100))
fmt.Println(C.multiply(10, 20))
}
mylib.h頭文件以及mylib.c位于main.go的同目錄下:
int multiply(int a, int b);
#include "mylib.h"
int multiply(int a, int b) {
return a * b;
}
假設(shè)電腦中安裝了C編譯器,那么只需要使用go build編譯該程序:
$ go build
$ ./example1
a: 3, b: 2, sum 5
5
10
200
背后發(fā)生了什么呢?標(biāo)準(zhǔn)庫(kù)中并沒(méi)有名為C的包。C是一個(gè)自動(dòng)生成的包,其標(biāo)識(shí)符多來(lái)自注釋中嵌入的C代碼。本例中,我們聲明了C函數(shù)add,cgo讓其在Go程序中可以C.add進(jìn)行訪問(wèn)。我們還可以調(diào)用注釋代碼塊中通過(guò)頭文件導(dǎo)入的庫(kù)中的函數(shù)或全局變量,可以看到調(diào)用了main(通過(guò)math.h導(dǎo)入)中的C.sqrt或是C.multiply(通過(guò)mylib.h導(dǎo)入)。
除了在注釋塊中出現(xiàn)(或?qū)氲模┑臉?biāo)識(shí)名,偽包C還定義了類(lèi)型C.int和C.char用于表示內(nèi)置的C語(yǔ)言類(lèi)型和函數(shù),如C.CString將Go字符串轉(zhuǎn)化為C字符串。
我們可以用更多魔法來(lái)在C函數(shù)中調(diào)用Go函數(shù)??赏ㄟ^(guò)在函數(shù)前添加//export注釋來(lái)將Go函數(shù)暴露給C代碼:
//export doubler
func doubler(i int) int {
return i * 2
}
如果這么做,就不能在import "C"語(yǔ)句前的注釋中直接聲明C代碼了。只能聲明函數(shù),而無(wú)法定義函數(shù):
/*
extern int add(int a, int b);
*/
import "C"
然后可以將C代碼放到Go代碼同目錄的 .c文件中并包含魔法頭文件"_cgo_export.h":
#include "_cgo_export.h"
int add(int a, int b) {
int doubleA = doubler(a);
int sum = doubleA + b;
return sum;
}
至此,一切都看起來(lái)很簡(jiǎn)單,但使用cgo還有一個(gè)絆腳石:Go是一種帶垃圾回收的語(yǔ)言,而C卻不是。這樣大型Go代碼與C集成會(huì)很困難。雖然可以向C代碼傳遞指針。這很局限性,因?yàn)樽址?、切片和函?shù)通過(guò)指針實(shí)現(xiàn),因此包含在傳遞給C函數(shù)的結(jié)構(gòu)體中。不止如此:C函數(shù)無(wú)法存儲(chǔ)函數(shù)返回后仍存在的Go指針的副本。如果違反規(guī)則,程序會(huì)編譯并運(yùn)行,但在指針指向的內(nèi)存被回收時(shí)運(yùn)行時(shí)可能會(huì)崩潰或出錯(cuò)。
還有其它的限制。例如,無(wú)法使用cgo調(diào)用C的宏函數(shù)(如printf)。C的共同體類(lèi)型會(huì)轉(zhuǎn)換成字節(jié)數(shù)組。并且無(wú)法調(diào)用C函數(shù)指針(但如果將其賦值給Go變量再傳回C函數(shù)則沒(méi)有問(wèn)題)。
這些規(guī)則使得cgo的使用頗費(fèi)周折。如果讀者有Python或Ruby背景的話,可能覺(jué)得出于性能原因cgo是有價(jià)值的。這些開(kāi)發(fā)者用C編寫(xiě)重性能的部分。NumPy的快速就是因?yàn)镻ython代碼所封裝的C庫(kù)。
大部分情況下,Go代碼比Python或Ruby快數(shù)倍,因此使用更低級(jí)代碼重寫(xiě)算法的需要大幅減少。你可能覺(jué)得將cgo保留到那些需要有性能收益的地方,但不幸的是,很難讓使用cgo的代碼更快速。因?yàn)樘幚砗蛢?nèi)存模型的不一致,通過(guò)Go調(diào)用C函數(shù)要比C自身調(diào)用慢大約29倍。在CapitalGo 2018上,F(xiàn)ilippo Valsorda做了一個(gè)演講,名為Why cgo is slow??上г撗葜v沒(méi)有錄制下來(lái),但可查看其幻燈片。其中解釋了為什么cgo很慢并且為什么未來(lái)不會(huì)明顯地提速。
因?yàn)?code>cgo并不快,并且在大型程序中也不易使用,使用cgo的唯一原因是有一個(gè)C必須要用,而Go語(yǔ)言又沒(méi)有合適的替代程序。除了自己編寫(xiě)cgo代碼,可以看看有沒(méi)有第三方模塊已經(jīng)提供了這種封裝。例如,如果希望在Go應(yīng)用中嵌入SQLite,請(qǐng)見(jiàn)GitHub。而對(duì)于ImageMagick,查看這個(gè)代碼倉(cāng)庫(kù)。
如果發(fā)現(xiàn)需要使用內(nèi)部的C庫(kù)或是找不到封裝的第三方庫(kù),可以參見(jiàn)Go官方文檔查看編寫(xiě)集成代碼的更多詳情。對(duì)于有關(guān)使用cgo時(shí)會(huì)遇到的性能問(wèn)題以及設(shè)計(jì)權(quán)衡,可以閱讀Tobias Griege博客中的文章The Cost and Complexity of Cgo。
小結(jié)
本文中,我們講解了反射、unsafe和cgo。這些特性可能是Go中最令人激動(dòng)的部分,因?yàn)樗鼈冊(cè)试S打破無(wú)聊Go作為類(lèi)型安全、內(nèi)存安全語(yǔ)言所設(shè)定的條條框框。更重要的是,我們學(xué)習(xí)到了為什么要打破這些規(guī)則以及為何在大部分時(shí)間避免這么做。