了解和使用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
我們知道int
和float64
可以相互強(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