golang反射機制介紹

golang反射機制介紹

Go語言提供了一種機制,在編譯時不知道類型的情況下,可更新變量、在運行時查看值、調用方法以及直接對他們的布局進行操作,這種機制稱為反射(reflection)。

reflect.Type 和 reflect.Value

反射功能由reflect包提供,它定義了兩個重要的類型:Type 和 Value,分別表示變量的類型和值。反射包里面,提供了reflect.TypeOf 和 reflect.ValueOf,返回被檢查對象的Type 和 Value:

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

可以看到,TypeOf和ValueOf的參數(shù)是interface{}類型,當把一個具體值賦給一個interface{}類型時,會發(fā)生一個隱式的類型轉換,轉換會生成一個包含兩個部分內容的接口值:動態(tài)類型部分是操作數(shù)的類型,動態(tài)值部分是操作數(shù)的值。

reflect.TypeOf返回接口值對應的動態(tài)類型,注意返回的時具體值類型而不是接口類型。比如下面的輸出的是"*os.File"而不是"io.Writer":

var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(v)) // "*os.File"

同理,reflect.ValueOf返回接口動態(tài)值,以reflect.Value的形式返回,與reflect.TypeOf類似:

v := reflect.ValueOf(3)
fmt.Println(v) //"3"

調用Value的Type方法會把它的類型以reflect.Type方式返回:

t := v.Type() //an reflect.Type
fmt.Println(t.String()) //"int"

reflect.ValueOf的逆操作是reflect.Value.Interface方法,它返回一個interface{}接口值,與reflect.Value包含同一個具體值:

v := reflect.ValueOf(3)
x := v.Interface()
i := x.(int)
fmt.Printf("%d\n", i) //"3"

reflect.Value 與 interface{}都可以包含任意值。二者的區(qū)別是空接口(interface{})隱藏了值的布局信息、內置操作和相關方法,除非我們知道它的動態(tài)類型,并用一個類型斷言來滲透進去,否則我們對所包含的值能做的事情很少。作為對比,Value有很多方法可以用來分析所包含的值,而不用知道它的類型。

利用反射遍歷結構體

前面介紹的是利用反射打印簡單數(shù)據(jù)類型的類型和結構,接下來介紹如何用反射遍歷一個結構體,可以參考下面的一個代碼:

func Display(i interface{}) {
    fmt.Printf("Display %T\n", i)   
    display("", reflect.ValueOf(i))
    
}
 
func display(path string, v reflect.Value) {
    switch v.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64,
        reflect.Uint, reflect.Uint8, reflect.Uint16,        
        reflect.Uint32, reflect.Uint64,         
        reflect.Float32, reflect.Float64,       
        reflect.Bool,       
        reflect.String:         
        fmt.Printf("%s: %v (%s)\n", path, v, v.Type().Name())       
    case reflect.Ptr:   
        v = v.Elem()        
        display(path, v)        
    case reflect.Slice, reflect.Array:  
        for i := 0; i < v.Len(); i++ {      
            display(" "+path+"["+strconv.Itoa(i)+"]", v.Index(i))           
        }       
    case reflect.Struct:    
        t := v.Type()   
        for i := 0; i < v.NumField(); i++ {         
            display(" "+path+"."+t.Field(i).Name, v.Field(i))           
        }       
    }   
}

首先創(chuàng)建一個Display函數(shù),然后在該函數(shù)里面封裝了一個display,display函數(shù)可以遞歸遍歷結構體內部的字段,如果是簡單的數(shù)據(jù)類型就直接打印出來,否則就遞歸遍歷結構體成員。接下來簡單解析一下display函數(shù):

數(shù)組與切邊:如果v為數(shù)組或者切邊,則調用v.Len()函數(shù),獲取該數(shù)組或切邊的長度,調用v.Index(i), 獲取該數(shù)組或切邊的第i個元素。

指針: 如果v為指針,調用v.Elem(),獲取指針指向的變量,然后遞歸該變量。

結構體:如果v為結構體,v.NumField()將返回該結構體的字段數(shù),v.Field(i)將返回第i個字段。

