go 實現一個簡單的文件反序列化器
- 需求
現在有一個文件,文件中的第一行是 "name,address,phone,country,male,age" 表示這個文件的後續內容類型,可以視爲列名。之後的每一行都是這幾部分數據,使用 "," 分割。例如,從第二行開始後續的每一行的內容大致爲:"crastom,hone,111111111,china,true,20"。
如果想要提取這些內容,是不是很簡單,只需要使用:
strings.Split(line, ",")
就可以獲得每一行中的各部分內容。然後把每部分數據賦值給一個結構體,例如:
type Person struct {
Name string
Address string
Phone string
Country string
Fale bool
Age int
}
這樣就完成了,但是如果之後需要解析更多的字段呢,或者需要解析的字段類型出現變化呢。
因此,本文就用 go 實現一種簡單的 Unmarshaler,它可以從文件中 Unmarshal 出所需要的數據,並且不需要寫冗長的賦值語句;可以適用於不同的文件內容。
- 思路
實現的思路也比較簡單:
-
使用第一行 headLine,來初始化一個 Unmarshaler,分析 headLine 中每個 name 對應的位置。例如,headLine 爲 "name,age,address,male",那麼 name 對應 idx 爲 0,age 爲 1,依次類推。
-
實現 Unmarshal 函數時,傳入需要反序列化的一行數據 line 以及存放數據的結構體 ds。結構體中通過字段的 tag 或者字段名獲取該字段的數據在一行中對應的位置。例如,line 爲 "crastom,20,home,true",那麼 crastom 就對應與 headLine 的 name,以此類推。
-
在 Unmarshaler 中,找到數據的位置,從 line 中取出數據,然後通過反射設置 ds 對應字段的內容即可。line 中獲取到的內容都是 string,而 ds 的字段中可能存在多種類型:int、bool、string、float64 等。針對不同的類型,需要設計成爲可註冊的處理方式,這樣遇到對應的類型直接取出對應的 parseFunc 即可處理。
-
golang 實現
3.1. Unmarshaler 數據結構
Unmarshaler 的數據結構定義爲 fln,options 是 fln 的相關配置;total 是 headLine 中的列數;headToIdx 是一個 map,將 headLine 中列名與它的位置 idx 對應起來。
type fln struct {
options *Options
total int
headToIdx 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
}
這個函數用來新建一個 Unmarshaler,傳入的參數 oos 是用來配置 Options 的,Options 和 Option 的定義如下:
// Options 解析參數
type Options struct {
// 文件的第一行,
// 例如:"name,age,country"這些聲明字段
HeadLine string
// 文件中每一行各部分內容
// 的分割符,默認使用"\t"
Spliter string
}
// Option 用來設置options
type Option func(*Options)
// WithHeadLine 向Options中添加headLine
func WithHeadLine(line string) Option {
return func(o *Options) {
o.HeadLine = line
}
}
// WithSpliter 設置options中的spliter
func WithSpliter(spliter string) Option {
return func(o *Options) {
o.Spliter = spliter
}
}
Options 就是 fln 的相關參數配置,而 Option 就是用來處理 Options 的函數,目前有 WithHeadLine 以及 WithSpliter 這兩個函數。
而上面的 parseHeadLine 實現很簡單,就是把 headLine 通過 split 分割成 string 數據,然後映射到 headToIdx 中。
3.3. Unmarshal 實現
方法簽名如下:
func (f *fln) Unmarshal(data []byte, ptr interface{}) error
data 即每一行需要反序列化的數據,ptr 則是一個結構體指針,用來存放數據。
接下來是簡略實現思路:
-
將 data 轉化爲 string 然後分割成 string 數組:datas
-
對 ptr 指向的結構體中字段遍歷,跳過無法設置值的字段。
-
通過字段名或 tag 獲取該字段對應的數據在 datas 中的位置 idx,然後設置該字段的值爲 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.Name
idx := f.getIdxFromName(tagName, fieldName)
if idx == -1 {
continue
}
content := data[idx]
setValue(fieldv, fieldt, content)
}
在上面的 setValue 函數中,首先將 content 轉換爲 fieldt 的類型,然後通過 fieldv.SetXxx 進行設置。
func setValue(fieldv reflect.Value, fieldt reflect.StructField, value string) error {
var err error
defer 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 函數中,在 parseValueFuncs 找到對應的轉換函數 parseFunc:pf,然後用執行 pf。
那 parseValeFuncs 中的 parseFunc 是如何設置的呢?
type parseFunc func(reflect.Value, string) error
var parseValueFuncs map[reflect.Kind]parseFunc
func RegisteParseFunc(pf parseFunc, ks ...reflect.Kind) {
for _, k := range ks {
parseValueFuncs[k] = pf
}
}
在 init 函數中,已經實現了 int、float64、string、bool 等類型的 parseFunc,對於其他的類型,可以自己實現,然後註冊到 fln 中。
3.4. 測試
附加一個簡單的例子,幫助理解。
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)
})
}
- 總結
這個反序列化的工具比較簡單,主要內容就是使用反射設置字段數據。但是 fln 的配置 Options 以及 parseFunc 的註冊還是值得一看的,方便後續新功能的添加。相關代碼見 github:
https://github.com/crazyStrome/file-line-notion
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pW0dfPFLlNr77uMWh2bj9A