<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          牛x!搞明白 Go 反射的使用方法和應(yīng)用場景

          共 5185字,需瀏覽 11分鐘

           ·

          2021-11-06 11:59

          今天來聊一個平時用的不多,但是很多框架或者基礎(chǔ)庫會用到的語言特性--反射,反射并不是Go語言獨(dú)有的能力,其他編程語言都有。這篇文章的目標(biāo)是簡單地給大家梳理一下反射的應(yīng)用場景和使用方法。

          我們平時寫代碼能接觸到與反射聯(lián)系比較緊密的一個東西是結(jié)構(gòu)體字段的標(biāo)簽,這個我準(zhǔn)備放在后面的文章再梳理。

          我準(zhǔn)備通過用反射搞一個通用的SQL構(gòu)造器的例子,帶大家掌握反射這個知識點(diǎn)。這個是看了國外一個博主寫的例子,覺得思路很好,我又對其進(jìn)行了改進(jìn),讓構(gòu)造器的實(shí)現(xiàn)更豐富了些。

          本文的思路參考自:https://golangbot.com/reflection/ ,本文內(nèi)容并非只是對原文的簡單翻譯,具體看下面的內(nèi)容吧~!

          什么是反射

          反射是程序在運(yùn)行時檢查其變量和值并找到它們類型的能力。聽起來比較籠統(tǒng),接下來我通過文章的例子一步步帶你認(rèn)識反射。

          為什么需要反射

          當(dāng)學(xué)習(xí)反射的時候,每個人首先會想到的問題都是 “為什么我們要在運(yùn)行時檢查變量的類型呢,程序里的變量在定義的時候我們不都已經(jīng)給他們指定好類型了嗎?” 確實(shí)是這樣的,但也并非總是如此,看到這你可能心里會想,大哥,你在說什么呢,em... 還是先寫一個簡單的程序,解釋一下。

          package?main

          import?(??
          ????"fmt"
          )

          func?main()?{??
          ????i?:=?10
          ????fmt.Printf("%d?%T",?i,?i)
          }

          在上面的程序里, 變量i的類型在編譯時是已知的,我們在下一行打印了它的值和類型。

          現(xiàn)在讓我們理解一下 ”在運(yùn)行時知道變量的類型的必要“。假設(shè)我們要編寫一個簡單的函數(shù),它將一個結(jié)構(gòu)體作為參數(shù),并使用這個參數(shù)創(chuàng)建一個SQL插入語句。

          考慮一下下面這個程序

          package?main

          import?(??
          ????"fmt"
          )

          type?order?struct?{??
          ????ordId??????int
          ????customerId?int
          }

          func?main()?{??
          ????o?:=?order{
          ????????ordId:??????1234,
          ????????customerId:?567,
          ????}
          ????fmt.Println(o)
          }

          我們需要寫一個接收上面定義的結(jié)構(gòu)體o作為參數(shù),返回類似INSERT INTO order VALUES(1234, 567)這樣的SQL語句。這個函數(shù)定義寫來很容易,比如像下面這樣。

          package?main

          import?(??
          ????"fmt"
          )

          type?order?struct?{??
          ????ordId??????int
          ????customerId?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ù)oordIdcustomerId字段創(chuàng)建SQL。

          現(xiàn)在讓我們將我們的SQL創(chuàng)建函數(shù)定義地更抽象些,下面還是用程序附帶說明舉一個案例,比如我們想泛化我們的SQL創(chuàng)建函數(shù)使其適用于任何結(jié)構(gòu)體。

          package?main

          type?order?struct?{??
          ????ordId??????int
          ????customerId?int
          }

          type?employee?struct?{??
          ????name?string
          ????id?int
          ????address?string
          ????salary?int
          ????country?string
          }

          func?createQuery(q?interface{})?string?{??
          }

          現(xiàn)在我們的目標(biāo)是,改造createQuery函數(shù),讓它能接受任何結(jié)構(gòu)作為參數(shù)并基于結(jié)構(gòu)字段創(chuàng)建INSERT 語句。比如如果傳給createQuery的參數(shù)不再是order類型的結(jié)構(gòu)體,而是employee類型的結(jié)構(gòu)體時

          ?e?:=?employee?{
          ????????name:?"Naveen",
          ????????id:?565,
          ????????address:?"Science?Park?Road,?Singapore",
          ????????salary:?90000,
          ????????country:?"Singapore",
          ????}

          那它應(yīng)該返回的INSERT語句應(yīng)該是

          INSERT?INTO?employee?(name,?id,?address,?salary,?country)?
          VALUES("Naveen",?565,?"Science?Park?Road,?Singapore",?90000,?"Singapore")??

          由于createQuery 函數(shù)要適用于任何結(jié)構(gòu)體,因此它需要一個 interface{}類型的參數(shù)。為了說明問題,簡單起見,我們假定createQuery函數(shù)只處理包含stringint 類型字段的結(jié)構(gòu)體。

          編寫這個createQuery函數(shù)的唯一方法是檢查在運(yùn)行時傳遞給它的參數(shù)的類型,找到它的字段,然后創(chuàng)建SQL。這里就是需要反射發(fā)揮用的地方啦。在后續(xù)步驟中,我們將學(xué)習(xí)如何使用Go語言的反射包來實(shí)現(xiàn)這一點(diǎn)。

          Go語言的反射包

          Go語言自帶的reflect包實(shí)現(xiàn)了在運(yùn)行時進(jìn)行反射的功能,這個包可以幫助識別一個interface{}類型變量其底層的具體類型和值。我們的createQuery函數(shù)接收到一個interface{}類型的實(shí)參后,需要根據(jù)這個實(shí)參的底層類型和值去創(chuàng)建并返回INSERT語句,這正是反射包的作用所在。

          在開始編寫我們的通用SQL生成器函數(shù)之前,我們需要先了解一下reflect包中我們會用到的幾個類型和方法,接下來我們先逐個學(xué)習(xí)一下。

          reflect.Type 和 reflect.Value

          經(jīng)過反射后interface{}類型的變量的底層具體類型由reflect.Type表示,底層值由reflect.Value表示。reflect包里有兩個函數(shù)reflect.TypeOf()reflect.ValueOf() 分別能將interface{}類型的變量轉(zhuǎn)換為reflect.Typereflect.Value。這兩種類型是創(chuàng)建我們的SQL生成器函數(shù)的基礎(chǔ)。

          讓我們寫一個簡單的例子來理解這兩種類型。

          package?main

          import?(??
          ????"fmt"
          ????"reflect"
          )

          type?order?struct?{??
          ????ordId??????int
          ????customerId?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)

          }

          上面的程序會輸出:

          Type??main.order??
          Value??{456?56}?

          上面的程序里createQuery函數(shù)接收一個interface{}類型的實(shí)參,然后把實(shí)參傳給了reflect.Typeofreflect.Valueof 函數(shù)的調(diào)用。從輸出,我們可以看到程序輸出了interface{}類型實(shí)參對應(yīng)的底層具體類型和值。

          Go語言反射的三法則

          這里插播一下反射的三法則,他們是:

          1. 從接口值可以反射出反射對象。
          2. 從反射對象可反射出接口值。
          3. 要修改反射對象,其值必須可設(shè)置。

          反射的第一條法則是,我們能夠吧Go中的接口類型變量轉(zhuǎn)換成反射對象,上面提到的reflect.TypeOfreflect.ValueOf 就是完成的這種轉(zhuǎn)換。第二條指的是我們能把反射類型的變量再轉(zhuǎn)換回到接口類型,最后一條則是與反射值是否可以被更改有關(guān)。三法則詳細(xì)的說明可以去看看德萊文大神寫的文章 Go反射的實(shí)現(xiàn)原理,文章開頭就有對三法則說明的圖解,再次膜拜。

          下面我們接著繼續(xù)了解完成我們的SQL生成器需要的反射知識。

          reflect.Kind

          reflect包中還有一個非常重要的類型,reflect.Kind。

          reflect.Kindreflect.Type類型可能看起來很相似,從命名上也是,Kind和Type在英文的一些Phrase是可以互轉(zhuǎn)使用的,不過在反射這塊它們有挺大區(qū)別,從下面的程序中可以清楚地看到。

          package?main
          import?(??
          ????"fmt"
          ????"reflect"
          )

          type?order?struct?{??
          ????ordId??????int
          ????customerId?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)

          }

          上面的程序會輸出

          Type??main.order??
          Kind??struct??

          通過輸出讓我們清楚了兩者之間的區(qū)別。reflect.Type 表示接口的實(shí)際類型,即本例中main.orderKind表示類型的所屬的種類,即main.order是一個「struct」類型,類似的類型map[string]string的Kind就該是「map」。

          反射獲取結(jié)構(gòu)體字段的方法

          我們可以通過reflect.StructField類型的方法來獲取結(jié)構(gòu)體下字段的類型屬性。reflect.StructField可以通過reflect.Type提供的下面兩種方式拿到。

          //?獲取一個結(jié)構(gòu)體內(nèi)的字段數(shù)量
          NumField()?int
          //?根據(jù)?index?獲取結(jié)構(gòu)體內(nèi)字段的類型對象
          Field(i?int)?StructField
          //?根據(jù)字段名獲取結(jié)構(gòu)體內(nèi)字段的類型對象
          FieldByName(name?string)?(StructField,?bool)

          reflect.structField是一個struct類型,通過它我們又能在反射里知道字段的基本類型、Tag、是否已導(dǎo)出等屬性。

          type?StructField?struct?{
          ?Name?string
          ?Type??????Type??????//?field?type
          ?Tag???????StructTag?//?field?tag?string
          ??......
          }

          reflect.Type提供的獲取Field信息的方法相對應(yīng),reflect.Value也提供了獲取Field值的方法。

          func?(v?Value)?Field(i?int)?Value?{
          ...
          }

          func?(v?Value)?FieldByName(name?string)?Value?{
          ...
          }

          這塊需要注意,不然容易迷惑。下面我們嘗試一下通過反射拿到order結(jié)構(gòu)體類型的字段名和值

          package?main

          import?(
          ?"fmt"
          ?"reflect"
          )

          type?order?struct?{
          ?ordId??????int
          ?customerId?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???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)

          }

          上面的程序會輸出:

          FieldName:?ordId?FiledType:?int?FiledValue:?456
          FieldName:?customerId?FiledType:?int?FiledValue:?56

          除了獲取結(jié)構(gòu)體字段名稱和值之外,還能獲取結(jié)構(gòu)體字段的Tag,這個放在后面的文章我再總結(jié)吧,不然篇幅就太長了。

          reflect.Value轉(zhuǎn)換成實(shí)際值

          現(xiàn)在離完成我們的SQL生成器還差最后一步,即還需要把reflect.Value轉(zhuǎn)換成實(shí)際類型的值,reflect.Value實(shí)現(xiàn)了一系列Int(), String(),Float()這樣的方法來完成其到實(shí)際類型值的轉(zhuǎn)換。

          用反射搞一個SQL生成器

          上面我們已經(jīng)了解完寫這個SQL生成器函數(shù)前所有的必備知識點(diǎn)啦,接下來就把他們串起來,加工完成createQuery函數(shù)。

          這個SQL生成器完整的實(shí)現(xiàn)和測試代碼如下:

          package?main

          import?(
          ?"fmt"
          ?"reflect"
          )

          type?order?struct?{
          ?ordId??????int
          ?customerId?int
          }

          type?employee?struct?{
          ?name????string
          ?id??????int
          ?address?string
          ?salary??int
          ?country?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()?//?通過結(jié)構(gòu)體類型提取出SQL的表名
          ?sql?:=?fmt.Sprintf("INSERT?INTO?%s?",?tableName)
          ?columns?:=?"("
          ?values?:=?"VALUES?("
          ?for?i?:=?0;?i???//?注意reflect.Value?也實(shí)現(xiàn)了NumField,Kind這些方法
          ??//?這里的v.Field(i).Kind()等價于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?+?values
          ?fmt.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)行一下,上面的例子會根據(jù)傳遞給函數(shù)不同的結(jié)構(gòu)體實(shí)參,輸出對應(yīng)的標(biāo)準(zhǔn)SQL插入語句

          INSERT?INTO?order?(ordId,?customerId);?VALUES?(456,?56);?
          INSERT?INTO?employee?(name,?id,?address,?salary,?country);?VALUES?('Naveen',?565,?'Coimbatore',?90000,?'India');?

          總結(jié)

          這篇文章通過利用反射完成一個實(shí)際應(yīng)用來教會大家Go語言反射的基本使用方法,雖然反射看起來挺強(qiáng)大,但使用反射編寫清晰且可維護(hù)的代碼非常困難,應(yīng)盡可能避免,僅在絕對必要時才使用。

          我的看法是如果是要寫業(yè)務(wù)代碼,根本不需要使用反射,如果要寫類似encoding/jsongorm這些樣的庫倒是可以利用反射的強(qiáng)大功能簡化庫使用者的編碼難度。


          ?最后,再打個廣告吧。

          雙十一快到了,阿里云也開始搞活動了,剛好我這邊可以帶大家?白Piao?阿里云的服務(wù)器。

          說白了就是大家?可以一分錢不花,就可以領(lǐng)到服務(wù)器,規(guī)格是 2c2g(2vcpu 2G memory) 的機(jī)器。

          有需要的可以加我下微信,備注『服務(wù)器,我統(tǒng)一拉群,帶大家一起薅羊毛。


          瀏覽 43
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线免费观看一区 | 大香蕉国产在线看 | 黄色片视频欧美 | 78摸亚洲 | 欧美亚洲及日本黄色电影 |