Go logo

GoLand

A cross-platform Go IDE with extended support for JavaScript, TypeScript, and databases

Tutorials

database/sql 软件包使用入门

Read this post in other languages:

本文由外部贡献者撰写。

Damaso Sanoja

Damaso Sanoja

Damaso Sanoja 热衷于帮助他人通过数据驱动型决策达成目标。 这促使他撰写了大量文章,内容覆盖最流行的关系数据库、客户关系管理系统、企业资源规划系统、主数据管理工具,以及最近用于机器学习和 AI 项目的数据仓库系统。 这份对数据管理的执着可以归结于他的第一台电脑是没有软盘的 Commodore 64。

GitHub

 

 

 

 

database/sql 软件包是一个标准库,提供与 SQL 数据库交互的接口。 该软件包使应用程序能够查询和更新数据库,并提供可用于各种 SQL 数据库的可移植接口。 database/sql 软件包通常与数据库驱动程序结合使用,后者提供了一个具体的接口实现,允许您在方便的方法后抽象数据库选项。

本实践教程将展示如何开始使用 database/sql 软件包,我将指导您如何完成以下工作:

  • 创建 GoLand 项目。
  • 设置数据库。
  • 使用 Go 和 GoLand 实现基本查询和事务。
  • 如何构造错误消息。

database/sql 软件包使用入门

使用 Go 编写的数据库很易于上手。 但是,您的本地机器必须满足某些先决条件。

前提

在开始之前,您应该确保已经安装了所有必要工具并拥有完成本教程所需的适当资源:

  • Go:如果还没有,请在本地机器上下载并安装 Go
  • MySQL 服务器:Go 支持流行的关系数据库管理系统,例如 MySQL、Oracle、Postgres、SQL Server、SQLite 等。 但是,在本教程中,您将使用 MySQL。 在这里下载适用于您的操作系统的 MySQL Community Server。 有关设置 MySQL 的说明,请参阅 MySQL 使用入门
    • 重要说明:根据您选择的安装方法,MySQL 服务器可能会自动启动并在后台运行,直到您将其停止。 同样,安装服务后,MySQL 服务可能需要手动启动。
  • MySQL 数据库:您需要一个可以使用的空数据库。 本教程将数据库称为 recordings,但您可以使用任意名称。 您可以在找到有关在 MySQL 中创建数据库的信息。
  • GoLand IDE:您可以在这里下载适用于 macOS、Linux 或 Windows 的 GoLand。
  • 教程仓库:您可以将包含教程所用代码的仓库克隆到适当位置。

满足先决条件后,您就可以启动 GoLand 并开始您的第一个项目了。

在 GoLand 中创建新项目

第一步是在 GoLand 中创建新项目。 为此,在 Welcome(欢迎)屏幕上按 New Project(新建项目)按钮。

接下来,屏幕将类似于:

注意屏幕上的几个字段:

  • Location(位置)。 使用此字段为项目选择合适的位置。 但是,在本教程中,您必须选择仓库的位置。
  • GOROOT。 如果您已将 Go 安装在操作系统的默认位置,则此字段会被预填。 如果没有,请选择正确的 Go 根位置。
  • Environment(环境)。 这是一个可选字段,允许您声明环境变量,例如 GOPROXYGOPRIVATE。 在本教程中,您将使用此字段声明后续连接数据库所需的 MySQL 环境变量 DBUSERDBPASS。 为此,您必须输入相应的键值对,如下所示。

务必输入正确的值。 本教程使用以下值:

创建项目后,GoLand 会自动创建 go.mod 文件。

配置 MySQL 数据源

在 GoLand 中管理数据库非常简单,您只需要确保数据源配置正确。

要配置数据源,首先,点击 GoLand 右侧菜单中的数据库图标,然后点击加号 (+) 或配置图标,接着从可用数据库列表中选择 MySQL。

在下一个屏幕上,输入用户名和密码以连接到 MySQL。 接下来,输入数据库的名称,然后点击 Test Connection(测试连接)。

如果您之前没有配置过 MySQL,您可能会收到一条错误消息。 在这种情况下,点击 Download Driver Files(下载驱动程序文件),GoLand 将完成其余的工作。

成功测试连接后,点击 OK(确定)按钮继续。

在下一个屏幕上,将自动出现 MySQL 控制台,以及建议配置 SQL 方言的通知。 您可以将 MySQL 设置为默认方言,也可以点击 configure(配置)以选择其他方言。

现在,数据源已设置完成,您可以从 MySQL 控制台运行命令。 但是,如果您有 SQL 脚本(如本教程中所示),则不必这样做。

