用 Go database/sql 读写 MySQL:从连接到安全参数绑定
作者:Go 文档团队 来源:原文

把示例库限定在练习环境
教程建立 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。











暂无评论内容