我怎样才能使这个对象映射在 Go 中更加干燥和可重用?

Posted

技术标签:

【中文标题】我怎样才能使这个对象映射在 Go 中更加干燥和可重用?【英文标题】:How can I make this object mapping more dry and reusable in Go? 【发布时间】:2016-02-07 22:47:20 【问题描述】:

我在 Go 中创建了一个非关系的对象映射,非常简单。

我有几个看起来像这样的结构:

type Message struct 
    Id       int64
    Message  string
    ReplyTo  sql.NullInt64 `db:"reply_to"`
    FromId   int64         `db:"from_id"`
    ToId     int64         `db:"to_id"`
    IsActive bool          `db:"is_active"`
    SentTime int64         `db:"sent_time"`
    IsViewed bool          `db:"is_viewed"`

    Method   string `db:"-"`
    AppendTo int64  `db:"-"`

要创建一条新消息,我只需运行此函数:

func New() *Message 
    return &Message
        IsActive: true,
        SentTime: time.Now().Unix(),
        Method:   "new",
    

然后我有一个用于这个结构的 message_crud.go 文件,如下所示:

要按唯一列(例如按 id)查找消息,我运行此函数:

func ByUnique(column string, value interface) (*Message, error) 

    query := fmt.Sprintf(`
        SELECT *
        FROM message
        WHERE %s = ?
        LIMIT 1;
    `, column)

    message := &Message
    err := sql.DB.QueryRowx(query, value).StructScan(message)
    if err != nil 
        return nil, err
    
    return message, nil

为了保存消息(在数据库中插入或更新),我运行这个方法:

func (this *Message) save() error 
    s := ""
    if this.Id == 0 
        s = "INSERT INTO message SET %s;"
     else 
        s = "UPDATE message SET %s WHERE id=:id;"
    
    query := fmt.Sprintf(s, sql.PlaceholderPairs(this))

    nstmt, err := sql.DB.PrepareNamed(query)
    if err != nil 
        return err
    

    res, err := nstmt.Exec(*this)
    if err != nil 
        return err
    

    if this.Id == 0 
        lastId, err := res.LastInsertId()
        if err != nil 
            return err
        
        this.Id = lastId
    

    return nil

sql.PlaceholderPairs() 函数如下所示:

func PlaceholderPairs(i interface) string 

    s := ""
    val := reflect.ValueOf(i).Elem()
    count := val.NumField()

    for i := 0; i < count; i++ 
        typeField := val.Type().Field(i)
        tag := typeField.Tag

        fname := strings.ToLower(typeField.Name)

        if fname == "id" 
            continue
        

        if t := tag.Get("db"); t == "-" 
            continue
         else if t != "" 
            s += t + "=:" + t
         else 
            s += fname + "=:" + fname
        
        s += ", "
    
    s = s[:len(s)-2]
    return s

但是每次我创建一个新结构时,例如一个用户结构,我必须复制粘贴上面的“crud section”并创建一个 user_crud.go 文件并将“消息”一词替换为“用户”,然后将这些词“消息”与“用户”。我重复了很多代码,它不是很干。有什么办法可以避免重复使用这些代码吗?我总是有一个 save() 方法,并且总是有一个函数 ByUnique() ,我可以在其中返回一个结构并按唯一列搜索。

php 中这很容易,因为 PHP 不是静态类型的。

这可以在 Go 中实现吗?

【问题讨论】:

Don't use generic names such as "me", "this" or "self", identifiers typical of object-oriented languages that place more emphasis on methods as opposed to functions. - github.com/golang/go/wiki/CodeReviewComments#receiver-names 【参考方案1】:

您的ByUnique 几乎已经是通用的了。只需拉出变化的部分(桌子和目的地):

func ByUnique(table string, column string, value interface, dest interface) error 
    query := fmt.Sprintf(`
            SELECT *
            FROM %s
            WHERE %s = ?
            LIMIT 1;
        `, table, column)

    return sql.DB.QueryRowx(query, value).StructScan(dest)


func ByUniqueMessage(column string, value interface) (*Message, error) 
    message := &Message
    if err := ByUnique("message", column, value, &message); err != nil 
        return nil, err
    
    return message, error

您的save 非常相似。您只需要按照以下方式制作一个通用的保存功能:

func Save(table string, identifier int64, source interface)  ... 

然后在(*Message)save 内部,您只需调用通用Save() 函数。看起来很简单。

旁注:不要使用this 作为方法内对象的名称。有关更多信息,请参阅@OneOfOne 的链接。不要沉迷于 DRY。它本身并不是一个目标。 Go 专注于代码简单、清晰和可靠。不要为了避免键入简单的错误处理行而创建复杂而脆弱的东西。这并不意味着您不应该提取重复的代码。这只是意味着在 Go 中,通常最好重复一些简单的代码,而不是创建复杂的代码来避免它。


