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

查询参数解码

来源:17golang原创

时间:2026-11-11 15:50:58 102浏览 收藏

如果你用 Go 读取查询参数,发现 url.QueryUnescape("a+b") 得到的是 "a b",这不是函数把加号“误删”了,而是查询组件本来就把 + 当作空格的另一种表示。真正想保留字面量加号时,应让输入中出现 %2B,或者在构造参数时交给 url.QueryEscape、url.Values.Encode 处理。

本文只围绕查询参数解码这一件事展开:先解释加号语义,再区分路径解码,最后给出单值和整串查询的选择方法。

为什么 QueryUnescape 会把加号变成空格

QueryUnescape 的职责是还原查询组件。Go 官方文档说明,它会把形如 %AB 的三字节编码还原成对应字节;同时,查询编码约定把加号用于表示空格。因此,a+b 表示的查询值是 a b,而不是包含一个字面量加号的 a+b。

字面量加号要写成 %2B。所以这两组输入的含义不同:

  • hello+go:解码为 hello go。
  • hello%2Bgo:解码为 hello+go。
QueryUnescape 查询组件中加号、空格、%2B 与字面量加号的静态关系说明图
图1:查询组件的编码语义说明图,展示加号与字面量加号的区别,不是运行结果或终端截图。
package main

import (
	"fmt"
	"net/url"
)

func main() {
	// 查询组件里的 + 按约定表示空格,%2B 才表示字面量加号。
	inputs := []string{"a+b", "a%2Bb", "a%20b"}
	for _, input := range inputs {
		decoded, err := url.QueryUnescape(input)
		if err != nil {
			// 遇到不完整的百分号编码时,保留错误并停止使用结果。
			fmt.Printf("%s: decode error: %v\n", input, err)
			continue
		}
		fmt.Printf("%s -> %q\n", input, decoded)
	}
}

另一个容易忽略的边界是错误处理:百分号后面不是两个十六进制字符时,QueryUnescape 会返回错误。不要只接收第一个返回值,否则坏输入可能被当成正常查询值继续处理。

先分清 QueryUnescape 和 PathUnescape

这两个函数都能处理百分号编码,但服务的 URL 部位不同。QueryUnescape 面向查询组件,会把 + 变成空格;PathUnescape 面向路径片段,不会把 + 改成空格。

package main

import (
	"fmt"
	"net/url"
)

func main() {
	value := "a+b"

	// 查询参数中的 + 有空格语义。
	queryValue, queryErr := url.QueryUnescape(value)
	if queryErr != nil {
		panic(queryErr)
	}

	// 路径片段中的 + 保持为字面量,不复用查询规则。
	pathValue, pathErr := url.PathUnescape(value)
	if pathErr != nil {
		panic(pathErr)
	}

	fmt.Printf("query=%q path=%q\n", queryValue, pathValue)
}

判断方法很简单:变量来自 RawQuery、表单查询或键值对,就优先考虑查询规则;变量是路径片段,就不要为了“统一解码”而调用 QueryUnescape。函数名相似,语义位置并不相同。

构造查询参数时怎样保留字面量加号

最稳妥的做法是:不要手写 key=value 并自行替换字符,而是把原始值交给标准库编码。QueryEscape 会把字面量加号编码为 %2B;url.Values.Encode 则适合同时构造多个参数。

url.Values、QueryEscape、%2B、QueryUnescape 和 ParseQuery 的查询参数边界说明图
图2:查询参数构造与解析边界说明图,查看哪些表示适合传递字面量加号,不是软件界面截图。
package main

import (
	"fmt"
	"net/url"
)

func main() {
	// 原始值包含字面量 +,编码后应变成可逆的 %2B。
	original := "C++"
	escaped := url.QueryEscape(original)

	// Values.Encode 负责多个键值对的查询组件编码,不手拼分隔符。
	values := url.Values{}
	values.Set("language", original)
	values.Set("q", "a b")
	encodedQuery := values.Encode()

	fmt.Println("single:", escaped)
	fmt.Println("query:", encodedQuery)
}

如果上游已经把查询值编码好了,就不要再次调用 QueryEscape,否则可能出现双重编码。例如,已有的 %2B 再编码后会变成 %252B。工程上应约定:函数参数接收原始值,还是接收已编码值,不能两种状态混用。

需要解整串查询时优先用 ParseQuery

只解一个已经分离出来的值,可以用 QueryUnescape。如果手里是 a=1&b=hello%2Bgo 这样的完整查询串,更适合使用 url.ParseQuery。它负责按键值对解析、对每个键和值执行查询解码,并返回包含有效结果的 url.Values;如果遇到坏的百分号编码,还会返回错误。

package main

import (
	"fmt"
	"net/url"
)

func main() {
	// ParseQuery 适合完整查询串,键和值中的 +、%2B 都按查询规则处理。
	query := "name=C%2B%2B¬e=a+b"
	values, err := url.ParseQuery(query)
	if err != nil {
		// 解析结果可能已有部分有效键,是否继续使用要由业务决定。
		fmt.Println("query parse error:", err)
	}

	fmt.Printf("name=%q note=%q\n", values.Get("name"), values.Get("note"))
}

相比手写 strings.Split,标准库方案少了一层边界处理:空值、重复键、没有等号的键以及百分号错误都由同一个 API 负责。需要严格判断时,记得同时检查返回的 error,不要因为拿到了一个非空 Values 就忽略异常。

几个容易混淆的判断

输入或任务优先选择关键结论
解码单个查询值url.QueryUnescape+ 会按查询语义变为空格
解码单个路径片段url.PathUnescape+ 保持为加号
构造一个查询值url.QueryEscape字面量 + 编码为 %2B
构造多个键值对url.Values.Encode避免手拼查询分隔符和编码
解析完整查询串url.ParseQuery统一处理键值对和解码错误

记住一个最短规则:查询里的 + 是空格语义,字面量加号写成 %2B;路径里的 + 不走这条规则。只要先确认数据位于 query 还是 path,再选择对应 API,这类“加号被吃掉”的问题就能避免。

相关问题

为什么 QueryEscape 后还要检查错误?

QueryEscape 本身只返回编码后的字符串,不返回错误;需要检查错误的是解码和整串解析 API,例如 QueryUnescape、ParseQuery。

已经拿到 URL 对象时还要手动 QueryUnescape 吗?

如果使用 URL.Query 或 ParseQuery 读取完整查询,通常不应再次解码;二次解码可能把原本有意义的百分号序列继续改写。

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