Go項(xiàng)目實(shí)戰(zhàn):實(shí)現(xiàn)一個(gè)簡(jiǎn)單的文件反序列化器
1. 需求
現(xiàn)在有一個(gè)文件,文件中的第一行是 "name,address,phone,country,male,age" 表示這個(gè)文件的后續(xù)內(nèi)容類型,可以視為列名。之后的每一行都是這幾部分?jǐn)?shù)據(jù),使用","分割。例如,從第二行開(kāi)始后續(xù)的每一行的內(nèi)容大致為:"crastom,hone,111111111,china,true,20"。
如果想要提取這些內(nèi)容,是不是很簡(jiǎn)單,只需要使用:
strings.Split(line, ",")就可以獲得每一行中的各部分內(nèi)容。然后把每部分?jǐn)?shù)據(jù)賦值給一個(gè)結(jié)構(gòu)體,例如:
type Person struct {Name stringAddress stringPhone stringCountry stringFale boolAge int}
這樣就完成了,但是如果之后需要解析更多的字段呢,或者需要解析的字段類型出現(xiàn)變化呢。
因此,本文就用go實(shí)現(xiàn)一種簡(jiǎn)單的Unmarshaler,它可以從文件中Unmarshal出所需要的數(shù)據(jù),并且不需要寫冗長(zhǎng)的賦值語(yǔ)句;可以適用于不同的文件內(nèi)容。
2. 思路
實(shí)現(xiàn)的思路也比較簡(jiǎn)單:
使用第一行headLine,來(lái)初始化一個(gè)Unmarshaler,分析headLine中每個(gè)name對(duì)應(yīng)的位置。例如,headLine為"name,age,address,male",那么name對(duì)應(yīng)idx為0,age為1,依次類推。
實(shí)現(xiàn)Unmarshal函數(shù)時(shí),傳入需要反序列化的一行數(shù)據(jù)line以及存放數(shù)據(jù)的結(jié)構(gòu)體ds。結(jié)構(gòu)體中通過(guò)字段的tag或者字段名獲取該字段的數(shù)據(jù)在一行中對(duì)應(yīng)的位置。例如,line為"crastom,20,home,true",那么crastom就對(duì)應(yīng)與headLine的name,以此類推。
在Unmarshaler中,找到數(shù)據(jù)的位置,從line中取出數(shù)據(jù),然后通過(guò)反射設(shè)置ds對(duì)應(yīng)字段的內(nèi)容即可。line中獲取到的內(nèi)容都是string,而ds的字段中可能存在多種類型:int、bool、string、float64等。針對(duì)不同的類型,需要設(shè)計(jì)成為可注冊(cè)的處理方式,這樣遇到對(duì)應(yīng)的類型直接取出對(duì)應(yīng)的parseFunc即可處理。
3. golang實(shí)現(xiàn)
3.1. Unmarshaler數(shù)據(jù)結(jié)構(gòu)
Unmarshaler的數(shù)據(jù)結(jié)構(gòu)定義為fln,options是fln的相關(guān)配置;total是headLine中的列數(shù);headToIdx是一個(gè)map,將headLine中列名與它的位置idx對(duì)應(yīng)起來(lái)。
type fln struct {options *Optionstotal intheadToIdx map[string]int}
3.2. 初始化Unmarshaler
func NewFln(oos ...Option) (Unmarshaler, error) {options := Options{}for _, o := range oos {o(&options)}err := checkOptions(&options)if err != nil {return nil, err}f := &fln{options: &options,headToIdx: make(map[string]int),}err = f.parseHeadLine()if err != nil {return nil, err}return f, nil}
這個(gè)函數(shù)用來(lái)新建一個(gè)Unmarshaler,傳入的參數(shù)oos是用來(lái)配置Options的,Options和Option的定義如下:
// Options 解析參數(shù)type Options struct {// 文件的第一行,// 例如:"name,age,country"這些聲明字段HeadLine string// 文件中每一行各部分內(nèi)容// 的分割符,默認(rèn)使用"\t"Spliter string}// Option 用來(lái)設(shè)置optionstype Option func(*Options)// WithHeadLine 向Options中添加headLinefunc WithHeadLine(line string) Option {return func(o *Options) {o.HeadLine = line}}// WithSpliter 設(shè)置options中的spliterfunc WithSpliter(spliter string) Option {return func(o *Options) {o.Spliter = spliter}}
Options就是fln的相關(guān)參數(shù)配置,而Option就是用來(lái)處理Options的函數(shù),目前有WithHeadLine以及WithSpliter這兩個(gè)函數(shù)。
而上面的parseHeadLine實(shí)現(xiàn)很簡(jiǎn)單,就是把headLine通過(guò)split分割成string數(shù)據(jù),然后映射到headToIdx中。
3.3. Unmarshal實(shí)現(xiàn)
方法簽名如下:
func (f *fln) Unmarshal(data []byte, ptr interface{}) errordata即每一行需要反序列化的數(shù)據(jù),ptr則是一個(gè)結(jié)構(gòu)體指針,用來(lái)存放數(shù)據(jù)。
接下來(lái)是簡(jiǎn)略實(shí)現(xiàn)思路:
將data轉(zhuǎn)化為string然后分割成string數(shù)組:datas
對(duì)ptr指向的結(jié)構(gòu)體中字段遍歷,跳過(guò)無(wú)法設(shè)置值的字段。
通過(guò)字段名或tag獲取該字段對(duì)應(yīng)的數(shù)據(jù)在datas中的位置idx,然后設(shè)置該字段的值為datas[idx]
for i := 0; i < elev.NumField(); i++ {fieldt := elet.Field(i)fieldv := elev.Field(i)if !fieldv.CanSet() {continue}tagName := fieldt.Tag.Get(TAG_NAME)fieldName := fieldt.Nameidx := f.getIdxFromName(tagName, fieldName)if idx == -1 {continue}content := data[idx]setValue(fieldv, fieldt, content)}
在上面的setValue函數(shù)中,首先將content轉(zhuǎn)換為fieldt的類型,然后通過(guò)fieldv.SetXxx進(jìn)行設(shè)置。
func setValue(fieldv reflect.Value, fieldt reflect.StructField, value string) error {var err errordefer func() {if err != nil {err = fmt.Errorf("error from setValue: %+v", err)}}()pf, ok := parseValueFuncs[fieldt.Type.Kind()]if !ok {return fmt.Errorf("not suppoted type: %+v", fieldt.Type)}err = pf(fieldv, value)return err}
setValue函數(shù)中,在parseValueFuncs找到對(duì)應(yīng)的轉(zhuǎn)換函數(shù)parseFunc:pf,然后用執(zhí)行pf。
那parseValeFuncs中的parseFunc是如何設(shè)置的呢?
type parseFunc func(reflect.Value, string) errorvar parseValueFuncs map[reflect.Kind]parseFuncfunc RegisteParseFunc(pf parseFunc, ks ...reflect.Kind) {for _, k := range ks {parseValueFuncs[k] = pf}}
在init函數(shù)中,已經(jīng)實(shí)現(xiàn)了int、float64、string、bool等類型的parseFunc,對(duì)于其他的類型,可以自己實(shí)現(xiàn),然后注冊(cè)到fln中。
3.4. 測(cè)試
附加一個(gè)簡(jiǎn)單的例子,幫助理解。
type DS struct {Name string `fln:"myname"`MyAge int `fln:"age"`Address string `fln:"address"`Male bool `fln:"mymale"`}func TestNewFLN(t *testing.T) {head := "name,age,address,male"data := "crastom,10,home,true"want := DS{Name: "crastom",MyAge: 10,Address: "home",Male: true,}convey.Convey("test_new_fln", t, func() {f, err := NewFln(WithHeadLine(head),WithSpliter(","),)convey.So(f, convey.ShouldNotBeNil)convey.So(err, convey.ShouldBeNil)ds := DS{}err = f.Unmarshal([]byte(data), &ds)convey.So(err, convey.ShouldBeNil)convey.So(reflect.DeepEqual(want, ds), convey.ShouldBeTrue)t.Logf("%+v", ds)})}
4. 總結(jié)
這個(gè)反序列化的工具比較簡(jiǎn)單,主要內(nèi)容就是使用反射設(shè)置字段數(shù)據(jù)。但是fln的配置Options以及parseFunc的注冊(cè)還是值得一看的,方便后續(xù)新功能的添加。相關(guān)代碼見(jiàn)github:
https://github.com/crazyStrome/file-line-notion
推薦閱讀
