這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。反射是指一門程式設計語言可以在運行時( runtime )檢查其資料結構的能力。利用 Go 語言的反射機制,可以擷取結構體的公有欄位以及私人欄位的標籤名,甚至一些其他比較敏感的資訊。眾所周知Go標準庫中有一些包利用反射機制來實現它們的功能。我們經常會以 [encoding/json](https://golang.org/pkg/encoding/json/) 包為例,該包常用來把 JSON 文檔解析為結構體,同時也可以把結構體編碼為JSON格式的字串。本文中我想給大家介紹一個略微有點不一樣的例子,該例子是我最近在做的一個聊天項目的訊息體,該訊息體使用結構體來表示:```gotype Message struct {ID uint64 `db:"id"`Channel string `db:"channel"`UserName string `db:"user_name"`UserID string `db:"user_id"`UserAvatar string `db:"user_avatar"`Message string `db:"message"`RawMessage string `db:"message_raw"`MessageID string `db:"message_id"`Stamp time.Time `db:"stamp"`}``` 我想使用 命名 SQL 查詢語句把該條訊息寫入到資料庫,正常情況,我應該寫一條類似於下面的 SQL 查詢語句:> insert into messages set id=:id, channel=:channel,...然後使用 `jmoiron.sqlx` 包執行一次 db.NamedExec(query,message) 語句。很顯然,隨著結構體數量的增加,大量的時間將會浪費在寫這些查詢語句上面,更糟糕的是,一旦資料庫表結構發生變更的話,程式很可能會報錯。試想一下,如果我們能夠根據傳過來的結構體,自動產生查詢語句,會不會是一件很爽的事情?的確我們可以通過引用 reflect 包來達到我們的上述需求。接下來我將會帶著大家一起來領略一下整個實現過程,目前有很多 ORM 包也是使用了跟我類似的方法來達到相同的目的。## 把結構體轉為 reflect.value 類型我們需要先建立一個 `reflect.Value` 的執行個體,以便於能夠擷取結構體的欄位。同時我們也可以從該執行個體中擷取結構體的函數。建立一個 reflect.Value 執行個體非常直接:```gomessage_value := reflect.ValueOf(message)```我們需要調用 message_value.NumField() 函數來擷取結構體中欄位的總數以便於迭代結構體的所有欄位。如果我們試圖調用 NumField() 的時候傳一個 reflect.ValueOf 返回的指標值,程式會產生 panic 錯誤:```gopanic:reflect:call of reflect.Value.NumField on ptr Value```為瞭解決上面這個問題,我們使用 message_value.Kind() 來檢查是否是一個指標值,然後得到指標指向的實際的值:```goif message_value.Kind() == reflect.Ptr{message_value = message_value.Elem()}```然後我們再調用 message_value.NumField() 就會正確的輸出結構體欄位的總數。接下來我們將會使用這個值通過迴圈迭代的方式來擷取所有欄位的名稱和對應的欄位值。## 讀取欄位詳情從結構體欄位中我們可以擷取很多重要的資訊,不過我們最感興趣的還是想要擷取欄位聲明中的標籤資訊。由於 `reflect.Value` 是用來處理結構體中每個欄位實際儲存的值,所以我們需要用 reflect.Type 來擷取欄位的名稱(比如 UserName )或者關聯的標籤名。假如我們想要擷取結構體所有欄位的詳細資料列表,詳細資料包含欄位名稱,含有“db”的欄位關聯的標籤名以及欄位實際儲存的值,代碼可以這樣寫:```gomessage_fields := make([]struct {Name stringTag stringValue interface{}}, message_value.NumField())for i := 0; i < len(message_fileds); i++ {fieldValue := message_value.Field(i)fieldType := message_value.Type().Field(i)message_fields[i].Name = fieldType.Namemessage_fields[i].Value = fieldsValue.Interface()message_fields[i].Tag = fieldType.Tag.Get("db")}```上述代碼可以看出每個欄位的實際值通過 reflect.Value.Interface() 擷取,欄位的名稱和欄位的標籤名通過 reflect.Type 擷取。你可以在 [go playground](https://play.golang.org/p/Bu0J-jlsLB7) 上跑一下上述完整的樣本。## 組合一下功能其實上述的功能已經完全滿足我們的需求了,自動產生 SQL 查詢語句的關鍵點在於使用代碼來完成欄位的標籤名的拼接,如下代碼所示:```gofunc insert(table string, data interface{})string{message_value := reflect.ValueOf(data)if message_value.Kind() == reflect.Ptr{message_value = message_value.Elem()}message_fields := make([]string, message_value.NumField())for i := 0;i<len(message_fields);i++{fieldType := message_value.Type().Field(i)message_fields[i] = fieldType.Tag.Get("db")}sql := "insert into" + table + " set"for _,tagFull := range message_fields{if tagFull != "" && tagFull != "-"{tag := strings.Split(tagFull,",")sql = sql + " "+ tag[0]+"=:"+tag[0]+","}}return sql[:len(sql)-1]}```最終版的代碼在這裡[go playground code](https://play.golang.org/p/KcuTIWa3S1F)這裡還有一些注意事項需要說明一下:* 在我們的例子中,由於我們沒有深入對結構體進行解析,所以不論結構體的欄位是否是指標類型都無關緊要。同時 reflect.Type 資訊的擷取與欄位的實際值也無關。* 如果你需要繼續對結構體進行解析,一定要注意對指標類型的值進行特殊處理,就像我們前面對 message_value 一樣,通過 Elem() 來擷取指標值。* 還有一些其他非常優秀的包提供了反射機制,如果你想要閱讀真實的樣本,可以看一下[codegangsta/inject](https://github.com/codegangsta/inject)和[fatih/structs](https://github.com/fatih/structs)這兩個包。* 推薦大家閱讀 [The Laws Of Reflection, by Rob Pike](https://blog.golang.org/laws-of-reflection)最後提醒一下:如果你之前是一個 PHP 或者 Javascript 程式員,你應該知道在這些語言中缺乏型別安全監測。你可能希望通過反射機制來解決你遇到的一些問題。但是,如果你選擇使用 Go 來解決在之前這些語言中遇到的一些使用反射機制解決的問題,我估計你會很痛苦,甚至可能會放棄。從我以往使用 Go 案例的曆史來看,使用反射機制總是磕磕絆絆的。即便是本文中的案例,也是有些牽強的。你完全可以不用使用反射機制,使用字串切片或者查詢語句本身也可以達到上述的目的。如果說你寫的 API 介面完全可以滿足你的需求,就盡量避免使用反射機制。案例和觀點:即便對於 JSON ,也有一個 [json-iterator/go](https://github.com/json-iterator/go) 包用來代替標準庫 `encoding/json` 包。在該包中,大大減少了對反射機制的依賴,速度也得到了顯著的提升。反射機制重構的可能性不太大,建議大家使用 gRPC 和 Protobuf 機制。在這個機制中,型別安全可以在代碼編寫的過程中得到保障。
via: https://scene-si.org/2017/12/21/introduction-to-reflection/
作者:Tit Petric 譯者:yzhfd 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
650 次點擊