Golang中如何使用字符串准备查询语句?

Golang中如何使用字符串准备查询语句? 我正在努力寻找一种管理数百条SQL查询的方法。我发现使用fmt.Sprintf来“准备”查询可能是一个解决方案,但似乎失败了:

query = fmt.Sprintf(`SELECT job_id,job_name 
FROM job WHERE job_id = $?`, val)
fmt.Println(query)

“query”的结果是这样的:

SELECT job_id,job_name 
FROM job WHERE job_id = $?
%!(EXTRA string=20-0001)

使用它会导致错误:

rows, err := db.Queryx(query)
pq: syntax error at or near "$"

这是不可能的吗?还是有更聪明的方法来处理数百条查询?我需要一个准备好的语句吗?


更多关于Golang中如何使用字符串准备查询语句?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

不,你需要自己处理。此外,这存在SQL注入的风险。

再次强调,你应该优先使用数据库真正的预编译语句。

更多关于Golang中如何使用字符串准备查询语句?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果你想使用 fmt.Sprintf,你需要遵循 fmt 的语法,而不是 SQL 的语法。

不过,你应该优先使用真正的预处理语句,而不是格式化字符串。

如果你想使用 fmt.Sprintf,你需要遵循 fmt 的语法,而不是 SQL 的。

val := "20-0001"
query := fmt.Sprintf("SELECT job_id,job_name FROM job WHERE job_id = %v", val)
fmt.Println(query)

格式化结果为:

SELECT job_id,job_name FROM job WHERE job_id = 20-0001

有没有办法让它自动被单引号包围(‘20-0001’)?

Go Playground - The Go Programming Language

NobbZ 的意思是,永远不要将参数值直接嵌入字符串来构建查询。你的 SELECT 语句应该像这样:

selectStmt := `SELECT job_id, job_name FROM job WHERE job_id = ?;`

假设你正在使用 database/sql 包,并且已经通过 sql.Open(或 sql.OpenDB 等)创建了一个 *sql.DB,那么你应该像这样执行你的 SQL:

func getJobIDAndName(ctx context.Context, db *sql.DB, id string) (id, name string, error) {
    const selectStmt = `SELECT job_id, job_name FROM job WHERE job_id = ?;`
    var jobName string
    if err := db.QueryRowContext(ctx, selectStmt, id).Scan(&id, &jobName); err != nil {
        return "", "", err
    }
    return id, jobName, nil
}

将参数值与实际的 SELECT 语句分开,可以确保驱动程序和数据库引擎将这些值作为值来处理。

最糟糕的做法是使用 fmt.Sprintf。下面是如何以一种可能导致你被降职、解雇、甚至因重大过失而被起诉等方式来实现 getJobIDAndName 函数:

func prayWeGetJobIDAndName(ctx context.Context, db *sql.DB, id string) (id, name string, error) {
    const selectStmt = `SELECT job_id, job_name FROM job WHERE job_id = '%s';`
    var jobName string
    if err := db.QueryRowContext(ctx, fmt.Sprintf(selectStmt, id)).Scan(&id, &jobName); err != nil {
        return "", "", err
    }
    return id, jobName, nil
}

如果你使用了这个版本,并且有人这样调用它(至少在 MS SQL 上是这样。我不知道其他语法):

id, name, err := prayWeGetJobIDAndName("'; drop table job; --")

那么 fmt.Sprintf 会将你的查询变成:

SELECT job_id, job_name FROM job WHERE job_id = ''; drop table job; --';

然后你的 job 表可能就没了。

当然,没有人会把这样的代码写进去。更可能的情况是,你构建了一个 Web 服务,期望有人通过 HTTP GET 请求获取一个 job ID,然后你使用这个 ID 来查询你的 job 表。他们可以利用这个恶意的 “job ID” 字符串来破坏你的数据。

如果你使用第一个 getJobIDAndName 函数实现,并尝试将 "'; drop table job; --" 作为 job ID 传递,它将在数据库服务器上执行如下(再次说明,我使用的是 MS SQL 语法):

SELECT job_id, job_name FROM job WHERE job_id = ' ''; drop table job; --';

