php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:
雖然很多人使用 Go 語言有一定時間了,甚至有的使用了 1 年 2 年,然後對於 Go 語言中的反射還是模稜兩可,使用起來的時候,心裡也不是非常有底氣。【相關推薦:Go視訊教學、】
更有甚者,幾乎不使用反射,當然,也不是什麼錯,在工作中能用最簡單最高效,又可延伸,效能還好的方式來進行處理自然是最 nice ,沒有必要去生搬硬套一些高階用法,畢竟工作不是我們的試煉場,可以自己下來多多實驗,本次就來好好看看如何去玩反射
文章分別從如下五個方面來聊
簡單來看,反射就是在程式執行時期對程式本身進行存取和修改的能力,例如在程式執行時,可以修改程式的欄位名稱,欄位值,還可以給程式提供介面存取的資訊等等
這是 Go 語言中提供的一種機制,我們可以在 Go 語言公共庫中可以看到很多關於 reflect 的使用位置
例如常用的 fmt 包,常用的 json 序列化和反序列化,自然前面我們說到的 gorm 庫自然也是使用了反射的
可是我們一般為什麼要使用反射呢?
根據反射的能力,自然是因為我們提供的介面並不知道傳入的資料型別會是什麼樣的, 只有當程式執行的時候才知道具體的資料型別
但是我們編碼的時候又期望去校驗程式執行時傳入的型別會是什麼樣的(例如 json 的序列化)並對其這種具體的資料進行操作,這個時候,咱們就需要用到反射的能力了
所以對於使用到反射的地方,你都能看到 interface{} 是不是就不奇怪了呢?
正是因為不確定傳入的資料型別會是什麼樣的,所以才設計成 interface{} ,那麼如果對於 interface 有還不清楚他的特點和使用方式的,可以檢視歷史文章:
首先關注反射的三個重要的定律,知道規則之後,我們按照規則玩就不會有什麼問題,只有當我們不清楚規則,總是觸發條款的時候,才會出現奇奇怪怪的問題
反射是可以將 介面型別的變數 轉換成 反射型別的物件
對於上述 3 個規則也是比較好理解,還記的之前我們說過的 unsafe 包裡面的指標嗎?
都是將我們常用的資料型別,轉換成包(例如 unsafe包,或者 reflect 包)裡面的指定資料型別,然後再按照包裡面的規則進行修改資料
相當於,換個馬甲,就可以進行不同的操作了
一般咱們先會基本的應用,再去研究他的原理,研究他為什麼可以這樣用,慢慢的才能理解的更加深刻
實際上此處說的 介面型別的變數 我們可以傳入任意資料型別的變數,例如 int, float, string ,map, slice, struct
等等
反射型別的物件 這裡就可以理解成 reflect 反射包中的 reflect.Type
和 reflect.Value
物件,可以通過 reflect 包中提供的 TypeOf 和 ValueOf 函數得到
其中 reflect.Type
實際上是一個 interface ,他裡面包含了各種介面需要進行實現,它裡面提供了關於型別相關的資訊
其中如下圖可以檢視到 reflect.Type
的所有方法,其中
reflect.Value
實際上是一個 struct,根據這個 struct 還關聯了一組方法,這裡面存放了資料型別和具體的資料,通過檢視其資料結構就可以看出
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
登入後複製
看到此處的 unsafe.Pointer 是不是很熟悉,底層自然就可以將 unsafe.Pointer
轉換成 uintptr
,然後再修改其資料後,再轉換回來,對於 Go 指標不太熟悉的可以檢視這篇文章:
寫一個簡單的 demo 就可以簡單的獲取到變數的資料型別和值
func main() { var demoStr string = "now reflect"
fmt.Println("type:", reflect.TypeOf(demoStr))
fmt.Println("value:", reflect.ValueOf(demoStr))
}
登入後複製
我們可以通過將 reflect.Value
型別轉換成我們具體的資料型別,因為 reflect.Value
中有對應的 typ *rtype
以及 ptr unsafe.Pointer
例如我們可以 通過 reflect.Value
物件的 interface() 方法來處理
func main() { var demoStr string = "now reflect"
fmt.Println("type:", reflect.TypeOf(demoStr))
fmt.Println("value:", reflect.ValueOf(demoStr)) var res string
res = reflect.ValueOf(demoStr).Interface().(string)
fmt.Println("res == ",res)
}
登入後複製
首先我們看上書的 demo 程式碼,傳入 TypeOf 和 ValueOf 的變數實際上也是一個拷貝,那麼如果期望在反射型別的物件中修改其值,那麼就需要拿到具體變數的地址然後再進行修改,前提是這個變數是可寫的
舉個例子你就能明白
func main() {
var demoStr string = "now reflect"
v := reflect.ValueOf(demoStr)
fmt.Println("is canset ", v.CanSet())
//v.SetString("hello world") // 會panic
}
登入後複製
可以先呼叫 reflect.Value
物件的 CanSet
檢視是否可寫,如果是可寫的,我們再寫,如果不可寫就不要寫了,否則會 panic
那麼傳入變數的地址就可以修改了??
傳入地址的思路沒有毛病,但是我們去設定值的方式有問題,因此也會出現上述的 panic 情況
此處仔細看能夠明白,反射的物件 v 自然是不可修改的,我們應該找到 reflect.Value
裡面具體具體的資料指標,那麼才是可以修改的,可以使用 reflect.Value
的 Elem 方法
看上了上述案例可能會覺得那麼簡單的案例,一演示就 ok,但是工作中一用就崩潰,那自然還是沒有融會貫通,說明還沒有消化好,再來一個工作中的例子
type RDemo struct {
Name string
Age int
Money float32
Hobby map[string][]string
}
func main() {
tmp := &RDemo{
Name: "xiaomiong",
Age: 18,
Money: 25.6,
Hobby: map[string][]string{
"sport": {"basketball", "football"},
"food": {"beef"},
},
}
v := reflect.ValueOf(tmp).Elem() // 拿到結構體物件
h := v.FieldByName("Hobby") // 拿到 Hobby 物件
h1 := h.MapKeys()[0] // 拿到 Hobby 的第 0 個key
fmt.Println("key1 name == ",h1.Interface().(string))
sli := h.MapIndex(h1) // 拿到 Hobby 的第 0 個key對應的物件
str := sli.Index(1) // 拿到切片的第 1 個物件
fmt.Println(str.CanSet())
str.SetString("helloworld")
fmt.Println("tmp == ",tmp)
}
登入後複製
可以看到上述案例執行之後有時可以執行成功,有時會出現 panic 的情況,相信細心的 xdm 就可以看出來,是因為 map 中的 key 是 無序的導致的,此處也提醒一波,使用 map 的時候要注意這一點
看上述程式碼,是不是就能夠明白咱們使用反射去找到對應的資料型別,然後按照資料型別進行處理資料的過程了呢
有需要的話,可以慢慢的去熟練反射包中涉及的函數,重點是要了解其三個規則,物件轉換方式,存取方式,以及資料修改方式
那麼通過上述案例,可以知道關於反射中資料型別和資料指標對應的值是相當重要的,不同的資料型別能夠用哪些函數這個需要注意,否則用錯直接就會 panic
來看 TypeOf 的介面中涉及的資料結構
在 reflect 包中 rtype
是非常重要的,Go 中所有的型別都會包含這個結構,所以咱們反射可以應用起來,結構如下
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
登入後複製
其中可以看到此處的 rtype
的結構保持和 runtime/type.go
一致 ,都是關於資料型別的表示,以及對應的指標,關於這一塊的說明和演示可以檢視文末的 interface{} 處的內容
從 ValueOf 的原始碼中,我們可以看到,重要的是 emptyInterface 結構
// emptyInterface is the header for an interface{} value.type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}複製程式碼
登入後複製
emptyInterface 結構中有 rtype
型別的指標, word 自然是對應的資料的地址了
reflect.Value
物件中的方法也是非常的多,用起來和上述說到的 reflect.Type
介面中的功能類似
關於原始碼中涉及到的方法,就不再過多的贅述了,更多的還是需要自己多多實踐才能體會的更好
殊不知,此處的 reflect.Value
也是可以轉換成 reflect.Type
,可以檢視原始碼中 reflect\value.go
的 func (v Value) Type() Type {
其中 reflect.Value
,reflect.Type
,和任意資料型別
可以相互這樣來轉換
如下圖:
至此,關於反射就聊到這裡,一些關於原始碼的細節並沒有詳細說,更多的站在一個使用者的角度去看反射需要注意的點
關於反射,大多的人是建議少用,因為是會影響到效能,不過如果不太關注這一點,那麼用起來還是非常方便的
高階功能自然也是雙刃劍,你用不好就會 panic,如果你期望去使用他,那麼就去更多的深入瞭解和一步一步的吃透他吧
大道至簡,反射三定律,活學活用
更多程式設計相關知識,請存取:!!
以上就是一文詳解Golang中的反射的詳細內容,更多請關注TW511.COM其它相關文章!