Gin 处理GET请求获取URL 参数加号(“+“)问题

一、问题描述

最近在使用 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")
}

请求如下(示例):

Gin 处理GET请求获取URL 参数加号(“+“)问题_第1张图片

发现服务端返回的数据中没有"+",并且被转化成空格。

二、问题分析

通过 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
}

三、解决问题

  1. 请求参数中"+"替换"%2B"

有些符号在URL中是不能直接传递的,如果要在URL中传递这些特殊符号,那么就要使用他们的编码了。编码的格式为:%加字符的ASCII码,即一个百分号%,后面跟对应字符的ASCII(16进制)码值。例如 "+"编码值是"%2B"。

请求如下(示例):

Gin 处理GET请求获取URL 参数加号(“+“)问题_第2张图片

2.将请求 GET 方式修改为 POST

代码如下(示例):

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")
}

请求如下(示例):

Gin 处理GET请求获取URL 参数加号(“+“)问题_第3张图片

3.将接收到 GET 参数匹配"+"替换为"%2B"

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")
}

请求如下(示例):

Gin 处理GET请求获取URL 参数加号(“+“)问题_第4张图片

你可能感兴趣的:(Gin,Go,gin,url)