golang reflect遍歷struct并賦值 & struct創(chuàng)建和數(shù)據(jù)綁定

了解和使用golang有一段時(shí)間了,由于項(xiàng)目比較趕驱闷,基本是現(xiàn)學(xué)現(xiàn)賣的節(jié)奏耻台。最近有時(shí)間會(huì)在簡(jiǎn)書上記錄遇到的一些問題和解決方案,希望可以一起交流探討空另。

需求

  • 在golang中盆耽,給定一組數(shù)據(jù),例如map[string]interface{}類型的數(shù)據(jù)扼菠,創(chuàng)建一個(gè)對(duì)應(yīng)的struct并賦值

簡(jiǎn)易實(shí)現(xiàn)

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}
這樣實(shí)現(xiàn)簡(jiǎn)單快速摄杂,但也有缺點(diǎn):

  • 難以維護(hù),每次新增字段都要修改newFruit函數(shù)
  • 不夠優(yōu)雅循榆,需要手動(dòng)對(duì)每一個(gè)字段進(jìn)行賦值和類型轉(zhuǎn)換
  • 不夠通用析恢,只能創(chuàng)建欽定的struct

改進(jìn)

是否有更好的解決方法,自動(dòng)遍歷struct對(duì)象秧饮,并進(jìn)行賦值呢映挂?

首先想到for...range操作符,但golang里range無(wú)法對(duì)結(jié)構(gòu)體進(jìn)行遍歷盗尸。
(如果只需遍歷struct而不用賦值柑船,可以嘗試邪道組合:json.Marshal()json.Unmarshal() 一鍵把struct轉(zhuǎn)成map[string]interface())

實(shí)際上要遍歷一個(gè)struct,需要使用golang的reflect包泼各。關(guān)于golang的反射機(jī)制不再贅述鞍时,可以參考go的文檔,有很詳細(xì)的說(shuō)明扣蜻。
那么現(xiàn)在利用reflect逆巍,嘗試改進(jìn)之前的代碼

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

type Fruit struct {
    ID    int
    Name  string
    Price float64
}

// 遍歷struct并且自動(dòng)進(jìn)行賦值
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")
    }
    // 遍歷結(jié)構(gòu)體
    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)
}

編譯運(yùn)行
> panic: ID not found
新的問題出現(xiàn)了莽使,結(jié)構(gòu)體的字段名ID和data中的id大小寫不一致锐极,導(dǎo)致無(wú)法從data中取得對(duì)應(yīng)的數(shù)據(jù)。

修改data的key name吮旅,或者修改struct的field name當(dāng)然可以解決溪烤,但在實(shí)際應(yīng)用中,data往往從外部獲得不受控制庇勃,而data的key通常也不符合go的命名規(guī)范檬嘀,因此暴力改名不可取。

那怎么解決呢责嚷?這里可以利用go的成員變量標(biāo)簽(field tag)鸳兽,給struct的字段增加額外的元數(shù)據(jù),用以指定對(duì)應(yīng)的字段名罕拂。golang對(duì)json和xml等的序列化處理也是用了這個(gè)方法揍异。

type Fruit struct {
    ID    int     `key:"id"`
    Name  string  `key:"name"`
    Price float64 `key:"price"`
}
// 遍歷struct并且自動(dòng)進(jìn)行賦值
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")
    }
    // 遍歷結(jié)構(gòu)體
    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")
        }
    }
}

再次編譯運(yùn)行,這次得到了期望的結(jié)果
> fruit: {1001 apple 16.25}

類型轉(zhuǎn)換問題

到這里已經(jīng)基本實(shí)現(xiàn)了想要的功能衷掷,但還有一個(gè)問題辱姨,如果data中的數(shù)據(jù)類型,和struct中定義的類型稍有不一致戚嗅,反射賦值語(yǔ)句就會(huì)報(bào)錯(cuò)雨涛,

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

測(cè)試一下:
> panic: reflect.Set: value of type int is not assignable to type float64

我們知道intfloat64可以相互強(qiáng)制轉(zhuǎn)換,但是reflect.Set()方法并不想幫你轉(zhuǎn)懦胞。

一種做法是通過判斷Type.Kind()替久,對(duì)需要轉(zhuǎn)換類型的情況進(jìn)行手動(dòng)處理。

這里采用另一個(gè)辦法躏尉,利用reflect包的兩個(gè)方法蚯根,Type.ConvertibleTo(u Type)用來(lái)判斷能否轉(zhuǎn)換到指定類型,再通過Value.Convert(t Type)來(lái)進(jìn)行類型轉(zhuǎn)換胀糜。

再次優(yōu)化我們的函數(shù):

// 遍歷struct并且自動(dòng)進(jìn)行賦值
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")
    }
    // 遍歷結(jié)構(gòu)體
    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 {
            // 檢查是否需要類型轉(zhuǎn)換
            dataType := reflect.TypeOf(v)
            structType := f.Type()
            if structType == dataType {
                f.Set(reflect.ValueOf(v))
            } else {
                if dataType.ConvertibleTo(structType) {
                    // 轉(zhuǎn)換類型
                    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是否一致僚纷,如果不一致則進(jìn)行轉(zhuǎn)換矩距。運(yùn)行看看結(jié)果:
> fruit: {1001 apple 16}

很棒!這樣預(yù)想的功能都實(shí)現(xiàn)了怖竭,示例代碼中遇到錯(cuò)誤都直接拋出panic,可以根據(jù)實(shí)際項(xiàng)目進(jìn)行調(diào)整陡蝇。

注意到代碼里沒有處理嵌套的結(jié)構(gòu)體等情況痊臭,這部分通過判斷Type為struct時(shí),進(jìn)行遞歸處理就可以實(shí)現(xiàn)登夫。

完整代碼:
GitHub

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末广匙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子恼策,更是在濱河造成了極大的恐慌鸦致,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涣楷,死亡現(xiàn)場(chǎng)離奇詭異分唾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)狮斗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門绽乔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人碳褒,你說(shuō)我怎么就攤上這事折砸】戳疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵睦授,是天一觀的道長(zhǎng)两芳。 經(jīng)常有香客問我,道長(zhǎng)去枷,這世上最難降的妖魔是什么盗扇? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮沉填,結(jié)果婚禮上疗隶,老公的妹妹穿的比我還像新娘。我一直安慰自己翼闹,他們只是感情好斑鼻,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著猎荠,像睡著了一般坚弱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上关摇,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天荒叶,我揣著相機(jī)與錄音,去河邊找鬼输虱。 笑死些楣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宪睹。 我是一名探鬼主播愁茁,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼亭病!你這毒婦竟也來(lái)了鹅很?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤罪帖,失蹤者是張志新(化名)和其女友劉穎促煮,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體整袁,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菠齿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了葬项。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泞当。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出襟士,到底是詐尸還是另有隱情盗飒,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布陋桂,位于F島的核電站逆趣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嗜历。R本人自食惡果不足惜宣渗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梨州。 院中可真熱鬧痕囱,春花似錦、人聲如沸暴匠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)每窖。三九已至帮掉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窒典,已是汗流浹背蟆炊。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瀑志,地道東北人涩搓。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像后室,于是被迫代替她去往敵國(guó)和親缩膝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容