使用 SQL 脚本设置数据库表

您可以使用 GoLand 轻松运行 SQL 脚本。 为此,只需右键点击相应的文件并从下拉列表中选择 Run(运行)。 尝试运行 table-01.sql 脚本。 执行脚本之前,您必须为其指定一个数据目标,在本例中为 recordings 数据库。 按下 Run(运行)按钮时,脚本的输出将自动显示。

显示的脚本应如下所示:

DROP TABLE IF EXISTS album;
CREATE TABLE album (
                       id         INT AUTO_INCREMENT NOT NULL,
                       title      VARCHAR(128) NOT NULL,
                       artist     VARCHAR(255) NOT NULL,
                       price      DECIMAL(5,2) NOT NULL,
                       quantity   INT UNSIGNED,
                       PRIMARY KEY (`id`)
);


INSERT INTO album
(title, artist, price, quantity)
VALUES
    ('Blue Train', 'John Coltrane', 56.99, 5),
    ('Giant Steps', 'John Coltrane', 63.99, 62),
    ('Jeru', 'Gerry Mulligan', 17.99, 0),
    ('Sarah Vaughan', 'Sarah Vaughan', 34.98, 127);

在上面的 SQL 脚本中:

  • 第一行会删除专辑表。 这使脚本更容易根据需要多次运行,同时避免了重新创建现有表时可能出现的错误。
  • 第 2 行到第 7 行与专辑表以及相应字段的创建有关。
  • 第 8 行将 id 设为主键。
  • 脚本的其余部分使用 INSERT 语句以虚拟数据填充表。

接下来,重复这一过程来运行第二个 SQL 脚本 table-02.sql

下面的代码对应于第二个 SQL 脚本:

DROP TABLE IF EXISTS album_trx;
CREATE TABLE album_trx (
                             trx_id    INT AUTO_INCREMENT NOT NULL,
                             trx_check     INT UNSIGNED,
                             PRIMARY KEY (`trx_id`)
);

正如您所看到的,这是一个更简单的脚本,它创建的表只有 2 个字段。 您将在事务部分使用此表。

至此,您已经将 GoLand 配置为与本地 MySQL 服务关联,因此,您可以使用 SQL 脚本轻松创建和预填充表。 同样,在下一部分中,您将了解在 GoLand 中使用 database/sql 软件包有多轻松。

使用 database/sql 连接到数据库

database/sql 软件包使得使用 Go 管理数据库变得非常简单,如 connection.go 代码所示:

package main
import (
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "log"
    "os"
)
var db *sql.DB
type Album struct {
    ID       int64
    Title    string
    Artist   string
    Price    float32
    Quantity int64
}
func main() {
    // Capture connection properties.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
}

我们来检查代码。 首先,导入必要的软件包:

  • 如前所述,database/sql 提供围绕 SQL(或类 SQL)数据库的泛型接口
  • fmt使用类似于 C 语言的 printf 和 scanf 的函数实现格式化 I/O 的软件包。
  • github.com/go-sql-driver/mysql – MySQL 的 database/sql 的实现(数据库驱动程序)。
  • log – 提供日志记录功能的标准库。
  • os为操作系统功能提供独立于平台的接口的软件包。

*var db sql.DB – 这一行是键。 db 变量使用充当数据库连接池的 sql.DB 结构声明。 如前言中所述,这使您可以方便地抽象数据库选项。

type Album struct {...} – 这是 Album 表结构的定义,它将保存数据库的行数据。

func main() – 此函数利用上方声明的 sql.DB 结构以及 database/sql 软件包和 MySQL 驱动程序。

连接数据使用 DSN(数据源名称)格式捕获。 语法:

db, err := sql.Open("mysql", "user:password@/dbname")

为简化代码,声明收集连接属性的 cfg 变量(包括之前在 GoLand 中配置的环境),后续如上所示将其用于使用 DSN 格式获取数据库句柄。 func main 的最后一部分处理可能的错误,我们将在后面的部分讨论。

运行此文件非常简单,只需点击右键并从下拉列表中选择 Run ‘go build connection.go’(运行 ‘go build connection.go’)。

与预期相同,输出显示消息 Connected!(已连接!)。

 

现在,您已连接到数据库,可以开始执行插入新行或查询行等操作了。

插入新行

我们来检查 insert.go 的代码,它插入了一个新行:

package main
import (
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "log"
    "os"
)
var db *sql.DB
type Album struct {
    ID       int64
    Title    string
    Artist   string
    Price    float32
    Quantity int64
}
func main() {
    // Capture connection properties.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
    albID, err := addAlbum(Album{
        Title:    "The Modern Sound of Betty Carter",
        Artist:   "Betty Carter",
        Price:    49.99,
        Quantity: 10,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID of added album: %vn", albID)
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist, price, quantity) VALUES (?, ?, ?, ?)", alb.Title, alb.Artist, alb.Price, alb.Quantity)
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    return id, nil
}

此代码块与前面的代码块非常相似,不同之处在于它向 album 表插入了一个新行。 右键点击文件并选择 Run ‘go build insert.go’(运行 ‘go build insert.go’)运行代码。

正如预期的那样,输出为:

Connected!
ID of added album: 5

正如您所看到的,使用 database/sql 管理数据库连接非常简单。 定义一个数据库句柄,然后就可以使用 SQL 命令、函数和条件语句来管理数据库。 查询数据库中的一行来巩固所学的知识。

查询单行

和前面一样,我们先检查查询单行的 single.go 的代码:

package main
import (
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "log"
    "os"
)
var db *sql.DB
type Album struct {
    ID       int64
    Title    string
    Artist   string
    Price    float32
    Quantity int64
}
func main() {
    // Capture connection properties.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
    // Hard-code ID 2 here to test the query.
    alb, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %vn", alb)
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
    // An album to hold data from the returned row.
    var alb Album
    row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
    if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil {
        if err == sql.ErrNoRows {
            return alb, fmt.Errorf("albumsById %d: no such album", id)
        }
        return alb, fmt.Errorf("albumsById %d: %v", id, err)
    }
    return alb, nil
}

这里有几个代码块值得强调。

首先,第 44 行以注释 // Hard-code ID 2 here to test the query. 作为其标题,调用 albumByID(2) 查询哪个专辑对应 ID 号 2。

第二个块对应于 albumByID 函数。 此函数使用 SELECT * FROM album WHERE id = ? 查询具有特定 ID 的专辑的数据。 如果 ID 不存在,则使用 if 条件显示错误消息。

运行代码以验证一切都按预期运作。

输出应如下所示:

Connected!
Album found: {2 Giant Steps John Coltrane 63.99 62}

查询多行

要了解如何使用 database/sql 在 Go 中查询多行,您将使用 multiple.go。 查询多行背后的逻辑类似于查询单行。

albumsByArtist 函数会查找其艺术家在 main 函数中硬编码的所有专辑。 然后,如果有匹配项,它会在输出中打印,如果没有,它会打印一个空字符串。

右键点击文件并选择 Run ‘go build multiple.go’(运行 ‘go build multiple.go’)运行 multiple.go

输出应为:

Connected!
Albums found: [{1 Blue Train John Coltrane 56.99 5} {2 Giant Steps John Coltrane 63.99 62}]

您已经学会了如何执行单行和多行查询,现在该执行更高级的操作(例如预备语句)了。

使用预备语句

根据 Go 文档:

“预备语句是由 DBMS 解析和保存的 SQL,通常包含占位符,但没有实际形参值。 后续,可以使用一组形参值执行该语句。”

在实践中,预备语句有助于避免每次代码执行相同的数据库操作时都重新创建语句,从而提高性能。

如果您有兴趣了解预备语句,请查看 prepared.go 文件。

package main
import (
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "log"
    "os"
)
var db *sql.DB
type Album struct {
    ID       int64
    Title    string
    Artist   string
    Price    float32
    Quantity int64
}
func main() {
    // Capture connection properties.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
    // Hard-code ID 2 here to test the query.
    Album, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %vn", Album)
}
// AlbumByID retrieves the specified album.
func albumByID(id int) (Album, error) {
    // Define a prepared statement. You'd typically define the statement
    // elsewhere and save it for use in functions such as this one.
    stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
    if err != nil {
        log.Fatal(err)
    }
    var album Album
    // Execute the prepared statement, passing in an id value for the
    // parameter whose placeholder is ?
    err = stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
    if err != nil {
        if err == sql.ErrNoRows {
            // Handle the case of no rows returned.
        }
        return album, err
    }
    return album, nil
}

正如您所看到的,该代码与用于查询单行的代码非常相似。 不同之处在于 albumByID 函数现在使用预备语句。 函数的第一部分定义使用 sql.Stmt 准备 SQL 语句的预备语句 (db.Prepare),然后调用时在第二部分中执行它 (stmt.QueryRow)。

运行代码以检查结果:

结果与前面的示例(查询单行)相同,但实际上,节省的资源在处理具有数千行的数据库时会产生显著影响。 要详细了解预备语句,请阅读此处的文档。

处理事务

事务允许您执行各种操作,例如插入、更新、查询或删除表行。 事务之所以有用,是因为它们能够将多个操作(查询)组合在一起,仅在它们都满足指定条件时才执行。 否则,不执行任何操作。

为了理解这个概念,我们来检查 transaction.go

package main
import (
    "context"
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "log"
    "os"
)
var db *sql.DB
type Album struct {
    ID       int64
    Title    string
    Artist   string
    Price    float32
    Quantity int64
}
type Album_trx struct {
    TRX_ID    int64
    TRX_CHECK int64
}
func main() {
    // Capture connection properties.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
    // Start the transaction
    ctx := context.Background()
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }
    // First query
    _, err = tx.ExecContext(ctx, "INSERT INTO album (title, artist, price, quantity) VALUES ('Master of Puppets', 'Metallica', '49', '1')")
    if err != nil {
        tx.Rollback()
        return
    }
    // Second query
    _, err = tx.ExecContext(ctx, "INSERT INTO album_trx (trx_check) VALUES (-1)")
    if err != nil {
        tx.Rollback()
        fmt.Println("Transaction declined")
        return
    }
    // If no errors, commit the transaction
    err = tx.Commit()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Transaction accepted!")
}

该结构类似于本教程中讨论的结构,除了最后一部分,它包含 2 个查询。 根据文档,您可以使用代表事务的 sql.Tx 执行数据库事务

让我们从注释 // Start the transaction 开始检查代码,了解这些事务的运作方式。 此代码创建一个上下文 (ctx),用于使用 DB.BeginTx 方法启动事务。 接下来,是 2 个使用 tx.ExecContext 方法的查询。

如果事务成功,第一个查询只在专辑表中插入一个新行。 请注意,如果出现错误,tx.Rollback() 方法会阻止操作执行。

在本例中,第二个查询用于显示事务背后的逻辑。 请注意,在本例中,它还使用 INSERT 语句向 album_trx 控制表添加新行。 但是,该值为负,并且由于字段 trx_check 不接受负值(它被定义为 INT UNSIGNED),该查询将不会进行。

因此,条件将触发 tx.Rollback() 方法,终端将显示 Transaction declined 错误消息。 而且,无论第一个查询是否顺利通过,都不会更新任何表。

运行代码以检查事务的行为。

与预期相同,事务被拒绝。

现在,编辑 _, err = tx.ExecContext(ctx, "INSERT INTO album_trx (trx_check) VALUES (-1)") 行,将值 -1 更改为 1,然后再次运行代码。

点击数据库图标,展开数据库表,然后双击每个表名,即可轻松检查表是否被修改。

本教程中展示的相同原理可用于创建具有同时影响多个表的多项检查的复杂操作。 一次查询失败就足以导致事务中断。

在 Go 中处理错误

许多原因可能导致错误。 例如,如果您尝试使用尚未声明的变量、尝试除以 0 或存在拼写错误,就可能会出错。 幸运的是,要处理 Go 中的代码错误,您可以使用内置的错误处理函数。

这些函数已在本教程中广泛使用,只是没有详细说明。 我们来看一下获取 MySQL 数据库句柄时错误消息是如何管理的:

 // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }
    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")

在这段代码中:

  • 第一行声明变量 errerror 并将其初始化为零。
  • 第二行使用 database/sql 软件包通过 Go MySQL Driver 连接到数据库。
  • 在第三行中,使用了一个条件。 如果身份验证期间发生错误(布尔值不为 null),则使用 log.Fatal(err) 存储日志。 相反,如果没有错误,则执行下一个代码块。
  • 最后一段代码使用 Ping() 测试与数据库的连接。 如果发生错误,log.Fatal(pingErr) 会将其存储在日志中,但如果没有,将使用 fmt.Println("Connected!") 打印成功消息。

正如您所看到的,使用 fmtlog 等软件包构建错误消息非常简单。

您可以像在代码中构建错误消息一样轻松地使用 GoLand 调试代码中的问题。 您可以打开任意教程文件来查找错误消息示例。

结论

本教程解释了如何实现 database/sql 软件包以在 Go 中访问数据库、执行不同类型的查询、插入数据,以及执行事务。 此外,我们还简要回顾了 Go 中的错误处理。 在教程中,您已经了解到 GoLand 在编写和运行代码方面有多么方便。 您可以在这里详细了解 JetBrains 为开发者提供的工具。

本博文英文原作者:

image description

Discover more