取消正在进行中的数据库操作

本文翻译自《Canceling in-progress operations》。 你可以使用Go的context.Context管理正在进行中的操作。Context是一个标准的Go类型,可用来报告它所代表的整体操作是否已被取消并且不再需要。通过在你的应用程序中跨函数或服务传递context.Context,可以提前停止工作并返回一个错误。有关Context的更多信息,请参阅Go并发模式:Context

例如,你可能希望:

  • 结束长时间运行的操作,包括耗时过长的数据库操作。
  • 传播来自其他地方的“取消继续工作”请求,例如当客户端关闭连接时。

Go的许多API都采用Context参数,使你更容易在整个应用程序中使用Context

超时后取消正在进行中的数据库操作

你可以使用Context来设置超时时间或截止时间,在此之后操作将被取消。要派生具有超时时间或截止时间的Context,请调用Context.WithTimeoutContext.WithDeadline函数。

以下示例中的代码派生一个附带超时时间的Context,并将其传递到sql.DBQueryContext方法中。

func QueryWithTimeout(ctx context.Context) {
    // 创建一个附带超时时间的Context。
    queryCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // 传入这个附带超时时间的Context和一个查询语句。
    rows, err := db.QueryContext(queryCtx, "SELECT * FROM album")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // 处理返回的行数据。
}

当一个context从外部的context派生而来时,由于queryCtx在本例中是从函数参数ctx派生的,如果外部的context被取消,那么派生的context也会自动取消。例如,在HTTP服务器中,HTTP.Request.Context方法返回与请求相关联的context。如果HTTP客户端断开连接或取消HTTP请求(可能使用HTTP/2),则该context将被取消。如果整个HTTP请求被取消,或者查询耗时超过五秒,那么将HTTP请求的context传递给上面的QueryWithTimeout函数将导致数据库查询操作提前结束。

注意:当你在创建带有超时时间或截止时间的一个新context时,总是应该defer对这个context的cancel方法的调用。当包含这个新context的函数退出时,就能释放这个新context所持有的资源。它还将取消queryCtx,当这个新context的cancel方法返回时,就不应该再使用queryCtx

执行事务

本文翻译自《Executing transactions》。

目录

最佳实践

例子

你可以使用代表事务的sql.Tx结构体执行数据库事务操作。除了表示事务特定语义的CommitRollback方法之外,sql.Tx还具有用来执行常见数据库操作的所有方法。要获取sql.Tx,你可以调用DB.BeginDB.BeginTx方法。

数据库事务将多个数据库操作合成一组,作为更大目标的一部分。其中的所有数据库操作都必须成功执行或都不执行,在任何一种情况下都会保持数据的完整性。 通常,事务操作的流程包括:

1 开始事务。

2 执行一组数据库操作。

3 如果没有错误发生,提交事务以进行数据库更改。

4 如果发生错误,回滚事务以保持数据库不变。

sql包提供了开始和结束事务的方法,以及执行中间数据库操作的方法。这些方法对应于上述流程中的四个步骤。

1 开始事务。

DB.BeginDB.BeginTx开始一个新的数据库事务,返回一个代表它的sql.Tx结构体。

2 执行数据库操作。

使用sql.Tx,你可以在单个数据库连接中查询或更新数据库。为了支持这一点,Tx导出了以下方法:

3 使用以下其中一项结束事务:

如果Commit方法执行成功(返回nil错误),则所有查询结果都被确认为有效,并且所有已执行的更新都作为单个原子更改应用于数据库。如果Commit方法执行失败,则TxQueryExec的所有结果都应被视为无效而丢弃。

即使Tx.Rollback方法执行失败,事务也不再有效,也不会提交到数据库。

最佳实践

遵循以下最佳实践,以更好地应对事务有时需要的复杂语义和连接管理。

  • 使用本节中描述的API来管理事务。不要直接使用BEGINCOMMIT等与事务相关的SQL语句——这样做会使你的数据库处于不可预测的状态,尤其是在并发程序中。
  • 使用事务的过程中,请注意不要直接调用非事务的sql.DB的方法,因为它们会在事务外部执行,从而使你的代码对数据库状态的看法不一致,甚至会导致死锁。

