用 Go database/sql 读写 MySQL:从连接到安全参数绑定

用 Go database/sql 读写 MySQL:从连接到安全参数绑定

作者:Go 文档团队 来源:原文

流程图: 练习数据库 -> MySQL 驱动与 sql.DB -> 参数化查询 -> 扫描/插入和错误检查
流程图: 练习数据库 -> MySQL 驱动与 sql.DB -> 参数化查询 -> 扫描/插入和错误检查 (本稿自绘示意图)

把示例库限定在练习环境

教程建立 recordings 数据库与 album 表,写入几张爵士唱片。初始化 SQL 使用 DROP TABLE IF EXISTS album 来方便重跑,它会删除同名表及数据,只能对空的练习数据库执行;不能把它放进真实生产库迁移流程。Go 模块用 go mod init 建立依赖清单,再引入 database/sql 与 MySQL 驱动。连接参数来自 DBUSER、DBPASS 环境变量,而非写进源代码。

sql.Open 返回的是可复用数据库句柄,并不保证已建立网络连接,因此教程用 Ping 确认连接成功。实际应用要在启动期间处理失败,设置连接数、空闲数与连接生命周期限制,结合 context 超时执行查询,并在生命周期结束时关闭句柄。全局变量只是让入门步骤更短,生产代码宜通过依赖注入或结构体持有 db。

多行、单行查询与参数安全

多行查询用 Query 返回 Rows,随后循环调用 Next 和 Scan,把每列写入字段。调用方需要 defer rows.Close,并在循环结束后再检查 rows.Err;否则驱动在中途遇错时,可能把不完整结果误认为正常结束。SQL 参数以占位符与值分开传入,例如按 artist 查找专辑,避免将用户输入用字符串拼接进 SQL,从而降低注入风险。占位符语法由驱动翻译,不能在所有数据库中照搬同一种写法。

确定最多返回一行时可用 QueryRow。它把执行错误延迟到 Scan 才报告,因此必须在 Scan 处区分 sql.ErrNoRows 与数据库故障,并给上层有意义的错误信息。绑定参数只处理值,不适用于动态表名、列名或 ORDER BY 片段;动态标识符仍须来自严格白名单。

插入、金额和生产校订

新增记录使用 Exec,检查 Exec 错误,再从结果取得 LastInsertId。某些驱动或数据库不支持该返回方式,跨平台实现应核实驱动能力;并发创建或重试还应设计幂等键或事务边界。教程把 price 声明为 float32 以便入门,但浮点数不能精确表达十进制金额,业务金额应使用 DECIMAL 与定点整数(例如分),或者合适的 decimal 类型,并明确币种。

本文补上教程的完整程序,并说明它仍是入门示例:它没有显式关闭全局数据库句柄、上下文超时或连接池约束,金额用 float32 也不适合生产账务。参数占位符绑定的是值,不是 SQL 标识符。文中如出现输出,只表示原教程展示结果,不是本次实测。

先建立仅供练习的表

源教程的初始化脚本会先删除同名 album 表。仅在独立、可丢弃的练习数据库中使用;不要对含有重要数据的库执行。字段的数据库金额类型为 DECIMAL,但 Go 示例结构使用 float32,存在精度差异。

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,
  PRIMARY KEY (`id`)
);

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

完整 Go 程序

以下程序按源教程的完整代码段整理,保留 ? 参数占位符与环境变量读取。代码没有在本次审查中执行。教程显示的 go get . 输出解析到 github.com/go-sql-driver/mysql v1.8.1;命令本身未固定该版本,实际解析会受当前模块状态影响。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

func main() {
    // Capture connection properties.
    cfg := mysql.NewConfig()
    cfg.User = os.Getenv("DBUSER")
    cfg.Passwd = os.Getenv("DBPASS")
    cfg.Net = "tcp"
    cfg.Addr = "127.0.0.1:3306"
    cfg.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!")
    albums, err := albumsByArtist("John Coltrane")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Albums found: %v\n", albums)

    // Hard-code ID 2 here to test the query.
    alb, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %v\n", alb)

    albID, err := addAlbum(Album{
        Title:  "The Modern Sound of Betty Carter",
        Artist: "Betty Carter",
        Price:  49.99,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID of added album: %v\n", albID)
}

// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
    // An albums slice to hold data from returned rows.
    var albums []Album

    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
    if err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    defer rows.Close()
    // 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); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        albums = append(albums, alb)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    return albums, nil
}

// 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); 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
}

// 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) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
    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
}

来源与许可说明

Go 官方版权页列明:除另有说明,网站内容采用 CC BY 4.0,代码采用 BSD 许可;完整代码许可文本见 Go LICENSE。完整程序来自该教程并保留源链接。

Go 示例代码 BSD 许可声明
Copyright 2009 The Go Authors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name of Google LLC nor the names of its contributors may
  be used to endorse or promote products derived from this software
  without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

本文为中文译写稿,原文:https://go.dev/doc/tutorial/database-access。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容