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

Go http.NewRequest 怎样构造带查询参数的请求

来源:17golang原创

时间:2026-10-21 23:41:00 192浏览 收藏

我在写 Go HTTP 客户端时,最容易先写出来的是把查询字符串直接拼到 URL 后面,例如 ?tag=go&tag=web。真正遇到中文、空格、重复参数或需要切换请求方法时,这种写法很快就变得难维护。更稳妥的做法是先用 url.Values 组装参数,再把编码后的查询串交给 http.NewRequest。

官方地址:https://pkg.go.dev/net/http#NewRequest

要点速览
  • url.Values 能表达重复键,并负责查询参数编码。
  • NewRequest 只负责构造请求;网络错误要在 Client.Do 处处理。
  • 收到响应后必须关闭 resp.Body,非 2xx 状态也不会自动变成 Go error。

先把查询参数从 URL 拼接中拆出来

查询参数本质上是 url.Values,它的值类型是“一个键对应多个字符串”。这正好覆盖筛选条件、分页参数和同名参数。使用 Add 表示追加,使用 Set 表示覆盖;两者不要混用到不清楚语义。

package main

import (
    "fmt"
    "net/url"
)

func buildQuery() string {
    values := url.Values{}
    values.Add("tag", "go")       // Add 保留同名参数,适合多选筛选条件
    values.Add("tag", "http")     // 第二个 tag 会以重复键形式编码
    values.Set("page", "2")       // Set 明确表示 page 只有一个最终值
    values.Set("keyword", "中文 教程") // Encode 会处理中文和空格

    return values.Encode() // 返回可直接放入 URL 的查询串
}

func main() {
    fmt.Println(buildQuery()) // 结果顺序按键排序,值会经过百分号编码
}

Encode 的结果适合放在 URL 的 RawQuery 中。不要先手写一部分编码,再把另一部分原文拼进去,否则同一个参数集合可能出现两套编码规则。

Go url.Values 组装重复查询参数并编码后交给 http.NewRequest 的结构说明图
图1:查询参数组装说明图,展示 url.Values、Encode 和 NewRequest 之间的输入关系;这是原创静态说明图,不是运行截图。

用 NewRequest 构造带查询参数的 GET 请求

http.NewRequest 的职责是根据方法、URL 和可选正文创建 *http.Request。查询参数已经由 url.Values.Encode 生成,因此这里只处理 URL 的组合和构造错误,不把网络访问混进来。

package main

import (
    "fmt"
    "net/http"
    "net/url"
)

func newSearchRequest(endpoint string) (*http.Request, error) {
    query := url.Values{}
    query.Add("tag", "go")          // 允许服务端收到多个同名 tag
    query.Add("tag", "http")
    query.Set("page", "2")          // 分页参数只保留一个值
    query.Set("keyword", "中文 教程")

    target, err := url.Parse(endpoint)
    if err != nil {
        return nil, fmt.Errorf("解析接口地址失败: %w", err) // 先拦截非法基础 URL
    }
    target.RawQuery = query.Encode() // 由 url.Values 统一负责查询串编码

    req, err := http.NewRequest(http.MethodGet, target.String(), nil)
    if err != nil {
        return nil, fmt.Errorf("构造 HTTP 请求失败: %w", err) // 方法或 URL 不合法时返回
    }
    req.Header.Set("Accept", "application/json") // 只设置本次请求需要的响应格式
    return req, nil
}

func main() {
    req, err := newSearchRequest("https://api.example.com/search")
    if err != nil {
        panic(err)
    }
    fmt.Println(req.URL.String()) // 检查最终 URL,不代表请求已经发出
}

官方文档把 NewRequest 描述为使用后台 context 的构造函数;如果一次请求需要独立的取消或截止时间,应改用 NewRequestWithContext,而不是在请求创建后再猜测如何补上下文。

把请求交给 Client.Do,并区分两类错误

请求对象创建成功,只说明方法和 URL 已经能组成一个请求。真正的 DNS、连接、TLS、重定向和响应读取问题会在 client.Do 阶段出现。另一个容易忽略的点是:服务器返回 404 或 500 时,Do 通常仍然返回一个响应对象,调用方需要自己判断状态码。