例子

以下示例中的函数使用事务为相册(album)创建新的客户订单(customer order)。在此过程中,该函数的代码将:

1 开始事务。

2 延迟事务的回滚。如果事务执行成功,它将在该函数退出之前提交,使延迟回滚调用(defer tx.Rollback())成为空操作。如果事务执行失败,则不会提交,这意味着回滚将在函数退出时被调用。

3 确认客户订购的专辑有足够的库存(inventory)。

4 如果足够,更新库存数量,减少订购的专辑数目。

5 创建一个新订单并获取新订单为客户生成的ID。

6 提交事务并返回这个ID。

此示例的Tx方法需要一个context.Context参数。使得函数的执行(包括数据库操作)在运行时间过长或客户端连接关闭时被取消。有关更多信息,请参阅取消正在进行中的数据库操作

// CreateOrder函数为相册(album)创建一个新的客户订单(customer order)并返回这个新订单的ID。
func CreateOrder(ctx context.Context, albumID, quantity, custID int) (orderID int64, err error) {

    // 创建一个帮助函数用于返回失败的结果。
    fail := func(err error) (int64, error) {
        return 0, fmt.Errorf("CreateOrder: %v", err)
    }

    // 获取一个事务实例tx用来执行事务操作。
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fail(err)
    }
    // 延迟执行一个回滚操作,以防在此事务中某些操作执行失败
    defer tx.Rollback()

    // 确认相册库存足以下这个订单。
    var enough bool
    if err = tx.QueryRowContext(ctx, "SELECT (quantity >= ?) from album where id = ?",
        quantity, albumID).Scan(&enough); err != nil {
        if err == sql.ErrNoRows {
            return fail(fmt.Errorf("no such album"))
        }
        return fail(err)
    }
    if !enough {
        return fail(fmt.Errorf("not enough inventory"))
    }

    // 更新相册库存以减去本订单中的相册数目。
    _, err = tx.ExecContext(ctx, "UPDATE album SET quantity = quantity - ? WHERE id = ?",
        quantity, albumID)
    if err != nil {
        return fail(err)
    }

    // 在album_order表中创建一个新行。
    result, err := tx.ExecContext(ctx, "INSERT INTO album_order (album_id, cust_id, quantity, date) VALUES (?, ?, ?, ?)",
        albumID, custID, quantity, time.Now())
    if err != nil {
        return fail(err)
    }
    // 获得刚刚创建的订单条目的ID。
    orderID, err = result.LastInsertId()
    if err != nil {
        return fail(err)
    }

    // 提交这个事务。
    if err = tx.Commit(); err != nil {
        return fail(err)
    }

    // 返回这个订单的ID。
    return orderID, nil
}

TSV文件和CSV文件有什么区别

TSV文件和CSV文件都是文本文件。

TSV,Tab-separated values,TSV文件的每一行用制表符(按Tab键输入’\t’)作为字段值的分隔符。

CSV,Comma-separated values,CSV文件的每一行用半角逗号(’,’)作为字段值的分隔符。

查询数据

本文翻译自《Querying for data》。

目录

查询单行

查询多行

处理可null列的值

从列中获取数据

处理多个结果集

执行返回行数据的SQL语句时,请使用database/sql包中提供的Query方法之一。其中每一个都返回一行或多行,你可以使用Scan方法将其数据复制到变量中。例如,你可以使用这些方法来执行SELECT语句。

执行不返回行数据的语句,可以改用ExecExecContext方法。有关详细信息,请参阅执行不返回数据的SQL语句

database/sql包提供了两种执行查询并返回结果的方法:

  • 查询单行QueryRow方法从数据库中最多返回一行作为一个Row结构体。有关详细信息,请参阅查询单行
  • 查询多行Query方法返回所有匹配的行作为代码可以循环的Rows结构体。有关详细信息,请参阅查询多行

