Golang中反射的常見用法分享
在之前的兩篇文章 《深入理解 go reflect - 反射基本原理》、《深入理解 go reflect - 要不要傳指針》 中, 我們講解了關(guān)于 go 反射的一些基本原理,以及通過反射對象修改變量的一些注意事項(xiàng)。 本篇文章將介紹一些常見的反射用法,涵蓋了常見的數(shù)據(jù)類型的反射操作。
根據(jù)類型做不同處理
使用反射很常見的一個(gè)場景就是根據(jù)類型做不同處理,比如下面這個(gè)方法,根據(jù)不同的 Kind
返回不同的字符串表示:
func getType(i interface{}) string { v := reflect.ValueOf(i) switch v.Kind() { case reflect.Bool: b := "false" if v.Bool() { b = "true" } return fmt.Sprintf("bool: %s", b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return fmt.Sprintf("int: %d", v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return fmt.Sprintf("uint: %d", v.Uint()) case reflect.Float32, reflect.Float64: return fmt.Sprintf("float: %.1f", v.Float()) case reflect.String: return fmt.Sprintf("string: %s", v.String()) case reflect.Interface: return fmt.Sprintf("interface: %v", v.Interface()) case reflect.Struct: return fmt.Sprintf("struct: %v", v.Interface()) case reflect.Map: return fmt.Sprintf("map: %v", v.Interface()) case reflect.Slice: return fmt.Sprintf("slice: %v", v.Interface()) case reflect.Array: return fmt.Sprintf("array: %v", v.Interface()) case reflect.Pointer: return fmt.Sprintf("pointer: %v", v.Interface()) case reflect.Chan: return fmt.Sprintf("chan: %v", v.Interface()) default: return "unknown" } } func TestKind(t *testing.T) { assert.Equal(t, "int: 1", getType(1)) assert.Equal(t, "string: 1", getType("1")) assert.Equal(t, "bool: true", getType(true)) assert.Equal(t, "float: 1.0", getType(1.0)) arr := [3]int{1, 2, 3} sli := []int{1, 2, 3} assert.Equal(t, "array: [1 2 3]", getType(arr)) assert.Equal(t, "slice: [1 2 3]", getType(sli)) }
標(biāo)準(zhǔn)庫 json 中的示例
在標(biāo)準(zhǔn)庫 encoding/json
中,也有類似的場景,比如下面這個(gè)方法,根據(jù)不同的 Kind
做不同的處理:
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { // ... 其他代碼 switch t.Kind() { case reflect.Bool: return boolEncoder case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return intEncoder case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return uintEncoder // ...省略其他 case... default: return unsupportedTypeEncoder } }
在進(jìn)行 json
編碼的時(shí)候,因?yàn)椴恢纻魅氲膮?shù)是什么類型,所以需要根據(jù)類型做不同的處理,這里就是使用反射來做的。 通過判斷不同的類型,然后返回不同的 encoder
。
基本類型的反射
這里說的基本類型是:int*
、uint*
、float*
、complex*
、bool
這種類型。
通過反射修改基本類型的值,需要注意的是,傳入的參數(shù)必須是指針類型,否則會 panic
:
func TestBaseKind(t *testing.T) { // 通過反射修改 int 類型變量的值 a := 1 v := reflect.ValueOf(&a) v.Elem().SetInt(10) assert.Equal(t, 10, a) // 通過反射修改 uint16 類型變量的值 b := uint16(10) v1 := reflect.ValueOf(&b) v1.Elem().SetUint(20) assert.Equal(t, uint16(20), b) // 通過反射修改 float32 類型變量的值 f := float32(10.0) v2 := reflect.ValueOf(&f) v2.Elem().SetFloat(20.0) assert.Equal(t, float32(20.0), f) }
通過反射修改值的時(shí)候,需要通過 Elem()
方法的返回值來修改。
數(shù)組類型的反射
通過反射修改數(shù)組中元素的值,可以使用 Index
方法取得對應(yīng)下標(biāo)的元素,然后再使用 Set
方法修改值:
func TestArray(t *testing.T) { // 通過反射修改數(shù)組元素的值 arr := [3]int{1, 2, 3} v := reflect.ValueOf(&arr) // 修改數(shù)組中的第一個(gè)元素 v.Elem().Index(0).SetInt(10) assert.Equal(t, [3]int{10, 2, 3}, arr) }
chan 反射
我們可以通過反射對象來向 chan
中發(fā)送數(shù)據(jù),也可以從 chan
中接收數(shù)據(jù):
func TestChan(t *testing.T) { // 通過反射修改 chan ch := make(chan int, 1) v := reflect.ValueOf(&ch) // 通過反射對象向 chan 發(fā)送數(shù)據(jù) v.Elem().Send(reflect.ValueOf(2)) // 在反射對象外部從 chan 接收數(shù)據(jù) assert.Equal(t, 2, <-ch) }
map 反射
通過反射修改 map
中的值,可以使用 SetMapIndex
方法修改 map
中對應(yīng)的 key
:
func TestMap(t *testing.T) { // 通過反射修改 map 元素的值 m := map[string]int{"a": 1} v := reflect.ValueOf(&m) // 修改 a 的 key,修改其值為 2 v.Elem().SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(2)) // 外部的 m 可以看到反射對象的修改 assert.Equal(t, 2, m["a"]) }
迭代反射 map 對象
我們可以通過反射對象的 MapRange
方法來迭代 map
對象:
func TestIterateMap(t *testing.T) { // 遍歷 map m := map[string]int{"a": 1, "b": 2} v := reflect.ValueOf(m) // 創(chuàng)建 map 迭代器 iter := v.MapRange() // 迭代 map 的元素 for iter.Next() { // a 1 // b 2 fmt.Println(iter.Key(), iter.Value()) } }
slice 反射
通過反射修改 slice
中的值,可以使用 Index
方法取得對應(yīng)下標(biāo)的元素,然后再使用 Set*
方法修改值,跟數(shù)組類似:
func TestSlice(t *testing.T) { // 通過反射修改 slice 元素的值 sli := []int{1, 2, 3} v := reflect.ValueOf(&sli) v.Elem().Index(0).SetInt(10) assert.Equal(t, []int{10, 2, 3}, sli) }
string 反射
對于 string
類型,我們可以通過其反射對象的 String
方法來修改其內(nèi)容:
func TestString(t *testing.T) { // 通過反射修改字符串的值 s := "hello" v := reflect.ValueOf(&s) v.Elem().SetString("world") assert.Equal(t, "world", s) }
interface/Pointer 反射
對于 interface
或 Pointer
類型,我們可以通過其反射對象的 Elem
方法來修改其內(nèi)容:
func TestPointer(t *testing.T) { a := 1 // 接口類型 var i interface{} = &a v1 := reflect.ValueOf(i) v1.Elem().SetInt(10) assert.Equal(t, 10, a) // 指針類型 var p = &a v2 := reflect.ValueOf(p) v2.Elem().SetInt(20) assert.Equal(t, 20, a) }
這兩種類型,我們都需要通過 Elem
方法來先獲取其實(shí)際保存的值,然后再修改其值。
結(jié)構(gòu)體的反射
對于 go 中的結(jié)構(gòu)體,反射系統(tǒng)中為我們提供了很多操作結(jié)構(gòu)體的方法,比如獲取結(jié)構(gòu)體的字段、方法、標(biāo)簽、通過反射對象調(diào)用其方法等。
先假設(shè)我們有如下結(jié)構(gòu)體:
type Person struct { Name string Age int sex uint8 } func (p Person) M1() string { return "person m1" } func (p *Person) M2() string { return "person m2" }
遍歷結(jié)構(gòu)體字段
我們可以通過 NumField
方法來獲取結(jié)構(gòu)體的字段數(shù)量,然后通過 Field
方法來獲取結(jié)構(gòu)體的字段:
func TestStruct1(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(p) // string Tom // int 18 // uint8 1 for i := 0; i < v.NumField(); i++ { fmt.Println(v.Field(i).Type(), v.Field(i)) } }
根據(jù)名稱或索引獲取結(jié)構(gòu)體字段
我們可以根據(jù)結(jié)構(gòu)體字段的名稱或索引來獲取結(jié)構(gòu)體的字段:
func TestStruct2(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(p) assert.Equal(t, 18, v.Field(1).Interface()) assert.Equal(t, 18, v.FieldByName("Age").Interface()) assert.Equal(t, 18, v.FieldByIndex([]int{1}).Interface()) }
修改結(jié)構(gòu)體字段
我們可以通過 Field
方法來獲取結(jié)構(gòu)體的字段,然后再使用 Set*
方法來修改其值:
func TestStruct2(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(&p) v.Elem().FieldByName("Name").SetString("Jack") assert.Equal(t, "Jack", p.Name) }
上面因?yàn)?Name
是 string
類型,所以我們使用 SetString
方法來修改其值,如果是 int
類型,我們可以使用 SetInt
方法來修改其值,依此類推。
結(jié)構(gòu)體方法調(diào)用
通過反射對象來調(diào)用結(jié)構(gòu)體的方法時(shí),需要注意的是,如果我們需要調(diào)用指針接收者的方法,則需要傳遞地址:
func TestStruct3(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} // 值接收者(receiver) v1 := reflect.ValueOf(p) assert.Equal(t, 1, v1.NumMethod()) // 注意:值接收者沒有 M2 方法 assert.False(t, v1.MethodByName("M2").IsValid()) // 通過值接收者調(diào)用 M1 方法 results := v1.MethodByName("M1").Call(nil) assert.Len(t, results, 1) assert.Equal(t, "person m1", results[0].Interface()) // 指針接收者(pointer receiver) v2 := reflect.ValueOf(&p) assert.Equal(t, 2, v2.NumMethod()) // 通過指針接收者調(diào)用 M1 和 M2 方法 results = v2.MethodByName("M1").Call(nil) assert.Len(t, results, 1) assert.Equal(t, "person m1", results[0].Interface()) results = v2.MethodByName("M2").Call(nil) assert.Len(t, results, 1) assert.Equal(t, "person m2", results[0].Interface()) }
說明:
- 結(jié)構(gòu)體參數(shù)是值的時(shí)候,
reflect.ValueOf
返回的反射對象只能調(diào)用值接收者的方法,不能調(diào)用指針接收者的方法。 - 結(jié)構(gòu)體參數(shù)是指針的時(shí)候,
reflect.ValueOf
返回的反射對象可以調(diào)用值接收者和指針接收者的方法。 - 調(diào)用
MethodByName
方法時(shí),如果方法不存在,則返回的反射對象的IsValid
方法返回false
。 - 調(diào)用
Call
方法時(shí),如果沒有參數(shù),傳nil
參數(shù)即可。如果方法沒有返回值,則返回的結(jié)果切片為空。 - 調(diào)用
Call
方法的參數(shù)是reflect.Value
類型的切片,返回值也是reflect.Value
類型的切片。
是否實(shí)現(xiàn)接口
對于這個(gè),其實(shí)有一個(gè)更簡單的方法,那就是利用接口斷言:
func TestStrunct4_0(t *testing.T) { type TestInterface interface { M1() string } var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(p) // v.Interface() 返回的是 interface{} 類型 // v.Interface().(TestInterface) 將 interface{} 類型轉(zhuǎn)換為 TestInterface 類型 v1, ok := v.Interface().(TestInterface) assert.True(t, ok) assert.Equal(t, "person m1", v1.M1()) }
另外一個(gè)方法是,通過反射對象的 Type
方法獲取類型對象,然后調(diào)用 Implements
方法來判斷是否實(shí)現(xiàn)了某個(gè)接口:
func TestStruct4(t *testing.T) { type TestInterface interface { M1() string } var p = Person{Name: "Tom", Age: 18, sex: 1} typ := reflect.TypeOf(p) typ1 := reflect.TypeOf((*TestInterface)(nil)).Elem() assert.True(t, typ.Implements(typ1)) }
結(jié)構(gòu)體的 tag
這在序列化、反序列化、ORM 庫中用得非常多,常見的 validator
庫也是通過 tag 來實(shí)現(xiàn)的。 下面的例子中,通過獲取變量的 Type
就可以獲取其 tag
了:
type Person1 struct { Name string `json:"name"` } func TestStruct5(t *testing.T) { var p = Person1{Name: "Tom"} typ := reflect.TypeOf(p) tag := typ.Field(0).Tag assert.Equal(t, "name", tag.Get("json")) }
修改結(jié)構(gòu)體未導(dǎo)字段
我們知道,結(jié)構(gòu)體的字段如果首字母小寫,則是未導(dǎo)出的,不能被外部包訪問。但是我們可以通過反射修改它:
func TestStruct6(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(&p) // 下面這樣寫會報(bào)錯: // panic: reflect: reflect.Value.SetInt using value obtained using unexported field // v.Elem().FieldByName("sex").SetInt(0) ft := v.Elem().FieldByName("sex") sexV := reflect.NewAt(ft.Type(), unsafe.Pointer(ft.UnsafeAddr())).Elem() assert.Equal(t, 1, p.sex) // 修改前是 1 sexV.Set(reflect.ValueOf(uint8(0))) // 將 sex 字段修改為 0 assert.Equal(t, 0, p.sex) // 修改后是 0 }
這里通過 NewAt
方法針對 sex
這個(gè)未導(dǎo)出的字段創(chuàng)建了一個(gè)指針,然后我們就可以通過這個(gè)指針來修改 sex
字段了。
方法的反射
這里說的方法包括函數(shù)和結(jié)構(gòu)體的方法。
入?yún)⒑头祷刂?/h3>
reflect
包中提供了 In
和 Out
方法來獲取方法的入?yún)⒑头祷刂担?/p>
func (p Person) Test(a int, b string) int { return a } func TestMethod(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(p) m := v.MethodByName("Test") // 參數(shù)個(gè)數(shù)為 2 assert.Equal(t, 2, m.Type().NumIn()) // 返回值個(gè)數(shù)為 1 assert.Equal(t, 1, m.Type().NumOut()) // In(0) 是第一個(gè)參數(shù),In(1) 是第二個(gè)參數(shù) arg1 := m.Type().In(0) assert.Equal(t, "int", arg1.Name()) arg2 := m.Type().In(1) assert.Equal(t, "string", arg2.Name()) // Out(0) 是第一個(gè)返回值 ret0 := m.Type().Out(0) assert.Equal(t, "int", ret0.Name()) }
說明:
In
和Out
方法返回的是reflect.Type
類型,可以通過Name
方法獲取類型名稱。NumIn
和NumOut
方法返回的是參數(shù)和返回值的個(gè)數(shù)。reflect.Value
類型的MethodByName
方法可以獲取結(jié)構(gòu)體的方法。
通過反射調(diào)用方法
reflect.Value
中對于方法類型的反射對象,有一個(gè) Call
方法,可以通過它來調(diào)用方法:
func TestMethod2(t *testing.T) { var p = Person{Name: "Tom", Age: 18, sex: 1} v := reflect.ValueOf(p) // 通過反射調(diào)用 Test 方法 m := v.MethodByName("Test") arg1 := reflect.ValueOf(1) arg2 := reflect.ValueOf("hello") args := []reflect.Value{arg1, arg2} rets := m.Call(args) assert.Len(t, rets, 1) assert.Equal(t, 1, rets[0].Interface()) }
說明:
Call
方法的參數(shù)是[]reflect.Value
類型,需要將參數(shù)轉(zhuǎn)換為reflect.Value
類型。Call
方法的返回值也是[]reflect.Value
類型。reflect.Value
類型的MethodByName
方法可以獲取結(jié)構(gòu)體的方法的反射對象。- 通過方法的反射對象的
Call
方法可以實(shí)現(xiàn)調(diào)用方法。
總結(jié)
- 通過
reflect.Kind
可以判斷反射對象的類型,Kind
涵蓋了 go 中所有的基本類型,所以反射的時(shí)候判斷Kind
就足夠了。 - 如果要獲取反射對象的值,需要傳遞指針給
reflect.Value
。 - 可以往
chan
的反射對象中發(fā)送數(shù)據(jù),也可以從chan
的反射對象中接收數(shù)據(jù)。 SetMapIndex
方法可以修改map
中的元素。MapRange
方法可以獲取map
的迭代器。- 可以通過
Index
方法獲取slice
的元素,也可以通過SetIndex
方法修改slice
的元素。 - 可以通過
SetString
方法修改string
的值。 - 對于
interface
和Pointer
類型的反射對象,可以通過Elem
方法獲取它們的值,同時(shí)也只有通過Elem
獲取到的反射對象能調(diào)用Set*
方法來修改其指向的對象。 reflect
包中提供了很多操作結(jié)構(gòu)體的功能:如獲取結(jié)構(gòu)體的字段、獲取結(jié)構(gòu)體的方法、調(diào)用結(jié)構(gòu)體的方法等。我們使用一些類庫的時(shí)候,會需要通過結(jié)構(gòu)體的tag
來設(shè)置一些元信息,這些信息只有通過反射才能獲取。- 我們可以通過
NewAt
來創(chuàng)建一個(gè)指向結(jié)構(gòu)體未導(dǎo)出字段的反射對象,這樣就可以修改結(jié)構(gòu)體的未導(dǎo)出字段了。 - 對于函數(shù)和方法,go 的反射系統(tǒng)也提供了很多功能,如獲取參數(shù)和返回值信息、使用
Call
來調(diào)用函數(shù)和方法等。
到此這篇關(guān)于Golang中反射的常見用法分享的文章就介紹到這了,更多相關(guān)Golang反射內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go-micro微服務(wù)domain層開發(fā)示例詳解
這篇文章主要為大家介紹了go-micro微服務(wù)domain層開發(fā)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01golang進(jìn)程內(nèi)存控制避免docker內(nèi)oom
這篇文章主要為大家介紹了golang進(jìn)程內(nèi)存控制避免docker內(nèi)oom示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10go語言搬磚之go jmespath實(shí)現(xiàn)查詢json數(shù)據(jù)
這篇文章主要為大家介紹了go語言搬磚之go jmespath實(shí)現(xiàn)查詢json數(shù)據(jù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Nunu快速構(gòu)建高效可靠Go應(yīng)用腳手架使用詳解
這篇文章主要為大家介紹了如何使用Nunu快速構(gòu)建高效可靠Go應(yīng)用腳手架詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06詳解Go語言實(shí)現(xiàn)線性查找算法和二分查找算法
線性查找又稱順序查找,它是查找算法中最簡單的一種。二分查找,也稱折半查找,相比于線性查找,它是一種效率較高的算法。本文將用Go語言實(shí)現(xiàn)這兩個(gè)查找算法,需要的可以了解一下2022-12-12