當然,switch語句這里,v.Kind()的類型不僅僅是這點,還有一些其他類型,比如reflect.Map,reflect.Interface, reflect.Func,reflect.Chan等等,這段代碼的目的僅僅是為了展示如何用反射遍歷結構體,所以沒有必要考慮所有的結構,都是大同小異的。

接下來我們,我們定義一個內嵌結構體的結構體,并且調用Display函數(shù),遍歷該結構體:

type Person struct {
    Name   string   
    Age    int  
    Gender bool
    
}

type Student struct {
    Person *Person  
    Course []string 
    Core   []float32    
}

func main() {
    st := &Student{
        Person: &Person{    
            Name:   "Jim",          
            Age:    18,         
            Gender: true,           
        },  
        Course: []string{"Math", "Data Structe", "Algorithm"},      
        Core:   []float32{90.5, 85, 89.9},      
    }   
    Display(st) 
}

輸出如下:

Display *main.Student
  .Person.Name: Jim (string)   
  .Person.Age: 18 (int)  
  .Person.Gender: true (bool)  
  .Course[0]: Math (string)   
  .Course[1]: Data Structe (string)   
  .Course[2]: Algorithm (string)  
  .Core[0]: 90.5 (float32)   
  .Core[1]: 85 (float32)   
  .Core[2]: 89.9 (float32)

利用reflect.Value修改值

Golang中,反射不只用來解析變量值,還可以改變變量的值,接下來介紹如何用reflect.Value來設置變量的值。

我們需要知道的是,在我們傳參給reflect.ValueOf獲取reflect.Value時,傳入的是參數(shù)的拷貝,所以reflect.ValueOf獲得的是傳入?yún)?shù)的拷貝的reflect.Value,對于它的修改對原來的數(shù)據(jù)是沒有影響的。所以如果我們想要修改原來的值,就需要用指針,然后還需要利用Elem方法獲取指針指向的變量,通過修改Elem()返回的變量,才能真正的修改原始變量的值,如下:

x := 2
v := reflect.ValueOf(&x)
e := v.Elem()
e.Set(reflect.ValueOf(3))
fmt.Println(x) //"3"

這里,還有一些基本類型特化的Set變種:SetInt、SetUint、SetString、SetFloat等:

e.SetInt(4)
fmt.Println(e) //"4"

在更新變量前,我們可以用CanSet方法來判斷是否可更改,如果更改一個不可更改的reflect.Value,將導致panic:

fmt.Println(e.CanSet()) //"true"
d := reflect.ValueOf(2)
fmt.Println(d.CanSet()) //"false"
d.Set(reflect.ValueOf(4)) // 這里將導致panic

備注

Golang的反射是i一個功能和表達能力都很強大的工具,但是使用它是要謹慎,具體有如下一些原因:

  • 基于反射的的代碼都很脆弱。能導致編譯器報告類型錯誤的每種寫法,在反射中都有一個對應的的誤用寫法。編譯器在編譯時就能向你報告的這個錯誤,而反射錯誤則要等到執(zhí)行時才以崩潰的方式來報告。并且反射還降低了自動重構和分析工具的安全性和準確度,因為它們無法檢測到類型信息。

  • 我們要避免使用反射的原因是,反射的相關操作無法做靜態(tài)類型檢查,所以對于大量使用反射的代碼是很難理解的。對于接受interfacef{}或者reflect.Value的函數(shù),一定要寫清楚期望的參數(shù)類型和其他限制條件

  • 基于反射的函數(shù)會比特定類型優(yōu)化的函數(shù)慢一兩個數(shù)量級。測試很適合用反射,但是對于關鍵路徑上的函數(shù),最好避免使用反射。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 第一次知道反射的時候還是許多年前在學校里玩 C# 的時候。那時總是弄不清楚這個復雜的玩意能有什么實際用途……然后發(fā)...
    勿以浮沙筑高臺閱讀 1,185評論 0 9
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,436評論 0 16
  • 。??!
    橘子殿下閱讀 182評論 0 0
  • 小兒13個月了,能說能哼,能走能跳,身體健康,我想,這已是為人父母最高興的事了吧。 昨兒清早,照例我和孩兒他爹7點...
    丹妹子在畫畫閱讀 257評論 0 0

友情鏈接更多精彩內容