如果你的代码将重复执行相同的SQL语句,请考虑使用准备好的语句(译者注:预处理语句)。有关详细信息,请参阅使用准备好的语句

注意:不要使用fmt.Sprintf等字符串拼接函数来组装SQL语句!否则你可能会引入SQL注入风险。有关更多信息,请参阅避免SQL注入风险

查询单行

QueryRow函数最多检索单个数据库行,例如通过唯一ID查找数据时。如果查询返回多行,则Scan方法会丢弃除第一行以外的所有行。

QueryRowContext函数的工作方式类似于QueryRow,但带有context.Context参数。有关更多信息,请参阅取消正在进行的数据库操作

以下示例使用查询来确定是否有足够的库存来支持购买。如果足够,则SQL语句返回true,否则返回falseRow.Scan方法通过指针将布尔返回值复制到enough变量中。

func canPurchase(id int, quantity int) (bool, error) {
    var enough bool
    // 查询基于单行的值。
    if err := db.QueryRow("SELECT (quantity >= ?) from album where id = ?",
        quantity, id).Scan(&enough); err != nil {
        if err == sql.ErrNoRows {
            return false, fmt.Errorf("canPurchase %d: unknown album", id)
        }
        return false, fmt.Errorf("canPurchase %d: %v", id)
    }
    return enough, nil
}

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而MySQL驱动程序使用?号。

处理错误

QueryRow函数本身不返回错误。相反,Scan方法会报告查找和扫描中的任何错误。当查询未找到任何行时,它返回sql.ErrNoRows错误。

以下列出返回单行的函数

DB.QueryRow

DB.QueryRowContext

单独运行单行查询。

Tx.QueryRow

Tx.QueryRowContext

在较大的事务中运行单行查询。有关更多信息,请参阅执行事务

Stmt.QueryRow

Stmt.QueryRowContext

使用一个准备好的语句运行单行查询。有关更多信息,请参阅使用准备好的语句。

Conn.QueryRowContext

与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接

查询多行

你可以使用QueryQueryContext函数查询多行,它们返回表示查询结果的Rows结构体。你的代码使用rows.Next方法对返回的行进行迭代。每次迭代都调用Scan方法将列值复制到变量中。

QueryContext函数的工作方式与Query类似,但带有context.Context参数。有关更多信息,请参阅取消正在进行中的数据库操作

以下示例执行查询以返回指定艺术家(artist)的专辑(album)信息。albumsql.Rows中返回。以下代码使用Rows.Scan函数将列值复制到由指针指出的变量中。

func albumsByArtist(artist string) ([]Album, error) {
    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    // 一个切片用来存储返回的行数据。
    var albums []Album

    // 循环遍历行数据,使用Scan方法把每一行的各列的值,赋值给一个结构体变量的对应字段。Loop through rows, using Scan to assign column data to struct fields.
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist,
            &alb.Price, &alb.Quantity); err != nil {
            return albums, err
        }
        albums = append(albums, album)
    }
    if err = rows.Err(); err != nil {
        return albums, err
    }
    return albums, nil
}

请注意对rows.Close函数的延迟调用。无论函数如何返回,这都会释放sql.Rows占有的所有资源。虽然循环遍历完毕所有的行也会隐式关闭它,但最好使用defer来确保无论如何都能关闭。

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而MySQL驱动程序使用?号。

处理错误

请务必在遍历查询结果后检查sql.Rows中是否有错误发生。如果查询失败,这就是你的代码发现错误的方式。

以下列出返回多行数据的函数

DB.Query

DB.QueryContext

单独运行查询。

Tx.Query

Tx.QueryContext

在较大的事务中执行查询。有关更多信息,请参阅执行事务

Stmt.Query

Stmt.QueryContext

使用一个准备好的语句运行查询。有关更多信息,请参阅使用准备好的语句。

Conn.QueryContext

与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接

处理可null列的值

当列的值可能为null时,database/sql包提供了几种特殊类型,可以用作Scan函数的参数。每个特殊类型都包括一个Valid字段,该字段报告列值是否为非null,如果为非null则该类型有另一个字段包含该列值。

