go 實現一個簡單的文件反序列化器

  1.  需求

現在有一個文件,文件中的第一行是 "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 出所需要的數據,並且不需要寫冗長的賦值語句;可以適用於不同的文件內容。

  1.  思路

實現的思路也比較簡單:

  1. 使用第一行 headLine,來初始化一個 Unmarshaler,分析 headLine 中每個 name 對應的位置。例如,headLine 爲 "name,age,address,male",那麼 name 對應 idx 爲 0,age 爲 1,依次類推。

  2. 實現 Unmarshal 函數時,傳入需要反序列化的一行數據 line 以及存放數據的結構體 ds。結構體中通過字段的 tag 或者字段名獲取該字段的數據在一行中對應的位置。例如,line 爲 "crastom,20,home,true",那麼 crastom 就對應與 headLine 的 name,以此類推。

  3. 在 Unmarshaler 中,找到數據的位置,從 line 中取出數據,然後通過反射設置 ds 對應字段的內容即可。line 中獲取到的內容都是 string,而 ds 的字段中可能存在多種類型:int、bool、string、float64 等。針對不同的類型,需要設計成爲可註冊的處理方式,這樣遇到對應的類型直接取出對應的 parseFunc 即可處理。

  4.  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 則是一個結構體指針,用來存放數據。

接下來是簡略實現思路:

  1. 將 data 轉化爲 string 然後分割成 string 數組:datas

  2. 對 ptr 指向的結構體中字段遍歷,跳過無法設置值的字段。

  3. 通過字段名或 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)
  })
}
  1.  總結

這個反序列化的工具比較簡單,主要內容就是使用反射設置字段數據。但是 fln 的配置 Options 以及 parseFunc 的註冊還是值得一看的,方便後續新功能的添加。相關代碼見 github:

https://github.com/crazyStrome/file-line-notion

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pW0dfPFLlNr77uMWh2bj9A