本文介绍如何在 go 中动态构建 postgresql 参数化查询,避免 sql 注入风险,同时支持可选条件、分页与全文检索等复杂场景,通过拼接占位符与参数切片实现清晰、可维护的查询逻辑。
在 Go 应用中与 PostgreSQL 交互时,常需根据用户输入动态添加 WHERE 条件(如搜索关键词、筛选范围、分页参数等)。但直接字符串拼接 SQL 容易引发 SQL 注入,而硬编码固定参数数量又丧失灵活性——尤其像 to_tsquery() 这类函数不支持通配符“匹配全部”,导致 AND to_tsvector(…) @@ to_tsquery($2) 必须存在或完全省略,无法简单传入空值跳过。
一个专业、安全且可扩展的解决方案是:动态构建 SQL 片段 + 同步维护参数切片。核心思想是——SQL 模板只含固定结构,所有可选条件通过 fmt.Sprintf 动态追加带正确 $N 占位符的子句,并同步将对应参数 append 到 []interface{} 切片中。PostgreSQL 驱动(如 github.com/lib/pq 或现代 github.com/jackc/pgx)能正确解析并绑定这些顺序占位符。
以下是一个完整、生产就绪的示例,适配你原始查询中的全文检索、多表关联及分页需求:
func buildJobSearchQuery(nameQuery, locationQuery string, limit, offset int) (string, []interface{}) {
// 基础查询(不含全文检索条件)
baseQuery := `
SELECT json_agg(row_to_json(t)) FROM (
SELECT jobs.*, companies.name AS company_name, locations.name AS location_name
FROM jobs
JOIN companies ON jobs.company_id = companies.id
JOIN locations ON jobs.location_id = locations.id
WHERE 1=1`
params := []interface{}{}
// 动态添加职位/公司/地点联合全文检索(必需条件)
if nameQuery != "" {
baseQuery += " AND to_tsvector(jobs.name || ' ' || companies.name || ' ' || locations.name) @@ to_tsquery($" + fmt.Sprintf("%d", len(params)+1) + ")"
params = append(params, nameQuery)
}
// 动态添加地点独立全文检索(可选条件)
if locationQuery != "" {
baseQuery += " AND to_tsvector(locations.name) @@ to_tsquery($" + fmt.Sprintf("%d", len(params)+1) + ")"
params = append(params, locationQuery)
}
// 分页支持(可选)
if limit > 0 {
baseQuery += " LIMIT $" + fmt.Sprintf("%d", len(params)+1)
params = append(params, limit)
if offset > 0 {
baseQuery += " OFFSET $" + fmt.Sprintf("%d", len(params)+1)
params = append(params, offset)
}
}
baseQuery += ") t"
return baseQuery, params
}
// 使用示例
func searchJobs(db *sql.DB, nameQ, locQ string, limit, skip int) ([]byte, error) {
query, args := buildJobSearchQuery(nameQ, locQ, limit, skip)
row := db.QueryRow(query, args...)
var result []byte
if err := row.Scan(&result); err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return result, nil
}
✅ 关键优势说明: