最近在使用 Gin 框架使用接受 GET 请求的时候,发现但参数携带参数"+",该收到数据"+"会变成空格。
代码如下(示例):
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/test", func(c *gin.Context) {
var title = c.DefaultQuery("title", "")
var content = c.DefaultQuery("content", "")
c.JSON(200, gin.H{
"title": title,
"content": content,
})
})
r.Run(":8080")
}
请求如下(示例):
发现服务端返回的数据中没有"+",并且被转化成空格。
通过 PHPStrom 点击"DefaultQuery",代码追溯到"net/url"包中, 下面看看具体代码逻辑。
// ParseQuery 解析 URL 编码的查询字符串并返回一个映射,列出为每个键指定的值。
func ParseQuery(query string) (Values, error) {
m := make(Values)
// m = map[] , query = "title=%E6%95%B0%E5%AD%A6&content=1+1=3?"
err := parseQuery(m, query)
return m, err
}
func parseQuery(m Values, query string) (err error) {
// 对query参数循环处理,直到为空终止
for query != "" {
key := query
// 返回Unicode代码顶一个索引,未匹配到返回-1, 这里提取query中kv值(列:title=%E6%95%B0%E5%AD%A6)
if i := strings.IndexAny(key, "&"); i >= 0 {
key, query = key[:i], key[i+1:]
} else {
query = ""
}
// 判断提取query中kv字符串包含“;”,包含则记录错误,跳过
if strings.Contains(key, ";") {
err = fmt.Errorf("invalid semicolon separator in query")
continue
}
// 判断提取query中kv字符串为空,则跳过
if key == "" {
continue
}
// 初始化value值
value := ""
// 提取kv字符串
if i := strings.Index(key, "="); i >= 0 {
key, value = key[:i], key[i+1:]
}
key, err1 := QueryUnescape(key)
if err1 != nil {
if err == nil {
err = err1
}
continue
}
value, err1 = QueryUnescape(value)
if err1 != nil {
if err == nil {
err = err1
}
continue
}
// 将处理好的 key、value 加入 map
m[key] = append(m[key], value)
}
return err
}
接下来我们看看函数 QueryUnescape。
// QueryUnescape 对 QueryEscape 进行逆向转换,将“%AB”形式的每个3字节编码子字符串转换为十六进制解码字节 0xAB。如果任何 % 后面没有跟两个十六进制数字,则返回错误。
func QueryUnescape(s string) (string, error) {
// 调用 unescape 函数,模式采用 encodeQueryComponent
return unescape(s, encodeQueryComponent)
}
// unescape 取消转义一个字符串; 模式指定 URL 字符串的哪一部分未被转义。
func unescape(s string, mode encoding) (string, error) {
n := 0
// hasPlus 标志是否存在加号,默认否
hasPlus := false
// 对字符串中的每个字符遍历
for i := 0; i < len(s); {
switch s[i] {
case '%':
n++
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
s = s[i:]
if len(s) > 3 {
s = s[:3]
}
return "", EscapeError(s)
}
if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" {
return "", EscapeError(s[i : i+3])
}
if mode == encodeZone {
v := unhex(s[i+1])<<4 | unhex(s[i+2])
if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) {
return "", EscapeError(s[i : i+3])
}
}
i += 3
// 判字符是"+"
case '+':
// 若 mode == encodeQueryComponent,则将 hasPlus == true
hasPlus = mode == encodeQueryComponent
i++
default:
if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
return "", InvalidHostError(s[i : i+1])
}
i++
}
}
// 若 n == 0 且 hasPlus == false,直接返回字符串 s
if n == 0 && !hasPlus {
return s, nil
}
// Builder 用于使用 Write 方法高效地构建字符串,它最小化内存复制,零值已准备好使用,不要复制非零生成器。
var t strings.Builder
// Grow 改变 t 的容量
t.Grow(len(s) - 2*n)
// 遍历字符串 s
for i := 0; i < len(s); i++ {
switch s[i] {
case '%':
t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2]))
i += 2
// 判断字符是否"+"
case '+':
//若 mode == encodeQueryComponent ,t 写入空字符串,否则写入"+"
if mode == encodeQueryComponent {
t.WriteByte(' ')
} else {
t.WriteByte('+')
}
default:
t.WriteByte(s[i])
}
}
return t.String(), nil
}
有些符号在URL中是不能直接传递的,如果要在URL中传递这些特殊符号,那么就要使用他们的编码了。编码的格式为:%加字符的ASCII码,即一个百分号%,后面跟对应字符的ASCII(16进制)码值。例如 "+"编码值是"%2B"。
请求如下(示例):
代码如下(示例):
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.POST("/test", func(c *gin.Context) {
type Data struct {
Title string `json:"title"`
Content string `json:"content"`
}
var data Data
err := c.BindJSON(&data)
if err != nil {
return
}
c.JSON(200, gin.H{
"title": data.Title,
"content": data.Content,
})
})
r.Run(":8080")
}
请求如下(示例):
c.Request.URL.RawQuery = strings.ReplaceAll(c.Request.URL.RawQuery, "+", "%2B")
代码如下(示例):
package main
import (
"github.com/gin-gonic/gin"
"strings"
)
func main() {
r := gin.Default()
r.GET("/test", func(c *gin.Context) {
c.Request.URL.RawQuery = strings.ReplaceAll(c.Request.URL.RawQuery, "+", "%2B")
var title = c.DefaultQuery("title", "")
var content = c.DefaultQuery("content", "")
c.JSON(200, gin.H{
"title": title,
"content": content,
})
})
r.Run(":8080")
}
请求如下(示例):