登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go SQL 占位符在 MySQL 和 PostgreSQL 中为什么不同

来源:17golang原创

时间:2026-09-12 17:46:01 221浏览 收藏

把 Go 项目从 MySQL 切到 PostgreSQL 时,最容易踩到的坑之一就是 SQL 里的参数标记。database/sql 会把查询字符串和参数交给驱动,但它不负责把 ? 自动改成 $1。因此,MySQL 常见写法是 ?,PostgreSQL 常见写法是按位置编号的 $1$2;参数仍然要作为方法参数传入,不能把用户输入拼回 SQL 文本。

要点速览
  • 占位符语法属于驱动和数据库方言,不属于 database/sql 的统一转换规则。
  • MySQL 用 ?,PostgreSQL 用 $1$2,编号必须和参数顺序对应。
  • 值可以绑定,表名、列名和排序方向不能直接绑定,动态结构要走白名单。

先看清 database/sql 到底统一了什么

database/sql 统一的是 Go 侧的调用模型:查询文本是一个字符串,参数通过变长参数传入,结果通过 RowsRowResult 读取。它的接口文档把参数描述为查询中的 placeholder parameters,但没有规定所有数据库都必须采用同一种符号。

真正解析占位符的是驱动。MySQL 驱动文档把 ? 作为查询和执行调用里的 placeholder;PostgreSQL 的预处理语句则按位置使用 $1$2。所以“换了数据库只改 DSN”并不成立,查询文本往往也要随方言切换。

Go database/sql、MySQL 驱动、PostgreSQL 驱动与参数绑定之间的静态层级关系示意图
图1:静态关系示意图,展示 database/sql 传递查询与参数,两个驱动分别解释 ? 和 $1 风格的占位符。

MySQL 的 ? 和 PostgreSQL 的 $1 怎么对应

下面两个查询表达的是同一个条件:按用户编号和状态筛选记录。差别只在查询文本,参数顺序保持一致。

package main

import (
	"context"
	"fmt"
)

// MySQL 使用问号占位符,参数按出现顺序绑定。
func mysqlQuery(ctx context.Context, db DB, userID int64, state string) error {
	rows, err := db.QueryContext(ctx,
		"SELECT id, name FROM users WHERE user_id = ? AND state = ?",
		userID, state,
	)
	if err != nil {
		return err // 把驱动或数据库返回的错误交给上层处理。
	}
	defer rows.Close() // 查询结束后及时释放结果集。
	return consumeRows(rows)
}

// PostgreSQL 使用从 1 开始的位置参数,顺序仍然对应实参。
func postgresQuery(ctx context.Context, db DB, userID int64, state string) error {
	rows, err := db.QueryContext(ctx,
		"SELECT id, name FROM users WHERE user_id = $1 AND state = $2",
		userID, state,
	)
	if err != nil {
		return err // 参数数量或类型不匹配时在这里暴露。
	}
	defer rows.Close() // 让连接池可以回收本次结果资源。
	return consumeRows(rows)
}

示例里的 DBconsumeRows 是文章用来表达接口边界的占位名称,重点在查询文本与实参的关系。不要为了“统一”而在业务层把值格式化进字符串;参数分离仍然是两种数据库都应保留的写法。

跨数据库项目应该把差异放在哪里

我更倾向于把差异收在 repository 的查询定义处,而不是让每个业务函数都判断数据库类型。最小做法是为每种方言准备一份查询常量,业务层只调用同一组方法:

type QueryDialect struct {
	FindUserByState string
}

var mysqlDialect = QueryDialect{
	// 问号不编号,实参顺序就是绑定顺序。
	FindUserByState: "SELECT id, name FROM users WHERE user_id = ? AND state = ?",
}

var postgresDialect = QueryDialect{
	// PostgreSQL 的位置参数从 $1 开始编号。
	FindUserByState: "SELECT id, name FROM users WHERE user_id = $1 AND state = $2",
}

// 只从程序内部选择方言,不接收用户传来的 SQL 片段。
func dialectFor(driver string) QueryDialect {
	if driver == "postgres" {
		return postgresDialect
	}
	return mysqlDialect
}

如果项目只支持一种数据库,直接使用该驱动的原生写法更清楚;只有确实需要双数据库、测试替换或多租户方言时,才值得增加这一层。抽象的目标是集中变化,不是把 SQL 藏到看不懂的模板里。

占位符不能替代表名、列名和排序方向

占位符适合绑定值,例如编号、状态、日期和搜索词。它不是 SQL 语法树的通用插槽,不能写成 SELECT * FROM ? 来绑定表名,也不能把 ORDER BY ? 当成列名替换方案。

动态结构要先转成受控的内部值,再从白名单选出完整片段:

var sortColumns = map[string]string{
	"newest": "created_at DESC",
	"name":   "name ASC",
}

// 排序片段只来自固定白名单,搜索词仍然通过占位符传入。
func buildUserSearch(sortKey string, keyword string) (string, []any, error) {
	sortSQL, ok := sortColumns[sortKey]
	if !ok {
		return "", nil, fmt.Errorf("unsupported sort key: %s", sortKey)
	}
	query := "SELECT id, name FROM users WHERE name LIKE ? ORDER BY " + sortSQL
	args := []any{"%" + keyword + "%"}
	return query, args, nil
}

这里的拼接只加入代码内白名单中的固定 SQL 片段,不能把原始请求参数直接作为列名或排序字符串。若同时支持 PostgreSQL,要让查询定义层提供对应的参数标记,并继续保持白名单策略。

内容推荐做法原因
用户编号、状态、日期、关键词使用 ?$1 绑定值与 SQL 文本分离
表名、列名、排序方向内部枚举或白名单选择它们属于 SQL 结构,不是值参数
MySQL / PostgreSQL 方言集中维护查询定义避免业务代码散落判断

最后用检查清单排掉“看起来一样”的错误

切换驱动后,先查每条 SQL 的占位符风格,再查数量和顺序。PostgreSQL 的编号可以重复引用同一个参数,但新增条件时要重新确认编号;MySQL 的问号则按出现次序消费实参。对可空条件、时间类型和 LIKE 通配符,也要分别准备测试用例。

  • 占位符数量和传入参数数量一致。
  • PostgreSQL 的编号从 $1 开始,没有把数组下标习惯带进 SQL。
  • 错误路径会关闭 Rows,并保留驱动返回的原始错误上下文。
  • 查询参数没有通过 fmt.Sprintf、字符串拼接或日志模板写回 SQL。
  • 双驱动测试覆盖空字符串、NULL、特殊字符和无结果情况。
Go 查询中值参数绑定、动态 SQL 白名单和 MySQL PostgreSQL 方言边界的静态关系图
图2:静态边界示意图,区分可绑定的值、必须白名单选择的 SQL 结构,以及两种方言查询定义。

相关问题

database/sql 会自动把 ? 转成 $1 吗?

不会。调用层传递查询和参数,具体占位符由驱动与目标数据库解释;跨库代码需要在查询定义层处理方言差异。

把参数直接拼进 SQL 会更兼容吗?

不会。这样既失去参数绑定的安全边界,也会让转义、类型和审计更难处理。兼容性问题应该通过方言查询或明确的转换层解决。

为什么表名不能使用占位符?

因为表名和列名属于 SQL 结构,数据库解析阶段就要知道它们。应使用内部白名单映射,而不是把请求参数直接拼进去。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>