当前位置:首页 > 文章列表 > Golang > Go问答 > Go SQL 占位符在 MySQL 和 PostgreSQL 中为什么不同

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

来源:17golang原创 2026-09-12 17:46:01 0浏览 收藏

把 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删除
Java ExecutorService 关闭后如何等待任务完成Java ExecutorService 关闭后如何等待任务完成
上一篇
Java ExecutorService 关闭后如何等待任务完成
Python typing.TypeIs 如何帮助类型检查器收窄类型
下一篇
Python typing.TypeIs 如何帮助类型检查器收窄类型
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    104次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    20次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    31次使用
  • AGI-Eval大模型评测平台:权威榜单、数据集与人机协同评测方案
    AGI-Eval
    AGI-Eval是由上海交大等高校联合发布的大模型评测社区,提供公正透明的LLM能力榜单、多领域评测集及Data Studio数据服务,助力AI模型性能评估与NLP科研开发。
    21次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    258次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码