以下示例中的代码用于查询客户名称。如果名称值为null,则代码将用另一个值替换它,以在应用程序中使用。

var s sql.NullString
err := db.QueryRow("SELECT name FROM customer WHERE id = ?", id).Scan(&s)
if err != nil {
    log.Fatal(err)
}

name := "Valued Customer"
// 如果客户名称不为null(合法,s.Valid的值为true),那么赋值给name变量
if s.Valid {
    name = s.String
}

请参阅sql包关于每个特殊类型的更多信息:

从列中获取数据

在查询返回的行上循环遍历时,可以使用Scan函数将行的列值复制到Go值中,例如rows.Scan的参考文档中所述。

所有驱动程序都支持一组基本的数据类型转换,例如将SQL的INT转换为Go的INT。一些驱动程序扩展了这组转换;有关详细信息,请参阅每个驱动程序的文档。

正如你所料,Scan函数将把列类型转换为类似的Go类型。例如,Scan将把SQL的CHARVARCHARTEXT转换为Go字符串。但是,Scan还可以把列值转换为另一个非常适合的Go类型。例如,如果列是始终包含数字的VARCHAR,则可以指定数字Go类型(例如int)来接收值,Scan将使用strconv.Atoi函数进行转换。

有关Scan函数进行类型转换的更多详细信息,请参阅Rows.Scan参考文档。

处理多个结果集

当数据库操作可能返回多个结果集时,你可以使用Rows.NextResultSet函数来检索这些结果集。例如,当你发送分别查询多个表的一个SQL语句,并为其中每个表都返回一个结果集时,这可能很有用。

Rows.NextResultSet函数会准备下一个结果集,以便调用Rows.Next函数从下一个集合中检索第一行。它返回一个布尔值,指示是否存在下一个结果集。

以下示例中的代码使用DB.Query函数来执行两条Select语句。第一个结果集来自第一个Select查询,检索相簿album表中的所有行。下一个结果集来自第二个Select查询,从歌曲表song中检索行。

rows, err := db.Query("SELECT * from album; SELECT * from song;")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// 循环遍历第一个结果集。
for rows.Next() {
    // 处理结果集…
}

// 前进到下一个结果集。
rows.NextResultSet()

// 循环遍历第二个结果集。
for rows.Next() {
    // 处理结果集…
}

// 检查在处理所有结果集的过程中,是否有错误发生。
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

避免SQL注入风险

本文翻译自《Avoiding SQL injection risk》。

你可以通过提供SQL参数值作为sql包里函数的参数来避免SQL注入风险。sql包中的许多函数为SQL语句(包括准备语句)的参数提供参数值。

以下示例中的代码使用?作为SQL语句的参数的占位符,由Query函数的id参数提供值:

// 使用参数执行一个SQL语句的正确格式
rows, err := db.Query("SELECT * FROM user WHERE id = ?", id)

执行数据库操作的sql包的函数根据你提供的参数创建SQL准备语句。在运行时,sql包将SQL语句转换为准备语句并将其与参数分开发送。

注意:参数占位符因你使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序使用$1形式的占位符形式,而不是?号。

你可能想使用fmt包中的函数将SQL语句组装为包含参数值的字符串——如下所示:

// 有安全风险!
rows, err := db.Query(fmt.Sprintf("SELECT * FROM user WHERE id = %s", id))

这不安全!执行此操作时,Go会组装整个SQL语句,在将完整语句发送到DBMS之前用参数值替换%s格式动词。这会带来SQL注入风险,因为代码的调用者可能会发送意外的SQL片段作为id参数。该片段可能会以不可预测的方式执行SQL语句,这对你的应用程序是危险的。

例如,通过传递一个特定的%s值,你可能会得到类似于以下内容的SQL语句,它可能会返回数据库中的所有用户记录:

SELECT * FROM user WHERE id = 1 OR 1=1;

使用准备好的语句(prepared statement)

本文翻译自《Using prepared statements》。

目录

什么是准备好的语句?

