实战演示 Go 反射的使用方法和应用场景
今天來(lái)聊一個(gè)平時(shí)用的不多,但是很多框架或者基礎(chǔ)庫(kù)會(huì)用到的語(yǔ)言特性--反射,反射并不是Go語(yǔ)言獨(dú)有的能力,其他編程語(yǔ)言都有。這篇文章的目標(biāo)是簡(jiǎn)單地給大家梳理一下反射的應(yīng)用場(chǎng)景和使用方法。
我們平時(shí)寫(xiě)代碼能接觸到與反射聯(lián)系比較緊密的一個(gè)東西是結(jié)構(gòu)體字段的標(biāo)簽,這個(gè)我準(zhǔn)備放在后面的文章再梳理。
我準(zhǔn)備通過(guò)用反射搞一個(gè)通用的SQL構(gòu)造器的例子,帶大家掌握反射這個(gè)知識(shí)點(diǎn)。這個(gè)是看了國(guó)外一個(gè)博主寫(xiě)的例子,覺(jué)得思路很好,我又對(duì)其進(jìn)行了改進(jìn),讓構(gòu)造器的實(shí)現(xiàn)更豐富了些。
本文的思路參考自:https://golangbot.com/reflection/ ,本文內(nèi)容并非只是對(duì)原文的簡(jiǎn)單翻譯,具體看下面的內(nèi)容吧~!
什么是反射
反射是程序在運(yùn)行時(shí)檢查其變量和值并找到它們類(lèi)型的能力。聽(tīng)起來(lái)比較籠統(tǒng),接下來(lái)我通過(guò)文章的例子一步步帶你認(rèn)識(shí)反射。
為什么需要反射
當(dāng)學(xué)習(xí)反射的時(shí)候,每個(gè)人首先會(huì)想到的問(wèn)題都是 “為什么我們要在運(yùn)行時(shí)檢查變量的類(lèi)型呢,程序里的變量在定義的時(shí)候我們不都已經(jīng)給他們指定好類(lèi)型了嗎?” 確實(shí)是這樣的,但也并非總是如此,看到這你可能心里會(huì)想,大哥,你在說(shuō)什么呢,em... 還是先寫(xiě)一個(gè)簡(jiǎn)單的程序,解釋一下。
package?mainimport?(??"fmt" )func?main()?{??i?:=?10fmt.Printf("%d?%T",?i,?i) }在上面的程序里, 變量i的類(lèi)型在編譯時(shí)是已知的,我們?cè)谙乱恍写蛴×怂闹岛皖?lèi)型。
現(xiàn)在讓我們理解一下 ”在運(yùn)行時(shí)知道變量的類(lèi)型的必要“。假設(shè)我們要編寫(xiě)一個(gè)簡(jiǎn)單的函數(shù),它將一個(gè)結(jié)構(gòu)體作為參數(shù),并使用這個(gè)參數(shù)創(chuàng)建一個(gè)SQL插入語(yǔ)句。
考慮一下下面這個(gè)程序
package?mainimport?(??"fmt" )type?order?struct?{??ordId??????intcustomerId?int }func?main()?{??o?:=?order{ordId:??????1234,customerId:?567,}fmt.Println(o) }我們需要寫(xiě)一個(gè)接收上面定義的結(jié)構(gòu)體o作為參數(shù),返回類(lèi)似INSERT INTO order VALUES(1234, 567)這樣的SQL語(yǔ)句。這個(gè)函數(shù)定義寫(xiě)來(lái)很容易,比如像下面這樣。
package?mainimport?(??"fmt" )type?order?struct?{??ordId??????intcustomerId?int }func?createQuery(o?order)?string?{??i?:=?fmt.Sprintf("INSERT?INTO?order?VALUES(%d,?%d)",?o.ordId,?o.customerId)return?i }func?main()?{??o?:=?order{ordId:??????1234,customerId:?567,}fmt.Println(createQuery(o)) }上面例子的createQuery使用參數(shù)o 的ordId和customerId字段創(chuàng)建SQL。
現(xiàn)在讓我們將我們的SQL創(chuàng)建函數(shù)定義地更抽象些,下面還是用程序附帶說(shuō)明舉一個(gè)案例,比如我們想泛化我們的SQL創(chuàng)建函數(shù)使其適用于任何結(jié)構(gòu)體。
package?maintype?order?struct?{??ordId??????intcustomerId?int }type?employee?struct?{??name?stringid?intaddress?stringsalary?intcountry?string }func?createQuery(q?interface{})?string?{?? }現(xiàn)在我們的目標(biāo)是,改造createQuery函數(shù),讓它能接受任何結(jié)構(gòu)作為參數(shù)并基于結(jié)構(gòu)字段創(chuàng)建INSERT 語(yǔ)句。比如如果傳給createQuery的參數(shù)不再是order類(lèi)型的結(jié)構(gòu)體,而是employee類(lèi)型的結(jié)構(gòu)體時(shí)
e?:=?employee?{name:?"Naveen",id:?565,address:?"Science?Park?Road,?Singapore",salary:?90000,country:?"Singapore",}那它應(yīng)該返回的INSERT語(yǔ)句應(yīng)該是
INSERT?INTO?employee?(name,?id,?address,?salary,?country)? VALUES("Naveen",?565,?"Science?Park?Road,?Singapore",?90000,?"Singapore")由于createQuery 函數(shù)要適用于任何結(jié)構(gòu)體,因此它需要一個(gè) interface{}類(lèi)型的參數(shù)。為了說(shuō)明問(wèn)題,簡(jiǎn)單起見(jiàn),我們假定createQuery函數(shù)只處理包含string 和 int 類(lèi)型字段的結(jié)構(gòu)體。
編寫(xiě)這個(gè)createQuery函數(shù)的唯一方法是檢查在運(yùn)行時(shí)傳遞給它的參數(shù)的類(lèi)型,找到它的字段,然后創(chuàng)建SQL。這里就是需要反射發(fā)揮用的地方啦。在后續(xù)步驟中,我們將學(xué)習(xí)如何使用Go語(yǔ)言的反射包來(lái)實(shí)現(xiàn)這一點(diǎn)。
Go語(yǔ)言的反射包
Go語(yǔ)言自帶的reflect包實(shí)現(xiàn)了在運(yùn)行時(shí)進(jìn)行反射的功能,這個(gè)包可以幫助識(shí)別一個(gè)interface{}類(lèi)型變量其底層的具體類(lèi)型和值。我們的createQuery函數(shù)接收到一個(gè)interface{}類(lèi)型的實(shí)參后,需要根據(jù)這個(gè)實(shí)參的底層類(lèi)型和值去創(chuàng)建并返回INSERT語(yǔ)句,這正是反射包的作用所在。
在開(kāi)始編寫(xiě)我們的通用SQL生成器函數(shù)之前,我們需要先了解一下reflect包中我們會(huì)用到的幾個(gè)類(lèi)型和方法,接下來(lái)我們先逐個(gè)學(xué)習(xí)一下。
reflect.Type 和 reflect.Value
經(jīng)過(guò)反射后interface{}類(lèi)型的變量的底層具體類(lèi)型由reflect.Type表示,底層值由reflect.Value表示。reflect包里有兩個(gè)函數(shù)reflect.TypeOf() 和reflect.ValueOf() 分別能將interface{}類(lèi)型的變量轉(zhuǎn)換為reflect.Type和reflect.Value。這兩種類(lèi)型是創(chuàng)建我們的SQL生成器函數(shù)的基礎(chǔ)。
讓我們寫(xiě)一個(gè)簡(jiǎn)單的例子來(lái)理解這兩種類(lèi)型。
package?mainimport?(??"fmt""reflect" )type?order?struct?{??ordId??????intcustomerId?int }func?createQuery(q?interface{})?{??t?:=?reflect.TypeOf(q)v?:=?reflect.ValueOf(q)fmt.Println("Type?",?t)fmt.Println("Value?",?v)} func?main()?{??o?:=?order{ordId:??????456,customerId:?56,}createQuery(o)}上面的程序會(huì)輸出:
Type??main.order?? Value??{456?56}上面的程序里createQuery函數(shù)接收一個(gè)interface{}類(lèi)型的實(shí)參,然后把實(shí)參傳給了reflect.Typeof和reflect.Valueof 函數(shù)的調(diào)用。從輸出,我們可以看到程序輸出了interface{}類(lèi)型實(shí)參對(duì)應(yīng)的底層具體類(lèi)型和值。
Go語(yǔ)言反射的三法則
這里插播一下反射的三法則,他們是:
從接口值可以反射出反射對(duì)象。
從反射對(duì)象可反射出接口值。
要修改反射對(duì)象,其值必須可設(shè)置。
反射的第一條法則是,我們能夠吧Go中的接口類(lèi)型變量轉(zhuǎn)換成反射對(duì)象,上面提到的reflect.TypeOf和 reflect.ValueOf 就是完成的這種轉(zhuǎn)換。第二條指的是我們能把反射類(lèi)型的變量再轉(zhuǎn)換回到接口類(lèi)型,最后一條則是與反射值是否可以被更改有關(guān)。三法則詳細(xì)的說(shuō)明可以去看看德萊文大神寫(xiě)的文章 Go反射的實(shí)現(xiàn)原理
下面我們接著繼續(xù)了解完成我們的SQL生成器需要的反射知識(shí)。
reflect.Kind
reflect包中還有一個(gè)非常重要的類(lèi)型,reflect.Kind。
reflect.Kind和reflect.Type類(lèi)型可能看起來(lái)很相似,從命名上也是,Kind和Type在英文的一些Phrase是可以互轉(zhuǎn)使用的,不過(guò)在反射這塊它們有挺大區(qū)別,從下面的程序中可以清楚地看到。
package?main import?(??"fmt""reflect" )type?order?struct?{??ordId??????intcustomerId?int }func?createQuery(q?interface{})?{??t?:=?reflect.TypeOf(q)k?:=?t.Kind()fmt.Println("Type?",?t)fmt.Println("Kind?",?k)} func?main()?{??o?:=?order{ordId:??????456,customerId:?56,}createQuery(o)}上面的程序會(huì)輸出
Type??main.order?? Kind??struct通過(guò)輸出讓我們清楚了兩者之間的區(qū)別。reflect.Type 表示接口的實(shí)際類(lèi)型,即本例中main.order 而Kind表示類(lèi)型的所屬的種類(lèi),即main.order是一個(gè)「struct」類(lèi)型,類(lèi)似的類(lèi)型map[string]string的Kind就該是「map」。
反射獲取結(jié)構(gòu)體字段的方法
我們可以通過(guò)reflect.StructField類(lèi)型的方法來(lái)獲取結(jié)構(gòu)體下字段的類(lèi)型屬性。reflect.StructField可以通過(guò)reflect.Type提供的下面兩種方式拿到。
//?獲取一個(gè)結(jié)構(gòu)體內(nèi)的字段數(shù)量 NumField()?int //?根據(jù)?index?獲取結(jié)構(gòu)體內(nèi)字段的類(lèi)型對(duì)象 Field(i?int)?StructField //?根據(jù)字段名獲取結(jié)構(gòu)體內(nèi)字段的類(lèi)型對(duì)象 FieldByName(name?string)?(StructField,?bool)reflect.structField是一個(gè)struct類(lèi)型,通過(guò)它我們又能在反射里知道字段的基本類(lèi)型、Tag、是否已導(dǎo)出等屬性。
type?StructField?struct?{Name?stringType??????Type??????//?field?typeTag???????StructTag?//?field?tag?string...... }與reflect.Type提供的獲取Field信息的方法相對(duì)應(yīng),reflect.Value也提供了獲取Field值的方法。
func?(v?Value)?Field(i?int)?Value?{ ... }func?(v?Value)?FieldByName(name?string)?Value?{ ... }這塊需要注意,不然容易迷惑。下面我們嘗試一下通過(guò)反射拿到order結(jié)構(gòu)體類(lèi)型的字段名和值
package?mainimport?("fmt""reflect" )type?order?struct?{ordId??????intcustomerId?int }func?createQuery(q?interface{})?{t?:=?reflect.TypeOf(q)if?t.Kind()?!=?reflect.Struct?{panic("unsupported?argument?type!")}v?:=?reflect.ValueOf(q)for?i:=0;?i?<?t.NumField();?i++?{fmt.Println("FieldName:",?t.Field(i).Name,?"FiledType:",?t.Field(i).Type,"FiledValue:",?v.Field(i))}} func?main()?{o?:=?order{ordId:??????456,customerId:?56,}createQuery(o)}上面的程序會(huì)輸出:
FieldName:?ordId?FiledType:?int?FiledValue:?456 FieldName:?customerId?FiledType:?int?FiledValue:?56除了獲取結(jié)構(gòu)體字段名稱(chēng)和值之外,還能獲取結(jié)構(gòu)體字段的Tag,這個(gè)放在后面的文章我再總結(jié)吧,不然篇幅就太長(zhǎng)了。
reflect.Value轉(zhuǎn)換成實(shí)際值
現(xiàn)在離完成我們的SQL生成器還差最后一步,即還需要把reflect.Value轉(zhuǎn)換成實(shí)際類(lèi)型的值,reflect.Value實(shí)現(xiàn)了一系列Int(), String(),Float()這樣的方法來(lái)完成其到實(shí)際類(lèi)型值的轉(zhuǎn)換。
用反射搞一個(gè)SQL生成器
上面我們已經(jīng)了解完寫(xiě)這個(gè)SQL生成器函數(shù)前所有的必備知識(shí)點(diǎn)啦,接下來(lái)就把他們串起來(lái),加工完成createQuery函數(shù)。
這個(gè)SQL生成器完整的實(shí)現(xiàn)和測(cè)試代碼如下:
package?mainimport?("fmt""reflect" )type?order?struct?{ordId??????intcustomerId?int }type?employee?struct?{name????stringid??????intaddress?stringsalary??intcountry?string }func?createQuery(q?interface{})?string?{t?:=?reflect.TypeOf(q)v?:=?reflect.ValueOf(q)if?v.Kind()?!=?reflect.Struct?{panic("unsupported?argument?type!")}tableName?:=?t.Name()?//?通過(guò)結(jié)構(gòu)體類(lèi)型提取出SQL的表名sql?:=?fmt.Sprintf("INSERT?INTO?%s?",?tableName)columns?:=?"("values?:=?"VALUES?("for?i?:=?0;?i?<?v.NumField();?i++?{//?注意reflect.Value?也實(shí)現(xiàn)了NumField,Kind這些方法//?這里的v.Field(i).Kind()等價(jià)于t.Field(i).Type.Kind()switch?v.Field(i).Kind()?{case?reflect.Int:if?i?==?0?{columns?+=?fmt.Sprintf("%s",?t.Field(i).Name)values?+=?fmt.Sprintf("%d",?v.Field(i).Int())}?else?{columns?+=?fmt.Sprintf(",?%s",?t.Field(i).Name)values?+=?fmt.Sprintf(",?%d",?v.Field(i).Int())}case?reflect.String:if?i?==?0?{columns?+=?fmt.Sprintf("%s",?t.Field(i).Name)values?+=?fmt.Sprintf("'%s'",?v.Field(i).String())}?else?{columns?+=?fmt.Sprintf(",?%s",?t.Field(i).Name)values?+=?fmt.Sprintf(",?'%s'",?v.Field(i).String())}}}columns?+=?");?"values?+=?");?"sql?+=?columns?+?valuesfmt.Println(sql)return?sql }func?main()?{o?:=?order{ordId:??????456,customerId:?56,}createQuery(o)e?:=?employee{name:????"Naveen",id:??????565,address:?"Coimbatore",salary:??90000,country:?"India",}createQuery(e) }同學(xué)們可以把代碼拿到本地運(yùn)行一下,上面的例子會(huì)根據(jù)傳遞給函數(shù)不同的結(jié)構(gòu)體實(shí)參,輸出對(duì)應(yīng)的標(biāo)準(zhǔn)SQL插入語(yǔ)句
INSERT?INTO?order?(ordId,?customerId);?VALUES?(456,?56);? INSERT?INTO?employee?(name,?id,?address,?salary,?country);?VALUES?('Naveen',?565,?'Coimbatore',?90000,?'India');總結(jié)
這篇文章通過(guò)利用反射完成一個(gè)實(shí)際應(yīng)用來(lái)教會(huì)大家Go語(yǔ)言反射的基本使用方法,雖然反射看起來(lái)挺強(qiáng)大,但使用反射編寫(xiě)清晰且可維護(hù)的代碼非常困難,應(yīng)盡可能避免,僅在絕對(duì)必要時(shí)才使用。
我的看法是如果是要寫(xiě)業(yè)務(wù)代碼,根本不需要使用反射,如果要寫(xiě)類(lèi)似encoding/json,gorm這些樣的庫(kù)倒是可以利用反射的強(qiáng)大功能簡(jiǎn)化庫(kù)使用者的編碼難度。
- END -
掃碼關(guān)注公眾號(hào)「網(wǎng)管叨bi叨」
給網(wǎng)管個(gè)星標(biāo),第一時(shí)間吸我的知識(shí)?👆
網(wǎng)管為大家整理了一本超實(shí)用的《Go 開(kāi)發(fā)參考書(shū)》收集了70多條開(kāi)發(fā)實(shí)踐。去公眾號(hào)回復(fù)【gocookbook】即刻領(lǐng)取!
覺(jué)得有用就點(diǎn)個(gè)在看? 👇👇👇
超強(qiáng)干貨來(lái)襲 云風(fēng)專(zhuān)訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的实战演示 Go 反射的使用方法和应用场景的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 简单看看 Go 1.17 的新版调用规约
- 下一篇: 搞定系统设计 03:系统设计面试的答题框