这个查询中的单引号被转义了,因此整个 job_id 参数被当作一个参数处理。然后查询会返回没有找到任何结果。

TL;DR:***永远永远永远***不要使用 fmt.Sprintf 来填充 SQL 查询参数。

在Go中处理SQL查询时,直接使用fmt.Sprintf拼接参数到查询字符串中是不安全的,这会导致SQL注入漏洞,而且你遇到的问题是参数占位符格式不正确。

以下是正确的解决方案:

1. 使用参数化查询(推荐)

// 使用?作为占位符(MySQL/PostgreSQL/SQLite)
query := `SELECT job_id, job_name FROM job WHERE job_id = ?`
rows, err := db.Queryx(query, val)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// PostgreSQL使用$1, $2...作为占位符
query := `SELECT job_id, job_name FROM job WHERE job_id = $1`
rows, err := db.Queryx(query, val)

2. 使用预处理语句提高性能

// 准备语句(适用于重复执行)
stmt, err := db.Preparex(`SELECT job_id, job_name FROM job WHERE job_id = ?`)
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

// 多次执行
for _, val := range []string{"20-0001", "20-0002", "20-0003"} {
    rows, err := stmt.Queryx(val)
    if err != nil {
        log.Fatal(err)
    }
    // 处理rows...
}

3. 管理数百条查询的方法

// 将查询定义为常量或从文件加载
const (
    GetJobByID = `SELECT job_id, job_name FROM job WHERE job_id = ?`
    GetJobsByStatus = `SELECT * FROM job WHERE status = ? LIMIT ?`
    InsertJob = `INSERT INTO job (job_id, job_name) VALUES (?, ?)`
)

// 使用map组织查询
var queries = map[string]string{
    "GetJobByID": `SELECT job_id, job_name FROM job WHERE job_id = ?`,
    "GetJobsByStatus": `SELECT * FROM job WHERE status = ? LIMIT ?`,
}

// 执行查询
func executeQuery(db *sqlx.DB, queryName string, args ...interface{}) (*sqlx.Rows, error) {
    query, ok := queries[queryName]
    if !ok {
        return nil, fmt.Errorf("query not found: %s", queryName)
    }
    return db.Queryx(query, args...)
}

// 使用
rows, err := executeQuery(db, "GetJobByID", "20-0001")

4. 使用sqlx的Named查询(支持命名参数)

type Job struct {
    ID   string `db:"job_id"`
    Name string `db:"job_name"`
}

// 使用命名参数
query := `SELECT job_id, job_name FROM job WHERE job_id = :job_id`
rows, err := db.NamedQuery(query, map[string]interface{}{
    "job_id": val,
})

// 或使用结构体
rows, err := db.NamedQuery(query, Job{ID: val})

5. 从文件加载查询(适用于大量查询)

// queries.sql文件内容:
// -- name: GetJobByID
// SELECT job_id, job_name FROM job WHERE job_id = ?
// 
// -- name: GetJobsByStatus
// SELECT * FROM job WHERE status = ? LIMIT ?

func loadQueries(filepath string) (map[string]string, error) {
    content, err := os.ReadFile(filepath)
    if err != nil {
        return nil, err
    }
    
    queries := make(map[string]string)
    lines := strings.Split(string(content), "\n")
    
    var currentName string
    var currentQuery strings.Builder
    
    for _, line := range lines {
        if strings.HasPrefix(line, "-- name:") {
            if currentName != "" {
                queries[currentName] = currentQuery.String()
            }
            currentName = strings.TrimSpace(strings.TrimPrefix(line, "-- name:"))
            currentQuery.Reset()
        } else if currentName != "" {
            currentQuery.WriteString(line + "\n")
        }
    }
    
    if currentName != "" {
        queries[currentName] = currentQuery.String()
    }
    
    return queries, nil
}

关键点:

  • 永远不要直接将用户输入拼接到SQL字符串中
  • 使用数据库驱动支持的参数占位符(? 或 $1, $2…)
  • 预处理语句可以提高重复查询的性能
  • 使用sqlx库可以简化数据库操作
回到顶部