如何使用准备好的语句

准备好的语句的行为

你可以定义准备好的语句以供重复使用。这可以避免每次代码执行数据库操作时重新创建语句的额外开销,从而帮助你的代码运行得更快一些。

注意:准备好的语句中的参数占位符因你使用的DBMS和驱动程序而异。例如Postgres的pq驱动程序需要$1形式的占位符,而不是?号占位符。

什么是准备好的语句?

准备好的语句是由DBMS解析和保存的SQL,通常包含占位符但没有实际参数值。稍后,可以使用一组参数值来执行该语句。(译者注:准备好的语句,准备语句,也叫预处理语句。)

如何使用准备好的语句

当你希望重复执行同一条SQL时,可以使用一个sql.Stmt结构体预先准备好SQL语句,然后按需执行。

以下示例创建一个准备好的语句,从数据库中选择一个特定的专辑(album)的信息。DB.Prepare函数返回一个sql.Stmt结构体,代表给定SQL文本的准备语句。你可以将SQL语句的参数值传递给Stmt.ExecStmt.QueryRowStmt.Query方法以运行该语句。

// AlbumByID函数返回一个特定的专辑(album)的信息。
func AlbumByID(id int) (Album, error) {
    // 定义准备好的语句。你通常会在别处定义该语句并将其保存以供在诸如本函数之类的函数中使用。
    stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
    if err != nil {
        log.Fatal(err)
    }

    var album Album

    // 执行准备好的语句,为占位符为?的参数传入一个id值。
    err := stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
    if err != nil {
        if err == sql.ErrNoRows {
            // 处理没有一行数据返回的情况。
        }
        return album, err
    }
    return album, nil
}

准备好的语句的行为

准备好的sql.Stmt提供常用的ExecQueryRowQuery方法来调用语句。有关使用这些方法的更多信息,请参阅查询数据执行不返回数据的SQL语句

但是,由于sql.Stmt已经代表了一条预设的SQL语句,所以它的ExecQueryRowQuery方法只需占位符对应的参数值,省略了SQL文本。

你可以用不同的方式定义一个新的sql.Stmt,这取决于你将如何使用它。

  • DB.PrepareDB.PrepareContext方法创建一个准备好的语句,它可以在事务之外单独执行,就像DB.ExecDB.Query方法一样。
  • Tx.PrepareTx.PrepareContextTx.StmtTx.StmtContext方法创建用于特定事务的准备好的语句。PreparePrepareContext方法使用SQL文本来定义语句。StmtStmtContext方法使用DB.PrepareDB.PrepareContext方法的结果。也就是说,它们将一个非事务的sql.Stmt转换为一个事务的sql.Stmt。
  • Conn.PrepareContext方法从sql.Conn方法创建准备好的语句,表示专用的连接。

当你的代码结束时,请确保调用stmt.Close方法。这将释放可能与其关联的任何数据库资源(例如底层连接)。对于函数中仅作为局部变量的语句,defer stmt.Close()就足够了。

用于创建准备好的语句的函数

DB.Prepare
DB.PrepareContext

准备一个单独执行的语句,或者使用Tx.Stmt方法把它转换为事务中的准备语句。

Tx.Prepare
Tx.PrepareContext
Tx.Stmt
Tx.StmtContext

准备用于特定事务的语句。有关更多信息,请参阅执行事务

Conn.PrepareContext

与专用的连接一起使用。有关更多信息,请参阅管理数据库连接

管理数据库连接

本文翻译自《Managing connections》。

目录

设置连接池属性

使用专用连接

对于绝大多数程序,你都无需调整sql.DB连接池的默认值。但对于某些高级程序,你可能需要调整连接池参数或显式地处理连接。本文教你如何操作。

sql.DB数据库句柄对于多个goroutine并发使用是安全的(这意味着该句柄是其他编程语言可能称之为“线程安全(thread-safe)”的)。其他一些数据库访问库基于每次只能进行一个操作的数据库连接方式。为了弥补这一差距,每个sql.DB都管理一个连接到底层数据库的活动连接池,根据需要可以在Go程序中创建新的并行的连接。

