活學活用golang的反射機制

瞭解和使用golang有一段時間了,由於項目比較趕,基本是現學現賣的節奏。最近有時間會在簡書上記錄遇到的一些問題和解決方案,希望可以一起交流探討。

需求

  • 在golang中,給定一組數據,例如 map[string]interface{} 類型的數據,創建一個對應的struct並賦值

簡易實現

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16.25,
}

type Fruit struct {
    ID    int
    Name  string
    Price float64
}

func newFruit(data map[string]interface{}) *Fruit {
    s := Fruit{
        ID:    data["id"].(int),
        Name:  data["name"].(string),
        Price: data["price"].(float64),
    }
    return &s
}

func main() {
    fruit := newFruit(data)
    log.Println("fruit:", fruit)
}

> fruit: &{1001 apple 16.25} 
這樣實現簡單快速,但也有缺點:

  • 難以維護,每次新增字段都要修改newFruit函數
  • 不夠優雅,需要手動對每一個字段進行賦值和類型轉換
  • 不夠通用,只能創建欽定的struct

改進

是否有更好的解決方法,自動遍歷struct對象,並進行賦值呢?

首先想到for...range操作符,但golang裏range無法對結構體進行遍歷。

(如果只需遍歷struct而不用賦值,可以嘗試邪道組合: json.Marshal() 和 json.Unmarshal()一鍵把struct轉成 map[string]interface() )

實際上要遍歷一個struct,需要使用golang的reflect包。關於golang的反射機制不再贅述,可以參考go的文檔,有很詳細的說明。

那麼現在利用reflect,嘗試改進之前的代碼

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16.25,
}

type Fruit struct {
    ID    int
    Name  string
    Price float64
}

// 遍歷struct並且自動進行賦值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍歷結構體
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        if v, ok := data[t.Name]; ok {
            f.Set(reflect.ValueOf(v))
        } else {
            panic(t.Name + " not found")
        }
    }
}
func main() {
    //fruit := newFruit(data)
    fruit := Fruit{}
    structByReflect(data, &fruit)
    log.Println("fruit:", fruit)
}

編譯運行

> panic: ID not found 

新的問題出現了,結構體的字段名 ID 和data中的 id 大小寫不一致,導致無法從data中取得對應的數據。

修改data的key name,或者修改struct的field name當然可以解決,但在實際應用中,data往往從外部獲得不受控制,而data的key通常也不符合go的命名規範,因此暴力改名不可取。

那怎麼解決呢?這裏可以利用go的 成員變量標籤(field tag) ,給struct的字段增加額外的元數據,用以指定對應的字段名。golang對json和xml等的序列化處理也是用了這個方法。

type Fruit struct {
    ID    int     `key:"id"`
    Name  string  `key:"name"`
    Price float64 `key:"price"`
}
// 遍歷struct並且自動進行賦值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍歷結構體
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        // 得到tag中的字段名
        key := t.Tag.Get("key")
        if v, ok := data[key]; ok {
            f.Set(reflect.ValueOf(v))
        } else {
            panic(t.Name + " not found")
        }
    }
}

再次編譯運行,這次得到了期望的結果

> fruit: {1001 apple 16.25}

類型轉換問題

到這裏已經基本實現了想要的功能,但還有一個問題,如果data中的數據類型,和struct中定義的類型稍有不一致,反射賦值語句就會報錯,

var data = map[string]interface{}{
    "id":    1001,
    "name":  "apple",
    "price": 16,  // 改成int類型
}

測試一下:

> panic: reflect.Set: value of type int is not assignable to type float64

我們知道 int 和 float64 可以相互強制轉換,但是 reflect.Set() 方法並不想幫你轉。

這裏還是要利用reflect包的兩個方法, Type.ConvertibleTo(u Type) 用來判斷能否轉換到指定類型,再通過 Value.Convert(t Type) 來進行類型轉換。

再次優化我們的函數:

// 遍歷struct並且自動進行賦值
func structByReflect(data map[string]interface{}, inStructPtr interface{}) {
    rType := reflect.TypeOf(inStructPtr)
    rVal := reflect.ValueOf(inStructPtr)
    if rType.Kind() == reflect.Ptr {
        // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value
        rType = rType.Elem()
        rVal = rVal.Elem()
    } else {
        panic("inStructPtr must be ptr to struct")
    }
    // 遍歷結構體
    for i := 0; i < rType.NumField(); i++ {
        t := rType.Field(i)
        f := rVal.Field(i)
        // 得到tag中的字段名
        key := t.Tag.Get("key")
        if v, ok := data[key]; ok {
            // 檢查是否需要類型轉換
            dataType := reflect.TypeOf(v)
            structType := f.Type()
            if structType == dataType {
                f.Set(reflect.ValueOf(v))
            } else {
                if dataType.ConvertibleTo(structType) {
                    // 轉換類型
                    f.Set(reflect.ValueOf(v).Convert(structType))
                } else {
                    panic(t.Name + " type mismatch")
                }
            }
        } else {
            panic(t.Name + " not found")
        }
    }
}

在f.Set()之前,先檢查data的Type和struct字段的Type是否一致,如果不一致則進行轉換。

> fruit: {1001 apple 16}

這樣功能就全部完成了,示例代碼中遇到錯誤都直接拋出panic,可以根據實際項目進行調整。

主要到這裏沒有處理嵌套的結構體等情況,這部分通過判斷Type爲struct時,進行遞歸處理就可以實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章