reflect 包的核心概念
Go 中的反射涉及兩個核心概念:
- Type:表示一個類型的結構體,
reflect.Type
是類型的描述。 - Value:表示一個值的結構體,
reflect.Value
是一個具體值的包裝。
反射讓我們能夠動態地訪問對象的類型和數據,并根據需要對其進行操作。
常用類型
reflect.Type
reflect.Type
是對 Go 類型的描述。可以通過它獲取有關類型的信息,比如類型名、類型的種類、是否是指針、結構體的字段等。
常見方法:
t.Kind()
:獲取reflect.Type
的底層類型(如int
、struct
、slice
等)。t.Name()
:獲取類型的名稱,僅對命名類型有效。t.NumField()
:獲取結構體類型的字段數。t.Field(i)
:獲取結構體的第i
個字段。
reflect.Value
reflect.Value
代表一個變量的值,它包含了具體的值,可以通過它獲取或修改數據。
常見方法:
v.Kind()
:獲取reflect.Value
的底層類型(如int
、struct
、slice
等)。v.Interface()
:將reflect.Value
轉換為interface{}
類型。v.Set()
:修改reflect.Value
的值(需要是可修改的,即傳入指針)。v.Type()
:獲取reflect.Value
的類型。v.String()
:獲取reflect.Value
的字符串表示。
常見的反射操作
獲取類型和值
使用 reflect.TypeOf
獲取類型,使用 reflect.ValueOf
獲取值。
package mainimport ("fmt""reflect"
)func main() {var x int = 42// 獲取類型t := reflect.TypeOf(x)// 獲取值v := reflect.ValueOf(x)fmt.Println("Type:", t) // 輸出:Type: intfmt.Println("Value:", v) // 輸出:Value: 42
}
動態修改值
reflect
允許我們在運行時動態修改值。要修改值,必須傳遞指向變量的指針。
package mainimport ("fmt""reflect"
)func main() {var x int = 42p := reflect.ValueOf(&x) // 傳入指針// 修改值p.Elem().SetInt(100)fmt.Println("Modified value:", x) // 輸出:Modified value: 100
}
獲取結構體字段
使用 reflect
獲取結構體字段名和值。
package mainimport ("fmt""reflect"
)type Person struct {Name stringAge int
}func printStructFields(s interface{}) {val := reflect.ValueOf(s)if val.Kind() == reflect.Struct {for i := 0; i < val.NumField(); i++ {field := val.Field(i)fmt.Printf("%s: %v\n", val.Type().Field(i).Name, field)}}
}func main() {p := Person{"Alice", 30}printStructFields(p)
}
使用反射調用方法
反射不僅可以獲取類型和值,還能動態調用方法。
package mainimport ("fmt""reflect"
)type Person struct {Name string
}func (p *Person) SayHello() {fmt.Println("Hello, my name is", p.Name)
}func main() {p := &Person{Name: "Alice"}// 獲取反射對象v := reflect.ValueOf(p)// 獲取方法并調用method := v.MethodByName("SayHello")method.Call(nil)
}
反射與類型斷言的對比
類型斷言與反射在用途上有很大區別:
- 類型斷言:通常用于接口類型的斷言,快速檢查和轉換接口類型為具體類型。
- reflect:允許動態地操作類型和值,可以用于獲取更多類型信息或修改值。
示例:類型斷言
package mainimport "fmt"func printType(i interface{}) {if str, ok := i.(string); ok {fmt.Println("String:", str)} else if num, ok := i.(int); ok {fmt.Println("Integer:", num)} else {fmt.Println("Unknown type")}
}func main() {printType("Hello")printType(42)printType(3.14)
}
示例:使用 reflect 獲取類型和值
package mainimport ("fmt""reflect"
)func main() {var x interface{} = 42v := reflect.ValueOf(x)t := reflect.TypeOf(x)fmt.Println("Type:", t) // 輸出:Type: intfmt.Println("Value:", v) // 輸出:Value: 42
}
總結:類型斷言與反射對比
特性 | 類型斷言 | reflect 包 |
---|---|---|
用途 | 用于接口類型的類型轉換 | 用于動態類型檢查、修改值、獲取字段等 |
性能 | 高效,編譯時確定類型 | 較慢,涉及運行時類型解析 |
語法簡潔性 | 簡單直觀 | 語法較復雜 |
類型安全 | 類型安全,編譯時檢查 | 無類型安全,運行時可能出錯 |
靈活性 | 靈活性較低,僅適用于接口類型斷言 | 高度靈活,可動態修改、調用方法等 |
- 案例
package _caseimport ("fmt""reflect"
)type student struct {Name string `json:"name,omitempty" db:"name2"`Age int `json:"age,omitempty"` // omitempty Zero-Value不序列化
}type User struct {Id intName stringAge int
}// 匿名字段
type Boy struct {UserAddr string
}func (u User) Hello(name string) {fmt.Println("hello", name)
}func ReflectCase1() {//reflectTest1()//reflectType("cz")//reflectValue(55.6)//reflectTest2()//u := User{1, "chen", 18}//Poni(u)//m := Boy{User{1, "sa", 20}, "bj"}//reflectTest3(m)//fmt.Println(u)//setValue(&u)//fmt.Println(u)//userMethod(u)//var s student//getTag(&s)
}func getTag(o any) {v := reflect.ValueOf(o)// 返回reflect.TypeOf類型t := v.Type()// 獲取字段for i := 0; i < t.Elem().NumField(); i++ {f := t.Elem().Field(i)fmt.Print(f.Tag.Get("json"), "\t")fmt.Println(f.Tag.Get("db"))}
}func userMethod(o any) {v := reflect.ValueOf(o)// 獲取方法m := v.MethodByName("Hello")// 有參數的話需要傳一個Value類型切片args := []reflect.Value{reflect.ValueOf("666")}// 沒有參數只需要:var args []reflect.Value// m.Call()m.Call(args)
}func setValue(o any) {v := reflect.ValueOf(o)// 獲取指針指向的元素v = v.Elem()// 取字段f := v.FieldByName("Name")if f.Kind() == reflect.String {f.SetString("zhen")}
}func reflectTest3(o any) {t := reflect.TypeOf(o)fmt.Println(t)// Anoymous:匿名fmt.Printf("%#v\n", t.Field(0))// 值信息fmt.Printf("%#v\n", reflect.ValueOf(o).Field(0))
}func Poni(o any) {t := reflect.TypeOf(o)fmt.Println("類型:", t)fmt.Println("字符串類型:", t.Name())// 獲取值v := reflect.ValueOf(o)fmt.Println(v)// 獲取所有屬性for i := 0; i < t.NumField(); i++ {f := t.Field(i)fmt.Printf("%s : %v, ", f.Name, f.Type)// 獲取字段值信息val := v.Field(i).Interface()fmt.Println("val:", val)}fmt.Println("==method==")for i := 0; i < t.NumMethod(); i++ {m := t.Method(i)fmt.Println(m.Name)fmt.Println(m.Type)}
}// 在處理處理少量已知類型時,使用類型斷言+switch性能更好,reflect性能低
// 相較于使用interface{} + switch + 類型推斷處理結構體時無法獲取詳細的字段或標簽信息。
// reflect處理復雜結構體內的字段,具有優勢可以獲取結構體的字段、標簽、方法等詳細信息。
// reflect使用場景:處理大量動態、未知的復雜數據類型,且這些類型在編譯時無法預知,使用 reflect 可以在運行時獲取這些類型信息
// 實現通用代碼
func reflectTest2() {stu := student{Name: "chenzhen",Age: 19,}v := reflect.ValueOf(stu)// 獲取struct字段數量fmt.Println("NumFields:", v.NumField())// 獲取字段Name值:// 1.v.Field(指定字段序號) -> 適用于不知道字段名(或者結合for遍歷操作)// 2.v.FieldByName("指定字段名") -> 適用于知道字段名fmt.Println("Name value:", v.Field(0).String(), ", ", v.FieldByName("Name").String())// 字段類型fmt.Println("Name type:", v.Field(0).Type())t := reflect.TypeOf(stu)for i := 0; i < t.NumField(); i++ {// 獲取字段名name := t.Field(i).Namefmt.Println("Field Name:", name)// 獲取tagif fieldName, ok := t.FieldByName(name); ok {tag := fieldName.Tagfmt.Println("tag-", tag, ", ", "json:", tag.Get("json"), ", id", tag.Get("id"))}}
}func reflectTest1() {x := 1.2345fmt.Println("TypeOf==")// TypeOf()返回接口中保存值的類型t := reflect.TypeOf(x)fmt.Println("type:", t)fmt.Println("kind:", t.Kind())fmt.Println("ValueOf==")v := reflect.ValueOf(x)fmt.Println("value:", v)fmt.Println("type:", v.Type())fmt.Println("kind:", v.Kind())// Float傳入一個Value類型值,返回一個float64類型fmt.Println("value:", v.Float())z := v.Interface() // Interface()返回一個any類型值fmt.Println(z)fmt.Printf("value is %g\n", z)x1 := []int{1, 2, 3}v1 := reflect.ValueOf(x1)fmt.Println("type:", v1.Type())fmt.Println("kind:", v1.Kind())x2 := map[string]string{"test1": "1", "test2": "2"}v2 := reflect.ValueOf(x2)fmt.Println("type:", v2.Type())fmt.Println("kind:", v2.Kind())fmt.Println("kind==")// Kind()返回類型種類,與Type()區別為:如下案例,Kind返回更底層type MyInt intm := MyInt(5)v3 := reflect.ValueOf(m)fmt.Println("type:", v3.Type())fmt.Println("kind:", v3.Kind())
}func reflectType(a any) {t := reflect.TypeOf(a)fmt.Println("類型是:", t)// kind()獲取具體類型k := t.Kind()fmt.Println(k)switch k {case reflect.Float64:fmt.Println("a is float64")case reflect.String:fmt.Println("string")default:panic("unhandled default case")}
}func reflectValue(a any) {v := reflect.ValueOf(a)fmt.Println(v)fmt.Println(v.Type())switch k := v.Kind(); k {case reflect.Float64:fmt.Println("a is ", v.Float())default:panic("unhandled default case")}
}
package _caseimport ("errors""fmt""reflect"
)func ReflectCase2() {type user struct {ID int64Name stringHobby []string}type outUser struct {ID int64Name stringHobby []string}u := user{ID: 1, Name: "nick", Hobby: []string{"籃球", "羽毛球"}}out := outUser{}// 需求1:使用reflect動態copy structrs := copy(&out, u)fmt.Println(rs, out)// 需求2:sliceUser := []user{{ID: 1, Name: "nick", Hobby: []string{"籃球", "羽毛球"}},{ID: 2, Name: "nick1", Hobby: []string{"籃球1", "羽毛球1"}},{ID: 3, Name: "nick2", Hobby: []string{"籃球2", "羽毛球2"}},}slice := sliceColumn(sliceUser, "Hobby")fmt.Println(slice)
}// 從一個切片或結構體中提取指定字段(colu)的值,并返回一個包含這些值的切片
// 每次 t = t.Elem() 或 v = v.Elem() 都是為了處理某一層的指針解引用問題,以便獲取實際的值或類型。
// 如果傳入的切片類型涉及指針,例如 *[]*Struct,就需要多次解引用才能得到實際的元素類型和值。// 對于四次t = t.Elem()解釋
// reflect.Elem(),顧名思義,是取得變量的元素部分
// 在Golang中,變量的元素部分指的是指針指向的變量本身。
// 第一個 t = t.Elem() 處理傳入 slice 是指針的情況。
// 第二個 t = t.Elem() 獲取切片元素的類型。
// 第三個 t = t.Elem() 處理切片元素是指針的情況,獲取指針指向的實際類型。
// o.Elem() 處理遍歷時元素是指針的情況,解引用以訪問字段。// 我的理解:對于
//
// if t.Kind() == reflect.Ptr {
// t = t.Elem()
// v = v.Elem()
// }
// 第一個t = t.Elem()這是為了處理傳入時傳入的是切片地址的情況,如果傳入的 slice 不是指針,比如 []Struct,這一段代碼不會執行,因此不會影響后面的邏輯。
// 而如果傳入的是切片,則會在第二個t = t.Elem()生效,這是因為切片打印出來是指向其第一個元素的地址,我們要的是其值,
// 所以要t = t.Elem()而接下來的
// if t.Kind() == reflect.Ptr {
// t = t.Elem()
// }則是為了應對其在切片內部還有一個切片指針的情況,需要獲取其值而最后的:
// if o.Kind() == reflect.Ptr {
// v1 := o.Elem()
// val := v1.FieldByName(colu)
// s = reflect.Append(s, val)
// }則是處理切片中的切片中的field中指針的情況。
func sliceColumn(slice any, colu string) any {t := reflect.TypeOf(slice)v := reflect.ValueOf(slice)// 因為這里傳入一個切片,切片值為指向其第一個元素的地址,所以要elemif t.Kind() == reflect.Ptr {t = t.Elem()v = v.Elem()}// 如果直接傳入的slice是一個結構體,那么直接返回要找的colu對應值if v.Kind() == reflect.Struct {val := v.FieldByName(colu)return val.Interface()}// 處理切片情況if v.Kind() != reflect.Slice {return nil}t = t.Elem()// 如果還是一個指針,要找value,我們期望他是一個structif t.Kind() == reflect.Ptr {t = t.Elem()}f, _ := t.FieldByName(colu)// 獲取要找字段的類型sliceT := reflect.SliceOf(f.Type)// 根據類型創建切片s := reflect.MakeSlice(sliceT, 0, 0)for i := 0; i < v.Len(); i++ {// index(i)返回v持有值的第i個元素。如果v的Kind不是Array、Chan、Slice、String,或者i出界,會panico := v.Index(i)if o.Kind() == reflect.Struct {val := o.FieldByName(colu)s = reflect.Append(s, val)}if o.Kind() == reflect.Ptr {v1 := o.Elem()val := v1.FieldByName(colu)s = reflect.Append(s, val)}}return s.Interface()
}func copy(dest any, source any) error {// 對sorece的reflect處理sT := reflect.TypeOf(source)sV := reflect.ValueOf(source)// 但是如果source傳入的是指針,那么還要多操作一次,獲取它的值if sT.Kind() == reflect.Ptr {sT = sT.Elem()sV = sV.Elem()}// 對于dest的reflect處理dT := reflect.TypeOf(dest)dV := reflect.ValueOf(dest)// 因為dest要被修改,所以傳入的一定是指針if dT.Kind() != reflect.Ptr {return errors.New("target對象必須為指針類型")}dT = dT.Elem()dV = dV.Elem()// source必須為struct或者struct指針if sV.Kind() != reflect.Struct {return errors.New("sorce必須為struct或者struct指針")}// dest必須為struct指針if dV.Kind() != reflect.Struct {return errors.New("dest對象必須為struct指針")}// New()返回一個Value類型值,該值持有一個指向類型為傳入類型的新申請的零值的指針,返回值的Type為PtrTo(typ)// 這里destObj是待復制對象,所以new出zero-valuedestObj := reflect.New(dT)for i := 0; i < dT.NumField(); i++ {// 每字段dField := dT.Field(i)if sField, ok := sT.FieldByName(dField.Name); ok {if dField.Type != sField.Type {continue}// 取sV中與dField.Name同名的Value賦給valuevalue := sV.FieldByName(dField.Name)// 設置destObj(指針)對應dField.Name的字段的值為valuedestObj.Elem().FieldByName(dField.Name).Set(value)}}dV.Set(destObj.Elem())// error nilreturn nil
}