我怎样才能使这个对象映射在 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 中更加干燥和可重用?的主要内容,如果未能解决你的问题,请参考以下文章