如何在没有泛型的情况下为此复合类型层次结构建模

时间:2017-08-01 09:27:03

标签: go types

我有一个解析日志文件的系统,该日志文件包含mysql表的变更集,可以想象像binlog这样的东西。可以有更新和插入,删除我们现在忽略。我的模块的功能得到如下输入:

type Changeset struct {
    Table string // which table was affected
    Type string // INSERT or UPDATE
    OldData map[string]string // these 2 fields contain all columns of a table row
    NewData map[string]string
}

OldDataINSERT变更集时为空,当变为UPDATE变更集时,OldDataNewData被填充(数据前后的数据)更新)。

现在我不想在我的模块中使用这样的无类型数据,因为我需要建模一些域,并且有一些类型安全性会更好。但是,如果更改是该域逻辑的插入或更新,我仍然需要保留知识(例如,如果它是更新,我将验证某些字段没有更改,作为示例)。

假设我有两个表(假设它们只有一个名为Id的字段,但实际上它们有更多不同的字段)。所以我像这样建模这些对象:

type Foo struct { // foo table
    Id string
    // ... imagine more fields  here ...
}

type Bar struct { // bar table
    Id string
    // ... imagine more fields  here ...
}

现在我可以从map[string][string]Changeset.OldData映射Changeset.NewData,但是如果更改是插入或更新,我就不知道了。我有点来回思考,但我想出的最好的是:

type FooInsert struct {
    New Foo
}

type FooUpdate struct {
    New Foo
    Old Foo
}

type BarInsert struct {
    New Bar
}

type BarUpdate struct {
    New Bar
    Old Bar
}

映射代码如下所示:

func doMap(c Changeset) interface{} {
    if c.Table == "foo" {
        switch c.Type {
            case "UPDATE":
                return FooUpdate{Old: Foo{Id: c.OldData["id"]}, New: Foo{Id: c.NewData["id"]}}

            case "INSERT":
                return FooInsert{New: Foo{Id: c.NewData["id"]}}
        }
    }

    if c.Table == "bar" {
        switch c.Type {
                // ... almost same as above, but return BarUpdate/BarInsert ...
        }
    }

    return nil
}

好处是,它使我能够在这个映射函数的结果上写一个类型开关,如下所示:

insertChangeset := Changeset{
    Table: "foo",
    Type: "INSERT",
    NewData: map[string]string{"id": "1"},
}

o := doMap(insertChangeset)

switch o.(type) {
    case BarUpdate:
        println("Got an update of table bar")

    case FooUpdate:
        println("Got an update of table foo")

    case BarInsert:
        println("Got an insert to table bar")

    case FooInsert:
        println("Got an insert to table foo")           
}

typeswitch是我最终需要的(每个更改变更集类型和每个实体的不同类型。)但是:

  • doMap中显示的映射代码非常难看且重复。
  • 对于我介绍的每个新实体X,我需要创建另外两种类型XInsertXUpdate

这个烂摊子有什么办法吗?在其他编程语言中,我可能会想到类似的东西:

type Update<T> {
    T Old
    T New
}

type Insert<T> {
    T New
}

但不确定如何在Go中对此进行建模。我还创建了一个游乐场示例,在一个程序中显示整个代码:https://play.golang.org/p/ZMnB5K7RaI

1 个答案:

答案 0 :(得分:0)

看看this solution。这是一种可能的解决方案。

通常:您希望在此处使用接口。在示例中,我使用接口DataRow来存储任何表的行的数据。所有表结构必须实现2个函数,如我的示例所示。 (另请参阅我关于使用泛型的基类中的常规函数​​的说明)

这里的代码再次出现:

package main

import "fmt"

type Foo struct {
    Id string
}

func (s *Foo) Fill(m map[string]string) {
    // If you want to build a general Fill you can build a base struct for Foo, Bar, etc. that works with reflect. 
    // Note that it will be slower than implementing the function here! Ask me if you want one I built recently.

    s.Id = m["id"]
}

func (s *Foo) GetRow() interface{} {
    return nil
}

type Bar struct {
    Id string
}

func (s *Bar) Fill(m map[string]string) {
    s.Id = m["id"]
}

func (s *Bar) GetRow() interface{} {
    return nil
}

type DataRow interface {
    Fill(m map[string]string)
    GetRow() interface{}
}

type Changeset struct {
    Table   string
    Type    string
    OldData map[string]string
    NewData map[string]string
}

type ChangesetTyped struct {
    Table   string
    Type    string
    OldData DataRow
    NewData DataRow
}

func doMap(c Changeset) ChangesetTyped {
    ct := ChangesetTyped{
        Table:   c.Table,
        Type:    c.Type,
        OldData: parseRow(c.Table, c.OldData),
    }

    if c.Type == "UPDATE" {
        ct.NewData = parseRow(c.Table, c.NewData)
    }

    return ct
}

func parseRow(table string, data map[string]string) (row DataRow) {
    if table == "foo" {
        row = &Foo{}
    } else if table == "bar" {
        row = &Bar{}
    }

    row.Fill(data)
    return
}

func main() {
    i := Changeset{
        Table:   "foo",
        Type:    "INSERT",
        NewData: map[string]string{"id": "1"},
    }

    u1 := Changeset{
        Table:   "foo",
        Type:    "UPDATE",
        OldData: map[string]string{"id": "20"},
        NewData: map[string]string{"id": "21"},
    }

    u2 := Changeset{
        Table:   "bar",
        Type:    "UPDATE",
        OldData: map[string]string{"id": "30"},
        NewData: map[string]string{"id": "31"},
    }

    m1 := doMap(i)
    m2 := doMap(u1)
    m3 := doMap(u2)

    fmt.Println(m1, m1.OldData)
    fmt.Println(m2, m2.OldData, m2.NewData)
    fmt.Println(m3, m3.OldData, m3.NewData)
}

如果要将实际行从DataRow转换为正确的类型使用(在此示例中为Foo类型):

foo, ok := dt.GetRow().(Foo)
if !ok {
    fmt.Println("it wasn't of type Foo after all")
}

希望这可以帮助你进行golang任务!