本指南帮助 Go 开发者从 PostgreSQL、MySQL 及其他数据库驱动迁移到 go-mssqldb SQL Server 驱动。 它涵盖了SQL语法差异、驱动程序特定的变更以及常见操作的等效模式。
注释
本文中的示例与 AdventureWorks2025 样本数据库比较。
为什么使用 go-mssqldb 迁移到 SQL Server
| 功能 | SQL Server 与 go-mssqldb |
|---|---|
| 企业安全性 | 始终加密、行级安全、动态数据掩蔽、透明数据加密(TDE)。 |
| Authentication | Microsoft Entra ID(前称Azure AD)、管理身份、Windows 身份验证、Kerberos. |
| Azure 集成 | 原生支持Azure SQL 数据库,支持自动加密、无密码连接和故障切换处理。 |
| Performance | 列存储索引、内存中 OLTP、用于自动计划更正的查询存储。 |
| 数据格式 | 内置JSON和XML支持,支持服务器端索引和查询。 |
| Tooling | SQL Server Management Studio (SSMS)、Azure Data Studio、Visual Studio Code SQL 扩展。 |
迁移考虑清单
在开始逐行移植查询前,请逐条查看以下内容:
| 注意事项 | 要更改的内容 |
|---|---|
| 占位符语法与许多 Go 驱动程序不同。 | 将 ?、$1 及类似占位符替换为 @name 或 @p1,然后在适当情况下传递 sql.Named() 参数。 |
不支持 LastInsertId()。 |
请改用 OUTPUT INSERTED.<column> 或 SELECT SCOPE_IDENTITY()。 |
Go string 值默认映射到 Unicode 类型。 |
除非你选择用于非 Unicode 数据的驱动程序特定类型(例如 mssql.VarChar),否则应预期采用 nvarchar 的语义。 |
| 临时表的作用域限定在物理连接内。 | 如果后续语句依赖该临时表,请将创建和使用临时表的逻辑放在同一连接上或同一事务内执行。 |
| Microsoft Entra 身份验证使用不同的驱动程序注册方式。 | 导入github.com/microsoft/go-mssqldb/azuread,使用sql.Open("azuresql", ...),并使用encrypt=true&TrustServerCertificate=false显式设置 Azure SQL TLS 选项。 |
有关这些领域的更深入指导,请参见查询与语句、限制、数据类型映射、Microsoft Entra ID 认证和故障排除。
从 lib/pq(PostgreSQL)迁移
更改导入和驱动名称
将导入和驱动名称替换 lib/pq 为 go-mssqldb:
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", connString)
// After (SQL Server with go-mssqldb):
import _ "github.com/microsoft/go-mssqldb"
db, err := sql.Open("sqlserver", connString)
更改连接字符串格式
将 PostgreSQL 的键值格式转换为 SQL Server URL:
// lib/pq connection string:
"host=<server> port=5432 user=<user> password=<password> dbname=AdventureWorks2025 sslmode=require"
// go-mssqldb connection string:
"sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true"
更改参数占位符
PostgreSQL 使用 $1, $2 位置占位符。 SQL Server 使用带有 @ 的命名参数:
// PostgreSQL (lib/pq):
db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1 AND status = $2", id, status)
// SQL Server (go-mssqldb):
db.QueryContext(ctx, "SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id AND Color = @color",
sql.Named("id", id),
sql.Named("color", color))
SQL 语法变更
| 运算 | PostgreSQL | SQL Server |
|---|---|---|
| 自动递增 |
SERIAL 或 GENERATED ALWAYS AS IDENTITY |
IDENTITY(1,1) |
| 获取插入的 ID | RETURNING id |
OUTPUT INSERTED.id 或 SELECT SCOPE_IDENTITY() |
| 布尔类型 | BOOLEAN |
BIT |
| 字符串拼接 | \|\| |
+ 或 CONCAT() |
| 当前时间戳 |
NOW() 或 CURRENT_TIMESTAMP |
GETUTCDATE() 或 SYSDATETIMEOFFSET() |
| 限制行数 | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| 字符串类型 |
TEXT 或 VARCHAR |
NVARCHAR(MAX) 或 NVARCHAR(n) |
| JSON 提取 | column->>'key' |
JSON_VALUE(column, '$.key') |
| Upsert | INSERT ... ON CONFLICT DO UPDATE |
MERGE 语句 |
| 数组类型 | INTEGER[] |
没有原生阵列。 使用表值参数。 |
| 事例敏感性 | 默认区分大小写 | 默认不区分大小写(取决于排序)。 |
获取插入的 ID
将PostgreSQL的RETURNING子句替换为SQL Server的OUTPUT子句:
var id int
err := db.QueryRowContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
name, email).Scan(&id)
// SQL Server (go-mssqldb) using OUTPUT:
var id int
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @groupName)",
sql.Named("name", name),
sql.Named("groupName", groupName)).Scan(&id)
分页
用OFFSET/FETCH NEXT语法替换LIMIT/OFFSET语法。
// PostgreSQL:
"SELECT * FROM employees ORDER BY id LIMIT $1 OFFSET $2"
// SQL Server (requires ORDER BY):
"SELECT ProductID, Name, ListPrice FROM Production.Product ORDER BY ProductID OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY"
从 pgx(PostgreSQL)迁移
pgx驱动使用自己的连接池和 API,与 database/sql 不同。 迁移时,切换到标准 database/sql 接口。
从 pgx 池改为数据库/SQL
用标准 database/sql 接口替换 pgxpool API。
// Before (pgx):
import "github.com/jackc/pgx/v5/pgxpool"
pool, err := pgxpool.New(ctx, "postgres://user:pass@<server>:5432/AdventureWorks2025")
defer pool.Close()
rows, err := pool.Query(ctx, "SELECT * FROM users WHERE id = $1", id)
// After (go-mssqldb with database/sql):
import (
"database/sql"
_ "github.com/microsoft/go-mssqldb"
)
db, err := sql.Open("sqlserver", "sqlserver://user:pass@<server>:1433?database=AdventureWorks2025")
defer db.Close()
rows, err := db.QueryContext(ctx,
"SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id", sql.Named("id", id))
替换 PGX 专属功能
| PGX功能 | go-mssqldb 的对应项 |
|---|---|
pgx.CollectRows |
手动 rows.Next() 和 rows.Scan() 循环。 |
pgx.RowToStructByName |
手动将 rows.Scan 写入结构体字段。 |
批量查询(pgx.Batch) |
多个 ExecContext 调用或存储过程。 |
COPY FROM 用于批量插入 |
mssql.CopyIn 用于批量插入。 |
pgx.ConnConfig |
msdsn.Config 或基于 URL 的连接字符串。 |
pgxpool.Pool |
sql.DB 带有 SetMaxOpenConns 和 SetMaxIdleConns。 |
从 go-sql-driver/mysql (MySQL) 迁移
把MySQL导入和驱动名互换一下
将MySQL导入和驱动名称替换为 go-mssqldb。
// Before (MySQL):
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", connString)
// After (SQL Server):
import _ "github.com/microsoft/go-mssqldb"
db, err := sql.Open("sqlserver", connString)
转换MySQL DSN格式
将MySQL DSN格式转换为SQL Server URL。
// MySQL DSN:
"user:password@tcp(<server>:3306)/AdventureWorks2025?tls=true"
// go-mssqldb URL:
"sqlserver://user:password@<server>:1433?database=AdventureWorks2025&encrypt=true"
替换问号占位符
MySQL 使用 ? 位置占位符。
// MySQL:
db.QueryContext(ctx, "SELECT * FROM users WHERE id = ? AND status = ?", id, status)
// SQL Server:
db.QueryContext(ctx, "SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id AND Color = @color",
sql.Named("id", id),
sql.Named("color", color))
比较MySQL和SQL Server语法
| 运算 | MySQL | SQL Server |
|---|---|---|
| 自动递增 | AUTO_INCREMENT |
IDENTITY(1,1) |
| 获取插入的 ID | LAST_INSERT_ID() |
SCOPE_IDENTITY() 或 OUTPUT INSERTED.id |
| 限制行数 | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| 当前时间戳 | NOW() |
GETUTCDATE() |
| 如果 零 | IFNULL(expr, default) |
ISNULL(expr, default) 或 COALESCE(expr, default) |
| 字符串长度 | LENGTH(str) |
LEN(str) |
| 子字符串 | SUBSTRING(str, start, len) |
SUBSTRING(str, start, len)(相同) |
| 日期格式 | DATE_FORMAT(d, '%Y-%m-%d') |
FORMAT(d, 'yyyy-MM-dd') 或 CONVERT(VARCHAR, d, 23) |
| Upsert | INSERT ... ON DUPLICATE KEY UPDATE |
MERGE 语句 |
| 回溯引用 | `column` |
[column] |
替换MySQL专用语法
| MySQL 语法 | SQL Server 对应项 |
|---|---|
AUTO_INCREMENT |
IDENTITY(1,1) |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY (需要 ORDER BY) |
AUTO_INCREMENT 到 IDENTITY:
-- MySQL:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- SQL Server:
CREATE TABLE HumanResources.NewDepartment (
DepartmentID SMALLINT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50) NOT NULL
);
将 LIMIT 改为 OFFSET/FETCH:
-- MySQL:
SELECT * FROM users LIMIT 10 OFFSET 20;
-- SQL Server:
SELECT ProductID, Name, ListPrice
FROM Production.Product
ORDER BY ProductID
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
常见迁移模式
获取最后插入的标识值
使用 OUTPUT 子句 或 SCOPE_IDENTITY() 来获取插入行的身份值:
// Pattern 1: OUTPUT clause (recommended, works with batch inserts).
var id int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @groupName)",
sql.Named("name", "Engineering"),
sql.Named("groupName", "Research and Development")).Scan(&id)
// Pattern 2: SCOPE_IDENTITY (works with single-row inserts).
var id int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@name, @groupName); SELECT SCOPE_IDENTITY()",
sql.Named("name", "Engineering"),
sql.Named("groupName", "Research and Development")).Scan(&id)
Tip
偏好该 OUTPUT 条款而非 SCOPE_IDENTITY()。 该 OUTPUT 条款适用于批量插入,不依赖于语句顺序。
Upsert(插入或更新)
PostgreSQL 的 ON CONFLICT 和 MySQL 的 ON DUPLICATE KEY UPDATE 映射到 SQL Server 的 MERGE:
_, err := db.ExecContext(ctx, `
MERGE HumanResources.Department AS target
USING (SELECT @id AS DepartmentID, @name AS Name, @groupName AS GroupName) AS source
ON target.DepartmentID = source.DepartmentID
WHEN MATCHED THEN
UPDATE SET Name = source.Name, GroupName = source.GroupName, ModifiedDate = GETDATE()
WHEN NOT MATCHED THEN
INSERT (Name, GroupName, ModifiedDate) VALUES (source.Name, source.GroupName, GETDATE());`,
sql.Named("id", dept.ID),
sql.Named("name", dept.Name),
sql.Named("groupName", dept.GroupName))
批量插入
用 mssql.CopyIn 替换 PostgreSQL 的 COPY 或 MySQL 的 LOAD DATA。
import mssql "github.com/microsoft/go-mssqldb"
stmt, err := db.Prepare(mssql.CopyIn("HumanResources.Department", mssql.BulkOptions{}, "Name", "GroupName", "ModifiedDate"))
if err != nil {
return err
}
for _, dept := range departments {
_, err = stmt.Exec(dept.Name, dept.GroupName, time.Now())
if err != nil {
return err
}
}
// Flush the buffer.
_, err = stmt.Exec()
if err != nil {
return err
}
stmt.Close()
处理 NULL 值
所有 Go 数据库驱动对 NULL 值的处理方式都相同,因为它们使用 database/sql 类型。
var color sql.NullString
err := db.QueryRowContext(ctx,
"SELECT Color FROM Production.Product WHERE ProductID = @id",
sql.Named("id", 1)).Scan(&color)
if color.Valid {
fmt.Println(color.String)
} else {
fmt.Println("NULL")
}
Transactions
所有 Go 数据库驱动都使用相同的 database/sql 事务 API。
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @fromLoc",
sql.Named("qty", qty),
sql.Named("pid", productID),
sql.Named("fromLoc", fromLocationID))
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity + @qty WHERE ProductID = @pid AND LocationID = @toLoc",
sql.Named("qty", qty),
sql.Named("pid", productID),
sql.Named("toLoc", toLocationID))
if err != nil {
return err
}
return tx.Commit()
迁移清单
| Step | Action |
|---|---|
| 安装驱动程序 | go get github.com/microsoft/go-mssqldb |
| 更改导入 | 用 _ "github.com/microsoft/go-mssqldb" 替换旧的驱动导入语句。 |
| 更新连接字符串 | 更改为 SQL Server URL 格式: sqlserver://user:pass@host?database=db。 |
| 替换参数占位符 | 使用 sql.Named 将 $1/$2 或 ? 更改为 @name。 |
| 更新SQL语法 | 更改 LIMIT/OFFSET、 RETURNING、 NOW()、 SERIAL及其他数据库专用的 SQL 代码。 |
| 替换批量操作 | 变更 COPY 或 LOAD DATA 变为 mssql.CopyIn。 |
| 更新架构 DDL | 将 SERIAL/AUTO_INCREMENT 更改为 IDENTITY,TEXT 更改为 NVARCHAR,BOOLEAN 更改为 BIT。 |
| 测试所有查询 | 把你的测试套件对比到SQL Server实例上,以发现语法差异。 |
| 配置加密 | 在生产环境中添加 encrypt=true(或依靠 Azure 自动检测)。 |
| 设置身份验证 | 配置 Microsoft Entra ID for Azure,或配置 SQL Server 认证用于本地。 |