连接池适用于大多数的数据库访问需求。当你调用sql.DBQueryExec方法时,sql.DB会从池中检索可用的连接,或者在需要时创建一个连接。当不再需要连接时,它会将连接返回到池。这种方式能支持大规模并行访问数据库。

设置连接池属性

你可以设置属性来指导sql包如何管理连接池。要获取有关这些属性效果的统计信息,请使用DB.Stats方法。

设置能打开的连接的最大数量

DB.SetMaxOpenConns方法对打开的连接数量进行了限制。超过此限制,新的数据库操作将等待现有操作完成,然后sql.DB将创建另一个连接。默认情况下,当需要连接时,sql.DB会在所有现有连接都在使用时创建一个新连接。

请记住,设置连接数限制会让Go程序连接数据库时使用类似于获取锁或信号量的机制,从而导致程序在等待新的数据库连接时可能会被阻塞等待。

设置空闲连接的最大数量

DB.SetMaxIdleConns方法更改sql.DB维护的最大空闲连接数。

当SQL操作在给定的数据库连接上完成时,通常不会立即关闭这个连接:应用程序可能很快再次需要使用该连接,保持打开的连接可以避免在下一次操作时重新连接到数据库。默认情况下,sql.DB在任何给定时刻保持两个空闲连接。提高最大空闲连接数可以避免在大规模并行程序中频繁重新建立连接。

设置连接可以空闲的最大时间

DB.SetConnMaxIdleTime方法设置连接在关闭之前可以处于空闲状态的最大时间长度。这会导致sql.DB关闭空闲时间超过给定持续时间的连接。

默认情况下,当一个空闲连接被添加到连接池时,它会一直保持在那里,直到再次需要它为止。使用DB.SetMaxIdleConns函数来增加并行活动突发期间允许的空闲连接数,也可以使用DB.SetConnMaxIdleTime函数来安排稍后在系统安静时释放这些连接。

设置连接的最大生存时间

使用DB.SetConnMaxLifetime函数设置连接在关闭之前可以保持打开的最大时间长度。

默认情况下,连接可以在任意长的时间内使用和重用,但要遵守上述限制。在某些系统中,例如使用负载均衡数据库服务器的系统,确保应用程序在不重新连接的情况下永远不会使用特定连接太长时间,这可能会很有用。

使用专用连接

当某个数据库可能为在一个特殊连接上执行的一系列操作指定隐式含义时,database/sql包提供可以使用的函数。

最常见的例子是事务,通常以BEGIN命令开始,以COMMITROLLBACK命令结束,并在这个事务的连接上发出所有命令(包括BEGINCOMMITROLLBACK这几个命令)。对于这个例子,请使用sql包的事务支持。请参阅执行事务

对于其他必须在同一个连接上执行一系列单独操作的例子,sql包提供了专用连接。DB.Conn获得一个专用连接sql.Connsql.ConnBeginTxExecContextPingContextPrepareContextQueryContextQueryRowContext方法,它们的行为与DB上的等效方法类似,但只使用专用的连接。专用连接使用完毕后,你的代码必须使用Conn.Close将其释放。

执行不返回行数据的SQL语句

本文翻译自《Executing SQL statements that don’t return data》。

执行不返回数据的数据库操作时,请使用database/sql包中的ExecExecContext方法。以这种方式执行的SQL语句包括INSERTDELETEUPDATE

当查询可能返回行数据时,请改QueryQueryContext方法。有关详细信息,请参阅查询一个数据库

ExecContext方法的工作原理与Exec方法相同,但有一个额外的context.Context参数,具体作用参见“取消正在进行中的数据库操作”。

以下示例中的代码使用DB.Exec执行一条语句,将一条唱片的信息添加到album表中。

func AddAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist) VALUES (?, ?)", alb.Title, alb.Artist)
    if err != nil {
        return 0, fmt.Errorf("AddAlbum: %v", err)
    }

    // 获取新插入的这条记录的ID
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("AddAlbum: %v", err)
    }
    // 返回这条新记录的ID
    return id, nil
}

