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)
}
Pagination
使用 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()
}
小提示
鍵組分頁速度明顯快於 OFFSET/FETCH 深層頁面(第 1000+頁),因為它使用索引尋道,而非掃描和跳過列。
將多個陳述式批次處理
在一次通話中傳送多個 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")
如果上下文截止期限過了,驅動程式會取消伺服器上的查詢,並回傳錯誤給呼叫者。