编辑:如果您想使用接口实现Save,那没问题。只需创建一个Identifier 接口即可。

type Ider interface 
    Id() int64
    SetId(newId int64)


func (msg *Message) Id() int64 
    return msg.Id


func (msg *Message) SetId(newId int64) 
    msg.Id = newId


func Save(table string, source Ider) error 
    s := ""
    if source.Id() == 0 
        s = fmt.Sprintf("INSERT INTO %s SET %%s;", table)
     else 
        s = fmt.Sprintf("UPDATE %s SET %%s WHERE id=:id;", table)
    
    query := fmt.Sprintf(s, sql.PlaceholderPairs(source))

    nstmt, err := sql.DB.PrepareNamed(query)
    if err != nil 
        return err
    

    res, err := nstmt.Exec(source)
    if err != nil 
        return err
    

    if source.Id() == 0 
        lastId, err := res.LastInsertId()
        if err != nil 
            return err
        
        source.SetId(lastId)
    

    return nil


func (msg *Message) save() error 
    return Save("message", msg)

可能与此有关的一件事情是对Exec 的调用。我不知道你使用的是什么包,如果你传递一个接口而不是实际的结构,Exec 可能无法正常工作,但它可能会工作。也就是说,我可能只是传递标识符而不是添加此开销。

【讨论】:

上次的Save函数我不太明白。你怎么会在函数中传入一个标识符参数?不能从源中提取id吗?像 source.id 一样。还是因为它是一个接口,所以语法会有所不同? @Alex 当然,您可以轻松创建标识符协议。我最初是这样写的,但它添加了另一层,我不知道你所有的项目是否都有一个 Id 字段。无论哪种方式都可以。 我仍然不明白如何在 Save() 函数中使用它。抱歉,我需要做一些额外的解释才能完全理解如何在 Save 函数中使用标识符,以及如何使用标识符协议。感谢您的帮助,我非常感谢您的帮助! 我正在使用“github.com/jmoiron/sqlx”。但是在上面的代码中,您首先执行的是:if source.Id == 0,然后再执行 if source.Id() == 0。为什么要使用两种不同的方式来检查 id?我也不明白为什么你这样做时有一个 setId 方法:source.Id = lastId,在代码中。 对不起;那应该是source.Id()source.SetId(lastId)。固定。【参考方案2】:

您可能想要使用 ORM。 它们消除了您所描述的大量样板代码。

请参阅 this question 了解“什么是 ORM?”

这里是 go 的 ORM 列表:https://github.com/avelino/awesome-go#orm

我自己从来没有用过,所以我不能推荐任何一个。主要原因是 ORM 从开发人员那里获得了很多控制权,并引入了不可忽略的性能开销。您需要亲自查看它们是否适合您的用例和/或您是否对这些库中正在发生的“魔力”感到满意。

【讨论】:

【参考方案3】:

我不建议这样做,我个人更喜欢明确地扫描到结构和创建查询。

但如果你真的想坚持反思,你可以这样做:

func ByUnique(obj interface, column string, value interface) error 
    // ...
    return sql.DB.QueryRowx(query, value).StructScan(obj)


// Call with
message := &Message
ByUnique(message, ...)

为了您的节省:

type Identifiable interface 
    Id() int64


// Implement Identifiable for message, etc.

func Save(obj Identifiable) error 
    // ...


// Call with
Save(message)

我使用并向您推荐的方法:

type Redirect struct 
    ID        string
    URL       string
    CreatedAt time.Time


func FindByID(db *sql.DB, id string) (*Redirect, error) 
    var redirect Redirect

    err := db.QueryRow(
        `SELECT "id", "url", "created_at" FROM "redirect" WHERE "id" = $1`, id).
        Scan(&redirect.ID, &redirect.URL, &redirect.CreatedAt)

    switch 
    case err == sql.ErrNoRows:
        return nil, nil
    case err != nil:
        return nil, err
    

    return &redirect, nil


func Save(db *sql.DB, redirect *Redirect) error 
    redirect.CreatedAt = time.Now()

    _, err := db.Exec(
        `INSERT INTO "redirect" ("id", "url", "created_at") VALUES ($1, $2, $3)`,
        redirect.ID, redirect.URL, redirect.CreatedAt)

    return err

这样做的好处是使用类型系统并且只映射它应该实际映射的东西。

【讨论】:

我不太了解 Identifiable 界面。您将如何在实践中使用 Save() 函数?顺便谢谢!

以上是关于我怎样才能使这个对象映射在 Go 中更加干燥和可重用?的主要内容,如果未能解决你的问题,请参考以下文章

具有多个命令和可重入的 BASH 变量

如何在函数式编程映射方法中模拟“返回”语句(或其他使该代码工作和可读的方法)[关闭]

线程安全和可重入函数

使用标题破坏

我怎样才能使这个查询 sargable?

不可重入和可重入函数概述使用信号避免僵尸进程