go-mssqldb驱动程序使用标准database/sql接口来运行查询和执行语句。 本文介绍了驱动程序访问数据的常见模式。
运行 SELECT 查询
使用 QueryContext 执行返回行的查询:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
sql.Named("p1", "Australia"))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name, location string
if err := rows.Scan(&id, &name, &location); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s (%s)\n", id, name, location)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
Important
始终调用 rows.Close()(通常使用 defer),并在循环后检查 rows.Err()。 未能关闭排位可能会导致泳池连接泄漏。
rows.Close() 在驱动耗尽剩余令牌时,也可能返回服务器端错误,因此当结果集未完全消耗时,不要忽略该错误。
如果你提前停止阅读,请明确关闭行并处理关闭错误:
rows, err := db.QueryContext(ctx,
"SELECT TOP (100) ProductID, Name FROM Production.Product ORDER BY ProductID")
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
_ = rows.Close()
log.Fatal(err)
}
fmt.Printf("%d %s\n", id, name)
break // Stop early for demonstration.
}
if err := rows.Close(); err != nil {
log.Fatal(err)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
本文中的示例与 AdventureWorks2025 样本数据库比较。 面向读取的示例会查询内置对象,如 Sales.vSalesPerson、Production.Product 和 Sales.SalesOrderHeader。 以写入为导向的示例以 HumanResources.Department 和 Production.ProductInventory 为目标。
查询单个行
当你预计恰好只有一行时,使用 QueryRowContext:
var id int
var name string
err := db.QueryRowContext(ctx,
"SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE BusinessEntityID = @p1",
sql.Named("p1", 280)).Scan(&id, &name)
if err == sql.ErrNoRows {
fmt.Println("No employee found.")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Employee %d: %s\n", id, name)
}
执行一个语句
将 ExecContext 用于 INSERT、UPDATE、DELETE 和 DDL 语句:
result, err := db.ExecContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
sql.Named("p1", "Data Science"),
sql.Named("p2", "Research and Development"))
if err != nil {
log.Fatal(err)
}
rowsAffected, _ := result.RowsAffected()
fmt.Printf("Rows affected: %d\n", rowsAffected)
Important
go-mssqldb驱动不支持 LastInsertId()。 呼叫它会返回错误。 使用 OUTPUT 子句或单独 SELECT SCOPE_IDENTITY() 查询来获取插入的身份值。
如果你使用 SELECT SCOPE_IDENTITY(),请将其与 INSERT 放在同一批处理或事务中运行,这样标识作用域就会保持在同一连接上。
如果存储过程或触发器使用 SET NOCOUNT ON,RowsAffected()则返回 0,因为 SQL Server 会抑制行计数消息。 如果你需要实际计数,要么从过程中移除 SET NOCOUNT ON ,要么通过输出参数或 SELECT 语句显式返回计数。
参数化查询
始终使用参数化查询以避免SQL注入。 驱动程序支持位置参数和命名参数。
Important
go-mssqldb驱动程序使用 @p1、@p2 等作为位置参数,使用 sql.Named() 作为命名参数。 某些其他驱动使用的 ? 占位符语法(例如 MySQL 的 go-sql-driver)无法用于名为 sqlserver 的驱动。 如果你是从另一个数据库迁移过来的,请将所有 ? 或 $1-style 占位符替换为 @p1-style 或命名参数。
位置参数
使用 @p1、@p2 占位符,并按顺序传递值:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
命名参数
使用 sql.Named() 将值绑定到命名占位符:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @location",
sql.Named("name", "Jared"),
sql.Named("location", "Australia"))
多个结果集
使用 rows.NextResultSet() 遍历单个批处理或存储过程返回的多个结果集。
Important
你必须先将每个结果集中的 rows.Next() 完全读取完,然后才能调用 rows.NextResultSet()。 在 NextResultSet() 之前调用 Next() 会返回 false,并静默地跳过其余行。
使用此循环模式以可靠地处理所有结果集:
rows, err := db.QueryContext(ctx,
`SELECT TOP (3) ProductID, Name
FROM Production.Product
ORDER BY ProductID;
SELECT TOP (3) SalesOrderID, CONVERT(NVARCHAR(10), OrderDate, 23) AS OrderDate
FROM Sales.SalesOrderHeader
ORDER BY SalesOrderID DESC;`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
setIndex := 0
for {
switch setIndex {
case 0:
for rows.Next() {
var productID int
var productName string
if err := rows.Scan(&productID, &productName); err != nil {
log.Fatal(err)
}
fmt.Printf("Product %d: %s\n", productID, productName)
}
case 1:
for rows.Next() {
var salesOrderID int
var orderDate string
if err := rows.Scan(&salesOrderID, &orderDate); err != nil {
log.Fatal(err)
}
fmt.Printf("Order %d: %s\n", salesOrderID, orderDate)
}
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
if !rows.NextResultSet() {
break
}
setIndex++
}
Transactions
用 BeginTx 来启动交易时设置特定的隔离级别。 有关包括隔离级别、保存点、死锁处理和重试模式在内的全面交易指导,请参见事务。
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
// Subtract from source location.
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @p1 WHERE ProductID = @p2 AND LocationID = 1",
sql.Named("p1", 5),
sql.Named("p2", 1))
if err != nil {
log.Fatal(err)
}
// Add to destination location.
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity + @p1 WHERE ProductID = @p2 AND LocationID = 6",
sql.Named("p1", 5),
sql.Named("p2", 1))
if err != nil {
log.Fatal(err)
}
if err = tx.Commit(); err != nil {
log.Fatal(err)
}
获取已插入的标识值
go-mssqldb驱动不支持 LastInsertId()。 使用 OUTPUT 子句检索同一语句中的标识值:
var newID int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @grp)",
sql.Named("name", "Data Science"),
sql.Named("grp", "Research and Development")).Scan(&newID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted department with ID: %d\n", newID)
对于多行数据:
rows, err := db.QueryContext(ctx, `
INSERT INTO HumanResources.Department (Name, GroupName)
OUTPUT INSERTED.DepartmentID, INSERTED.Name
VALUES (@n1, @g1), (@n2, @g2)`,
sql.Named("n1", "Data Science"), sql.Named("g1", "Research and Development"),
sql.Named("n2", "Cloud Ops"), sql.Named("g2", "Information Technology"))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted: %d - %s\n", id, name)
}
分页
使用 OFFSET 并 FETCH NEXT 用于服务器端分页。 需要一个 ORDER BY 条款:
基于偏移的分页
将偏移量和页面大小作为参数传递:
func getEmployeesPage(ctx context.Context, db *sql.DB, page, pageSize int) ([]Employee, error) {
offset := (page - 1) * pageSize
rows, err := db.QueryContext(ctx, `
SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
FROM Sales.vSalesPerson
ORDER BY BusinessEntityID
OFFSET @offset ROWS
FETCH NEXT @pageSize ROWS ONLY`,
sql.Named("offset", offset),
sql.Named("pageSize", pageSize))
if err != nil {
return nil, err
}
defer rows.Close()
var employees []Employee
for rows.Next() {
var e Employee
if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
return nil, err
}
employees = append(employees, e)
}
return employees, rows.Err()
}
大型表的键集分页
对于大型表,偏移分页会变慢,因为服务器必须跳过行。 键集分页使用最后一次看到的键高效地获取下一页:
func getNextPage(ctx context.Context, db *sql.DB, lastID int, pageSize int) ([]Employee, error) {
rows, err := db.QueryContext(ctx, `
SELECT TOP(@pageSize) BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
FROM Sales.vSalesPerson
WHERE BusinessEntityID > @lastID
ORDER BY BusinessEntityID`,
sql.Named("pageSize", pageSize),
sql.Named("lastID", lastID))
if err != nil {
return nil, err
}
defer rows.Close()
var employees []Employee
for rows.Next() {
var e Employee
if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
return nil, err
}
employees = append(employees, e)
}
return employees, rows.Err()
}
Tip
对于深度页面(第 1000 页及以后),键集分页比 OFFSET/FETCH 快得多,因为它使用的是索引查找,而不是扫描并跳过行。
批量处理多条语句
在一次调用中发送多个SQL语句,以减少网络往返。
rows, err := db.QueryContext(ctx, `
SELECT COUNT(*) FROM HumanResources.Employee;
SELECT COUNT(*) FROM Sales.SalesOrderHeader;
SELECT COUNT(*) FROM Production.Product;`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var empCount, orderCount, productCount int
if rows.Next() {
if err := rows.Scan(&empCount); err != nil {
log.Fatal(err)
}
}
if rows.NextResultSet() && rows.Next() {
if err := rows.Scan(&orderCount); err != nil {
log.Fatal(err)
}
}
if rows.NextResultSet() && rows.Next() {
if err := rows.Scan(&productCount); err != nil {
log.Fatal(err)
}
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("Employees: %d, Orders: %d, Products: %d\n",
empCount, orderCount, productCount)
高效处理大量结果集
对于返回数百万行的查询,处理结果以流式方式进行。 不要将所有行都保存在内存中。
func processLargeTable(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT TransactionID, CONVERT(NVARCHAR(30), TransactionDate, 126) FROM Production.TransactionHistory")
if err != nil {
return err
}
defer rows.Close()
var processed int
for rows.Next() {
var id int
var data string
if err := rows.Scan(&id, &data); err != nil {
return err
}
// Process each row without accumulating.
if err := handleRow(id, data); err != nil {
return err
}
processed++
if processed%10000 == 0 {
log.Printf("Processed %d rows", processed)
}
}
return rows.Err()
}
注意
处于打开状态的 *sql.Rows 会一直占用连接池中的一个连接,直到调用 rows.Close()。 对于非常长时间的处理结果集,建议使用键集分码将工作分段,以避免连接长时间等待。
使用 MERGE 进行插入或更新
SQL Server 使用该MERGE语句进行插入或更新(upsert)操作。
_, err := db.ExecContext(ctx, `
MERGE HumanResources.Department AS target
USING (SELECT @id AS DepartmentID, @name AS Name, @grp AS GroupName) AS source
ON target.DepartmentID = source.DepartmentID
WHEN MATCHED THEN
UPDATE SET Name = source.Name, GroupName = source.GroupName
WHEN NOT MATCHED THEN
INSERT (Name, GroupName)
VALUES (source.Name, source.GroupName);`,
sql.Named("id", dept.Id),
sql.Named("name", dept.Name),
sql.Named("grp", dept.GroupName))
预定义语句
用于 PrepareContext 创建可重复使用的预言语。 当同一查询多次运行不同参数时,预备语句可以提升性能。
stmt, err := db.PrepareContext(ctx,
"SELECT TOP (1) FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE CountryRegionName = @p1")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for _, location := range []string{"Australia", "India", "Germany"} {
var name string
err := stmt.QueryRowContext(ctx, location).Scan(&name)
if err != nil {
log.Println(location, err)
continue
}
fmt.Printf("%s: %s\n", location, name)
}
上下文消去
所有 database/sql 方法都接受一个 context.Context。 可将其用于超时控制和取消。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
如果上下文截止时间过了,驱动会在服务器上取消查询,并返回给调用者错误。