func fetch(req *http.Request) error {
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("发送请求失败: %w", err) // 传输层或重定向处理失败
    }
    defer resp.Body.Close() // 读取结束后释放响应体和连接复用资格

    if resp.StatusCode = 300 {
        return fmt.Errorf("服务端返回 HTTP %s", resp.Status) // 非 2xx 由业务决定是否报错
    }
    // 这里继续读取 resp.Body;示例只关注请求构造和资源清理
    return nil
}

生产代码通常会复用同一个 http.Client,并在需要时为单次请求绑定 context。客户端复用和参数构造是两件事:前者影响连接池,后者影响请求 URL 的可读性与正确性。

Go http.NewRequest 到 Client.Do 再到响应状态判断和 Body 关闭的流程说明图
图2:客户端请求生命周期说明图,展示请求构造、传输错误、状态码判断和响应体关闭的先后关系;这是原创静态说明图,不是运行截图。

POST 请求也要分清查询参数和请求正文

查询参数属于 URL,JSON 或表单数据通常属于请求正文。两者可以同时存在,但不要为了传递筛选条件把所有内容都塞进正文,也不要把 JSON 字符串当作查询参数直接拼接。

func newCreateRequest(endpoint string, body io.Reader) (*http.Request, error) {
    query := url.Values{}
    query.Set("dry_run", "true") // 这是 URL 上的控制参数

    target, err := url.Parse(endpoint)
    if err != nil {
        return nil, fmt.Errorf("解析地址失败: %w", err) // 基础 URL 不合法时停止
    }
    target.RawQuery = query.Encode()

    req, err := http.NewRequest(http.MethodPost, target.String(), body)
    if err != nil {
        return nil, fmt.Errorf("构造 POST 请求失败: %w", err) // body 只是输入流,不负责编码
    }
    req.Header.Set("Content-Type", "application/json") // 告诉服务端正文格式
    return req, nil
}

如果正文来自 bytes.Reader、strings.Reader 等类型,标准库可以获得更准确的长度并在部分重定向场景复用正文;如果传入的是普通流,则应按一次性读取来设计错误恢复。

按现象排查参数和请求构造问题

现象优先检查处理方式
中文或空格参数被截断是否绕过 url.Values.Encode统一由 url.Values 生成 RawQuery
同名参数只剩最后一个是否对同一键使用了 Set多值条件使用 Add,单值字段使用 Set
NewRequest 返回 error基础 URL、方法或最终 URL 是否非法保留原始错误并在构造阶段返回
Do 没有 error 但业务失败是否只判断了 Go error显式判断 StatusCode,并读取服务端错误正文
请求后连接越来越多是否关闭了 resp.Body拿到非 nil 响应后立即安排 defer resp.Body.Close()

我通常会先打印 req.URL.String() 和最终的 RawQuery,确认问题发生在“参数组装”还是“网络发送”。这比在 Client.Do 失败后盲目修改超时更快,也能避免把查询参数错误误判成服务端路由问题。

常见问题

为什么不直接把参数拼成字符串?

简单 ASCII 参数可以拼接,但重复键、中文、空格和特殊字符会让编码规则分散。url.Values 把这些规则集中在一个对象里,更适合维护。

Set 和 Add 有什么区别?

Set 会替换一个键已有的值,Add 会追加一个值。筛选标签等多选条件通常用 Add,分页或排序字段通常用 Set。

构造请求成功是不是代表接口调用成功?

不是。构造成功只代表请求对象可用;连接和响应阶段要看 Client.Do,业务结果还要看 HTTP 状态码和响应正文。

什么时候使用 NewRequestWithContext?

当请求需要取消、截止时间或调用链上下文时使用它。context 会覆盖客户端请求生命周期,而不是只控制建立连接这一小段。

把查询参数交给 url.Values,把 URL、方法和正文交给 http.NewRequest,再在 Client.Do 后分别处理传输错误、状态码和响应体,这条边界清晰的流程就能覆盖大多数 Go 客户端请求场景。

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