DB.Exec返回值:一个sql.Result和一个指示是否有错误发生的变量。当错误为nil时,你可以使用Result来获取最后插入项的ID(如示例中所示)或获取数据表中受操作影响的行数。

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而不是?

如果你的代码将重复执行相同的SQL语句,请考虑使用SQL.Stmt从SQL语句中创建一个可重复使用的准备语句。有关详细信息,请参阅使用准备好的语句

注意:不要使用fmt.Sprintf等字符串格式函数来组装SQL语句!因为可能会有SQL注入风险。有关更多信息,请参阅避免SQL注入风险

以下列出用于执行不返回行数据的SQL语句的函数

DB.Exec

DB.ExecContext

孤立地执行单个SQL语句。

Tx.Exec

Tx.ExecContext

在较大的事务中执行SQL语句。有关详细信息,请参阅执行事务

Stmt.Exec

Stmt.ExecContext

执行已准备好的SQL语句。有关详细信息,请参阅使用准备好的语句

Conn.ExecContext

用于专用的连接。有关详细信息,请参阅管理连接

模块、组件和服务分别是什么?它们有什么区别?

组件也叫构件。什么是组件?一个组件就是一个可以独立更换和升级的软件单元。组件具有如下特点:
1 能实现一定功能,或者提供一些服务。
2 不能单独运行,要作为系统的一部分来发挥作用。
3 是物理上的概念,不是逻辑上的概念。
4 可单独维护、可独立升级、可替换而不影响整个系统。
组件是物理上独立的一个东西,它可单独维护、升级、替换,画构件图的目的就是要做系统的构件设计,思考系统在物理上的划分,可利用现有哪些构件,哪些部分可做成构件供以后的项目重用等。

问题1:我们做软件设计时, 往往会提到 “模块 “ 这一词,“模块” 是不是构件呢?
不一定,每个人心中的“模块”的标准是不太一样的,有时候会按业务逻辑来划分模块,有时候从技术的角度来划分。模块只是为了方便说明问题,将软件人为地划分为几个部分而已,我们可以对照组件的上述几个特点来判断 “模块” 是不是构件。

问题2:软件常常会采用分层设计,那一层是一个构件吗?
大部分情况下分层设计中的每一层,仅是一个逻辑上的划分,物理上并不是单独的文件,这时这些分层不是组件。但具体要看实际的设计情况,可对照组件的上述几个特点来判断。

问题3:如何区分“服务”(service)和“组件”(component)?
所谓“组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,但后者不能对组件进行修改。也就是说,使用一个组件的应用程序不能修改组件的源代码,但可以通过作者预留的某种途径对其进行扩展,以改变组件的行为。

服务和组件有某种相似之处:它们都将被外部的应用程序使用。在我看来,两者之间最大的差异在于:组件是在本地使用的软件库(例如JAR文件、程序集、DLL、或者源码导入);而服务是进程外的组件,通过同步或异步的本机进程之间通信或远程接口调用(例如web service、消息系统、RPC,或者socket)这样的机制来被应用程序使用。

服务也可以调用其他服务,因为服务本身也是一个应用程序。

可以把一个个服务映射为一个个运行时的进程,但这仅仅是一个近似。一个服务也可以包括多个进程,比如一个服务应用程序的进程和仅被该服务使用的数据库进程。

服务可被独立部署。如果一个应用系统由在单个进程中的多个软件库所组成,那么对任一组件做一处修改,都不得不重新部署整个应用系统。但是如果该应用系统被分解为多个服务,那么对于一个服务的多处修改,仅需要重新部署这一个服务,例外是更改服务暴露出来的接口。

以服务的方式来实现组件化的另一个结果是,能获得更加显式的(explicit)组件接口。

服务的远程调用,比起进程内调用,远程调用更加昂贵。

参考
《微服务》 Martin Fowler
《IoC容器和依赖注入模式》 Martin Fowler
《火球 UML大战需求分析》