在Golang里做HTTP客户端开发,经常需要给发出的请求加上特定的Header,比如鉴权令牌、调用方标识或者链路追踪ID。标准库net/http把请求头抽象成http.Header类型,本质上是一个map[string][]string,因此同一个key可以对应多个值。理解这个结构,是实现自定义Header处理的基础。

使用net/http设置请求Header的基本方式
最常见做法是先通过http.NewRequest构造一个请求对象,然后直接操作它的Header字段。http.Header提供了Set、Add、Get、Del等方法,分别用于覆盖写入、追加写入、读取和删除。需要注意的是,Set会清空该key已有的所有值再写入新值,而Add是在原有值后面追加。
下面是一段最基础的示例,展示如何给GET请求加上自定义Header:
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
// 构造请求
req, err := http.NewRequest("GET", "https://ipipp.com/api/test", nil)
if err != nil {
panic(err)
}
// 覆盖式写入单值Header
req.Header.Set("Authorization", "Bearer token_abc123")
// 追加式写入,同一个key可多个值
req.Header.Add("X-Trace-Id", "trace-001")
req.Header.Add("X-Trace-Id", "trace-002")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
上面的代码里,Authorization只会出现一个值,而X-Trace-Id在服务端收到的请求中会有两个。很多初学者误以为Set和Add没差别,结果在需要传递多个相同Header时用了Set,导致前面的数据被冲掉。如果业务要求保留全部值,就必须用Add。
另外,http.Header的key在写入时会自动规范化为首字母大写、连字符分隔的形式,例如写x-trace-id也会被存为X-Trace-Id。这是遵循HTTP协议规范的行为,开发者不必自己手动转换大小写,但读取时也要用规范后的key。
封装可复用的Header处理中间件
在真实项目中,几乎每个对外请求都要带一堆公共Header,比如服务名、环境标识、认证信息。如果每次都手写Set和Add,既繁琐又容易遗漏。更合理的做法是将Header处理逻辑抽成一个函数,在发起请求前统一调用。
我们可以定义一个类型来承载公共Header,并提供一个Apply方法将其写入任意请求:
package main
import (
"net/http"
)
// CommonHeader 封装公共请求头
type CommonHeader struct {
ServiceName string
Env string
Token string
}
// Apply 将公共头写入请求
func (c *CommonHeader) Apply(req *http.Request) {
req.Header.Set("X-Service-Name", c.ServiceName)
req.Header.Set("X-Env", c.Env)
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
}
}
func newRequestWithHeader(method, url string, h *CommonHeader) (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
h.Apply(req)
return req, nil
}
这种封装让业务代码更干净,也方便在统一位置修改Header策略。例如后续要接入新的鉴权方案,只需调整Apply内部逻辑,不用改动所有调用点。对于多团队协作的系统,这种集中管理能显著降低沟通成本。
如果项目使用了自定义的http.Client,还可以借助RoundTripper接口在更底层统一注入Header。RoundTripper是每次请求必经的传输层,写一个包装类型的RoundTripper,就能在不改动业务代码的情况下,给所有经过该Client的请求自动加头。这种方式比每个函数里调用Apply更透明,也更适合做横切关注点处理。
处理特殊Header与常见误区
有些Header由net/http自动管理,不建议手动覆盖。例如Host头部在正常请求中由URL决定,虽然可以通过req.Host修改,但容易和服务端虚拟主机匹配逻辑冲突。还有Content-Type,在带body的请求里,如果调用了Set来传JSON,要确保值和实际编码一致,否则服务端可能解析失败。
另一个常见误区是认为Header值可以随意包含换行或空格。HTTP协议规定Header值不能包含换行,否则会被视为请求走私攻击。Golang在调用Set时不会主动过滤,但服务端或代理可能拒绝。因此写入前最好做基本的清理:
package main
import (
"net/http"
"strings"
)
func safeSet(req *http.Request, key, val string) {
// 去除换行和回车,避免非法Header
clean := strings.ReplaceAll(val, "n", "")
clean = strings.ReplaceAll(clean, "r", "")
req.Header.Set(key, clean)
}
func example() {
req, _ := http.NewRequest("POST", "https://ipipp.com/upload", nil)
safeSet(req, "X-User-Comment", "hellonworld")
}
这段代码展示了如何简单过滤掉危险字符。虽然多数内部服务不会遇到恶意输入,但在网关或对外API场景下,这类防御能减少很多奇怪的线上问题。同时要注意,Header名称本身也只能使用特定字符集,不要用中文或特殊符号做key。
最后,当使用http.Get这种快捷方法时,是无法直接加自定义Header的,因为它内部已经发起了请求。任何自定义Header处理都必须走http.NewRequest加client.Do这条路径,或者使用第三方封装库但确认其暴露了Header设置入口。理清这一点,才能避免在快捷方法和自定义需求之间反复踩坑。
通过Transport统一注入Header
前面提到的RoundTripper,在标准库里对应http.Transport。我们可以包装一个自定义的Transport,在RoundTrip方法里给请求补Header,再用它创建Client。这样连newRequestWithHeader都不用每次调用。
package main
import (
"net/http"
)
// HeaderTransport 包装原有Transport,自动加头
type HeaderTransport struct {
base http.RoundTripper
headers map[string]string
}
func (t *HeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for k, v := range t.headers {
req.Header.Set(k, v)
}
return t.base.RoundTrip(req)
}
func newClientWithHeaders(headers map[string]string) *http.Client {
return &http.Client{
Transport: &HeaderTransport{
base: http.DefaultTransport,
headers: headers,
},
}
}
这种写法把Header逻辑彻底下沉到传输层,业务侧只需要像平时一样用client.Do即可,完全感知不到Header的存在。对于需要对接多个下游、且每个下游要求不同固定头的场景,可以为不同Client实例配置不同headers映射,互不干扰。
不过也要注意,Transport层拿到的req可能被重试或重定向复用,如果在RoundTrip里依赖请求上下文动态取值,要通过req.Context()获取,而不是闭包捕获固定变量。否则在复杂调用链里会出现Header串味的问题。掌握好基础Set、Add与Transport封装,Golang里的HTTP请求Header自定义处理就能做到既灵活又安全。
GolangHTTP_Header自定义请求修改时间:2026-08-08 23:12:34