Question
I am making a simple HTTP GET request in Go:
client := &http.Client{}
req, _ := http.NewRequest("GET", url, nil)
res, _ := client.Do(req)
How can I customize the request headers? I could not find a clear way to do that in the documentation.
Short Answer
By the end of this page, you will understand how to add custom headers to an HTTP GET request in Go using the net/http package. You will also learn the difference between creating a request and sending it, how headers are stored, common mistakes to avoid, and how this pattern is used in real Go codebases.
Concept
In Go, HTTP headers are part of the http.Request object. That means you do not pass headers directly into client.Do(). Instead, you:
- Create a request with
http.NewRequest() - Add or modify headers on the request
- Send the request with
client.Do()
A header is just metadata sent with the request. Common examples include:
Authorizationfor tokens or API keysAcceptto say what response format you wantUser-Agentto identify the clientContent-Typefor request bodies
In Go, headers are stored in req.Header, which is a map-like structure of type http.Header.
Example:
req.Header.Set("Authorization", "Bearer my-token")
req.Header.Set("Accept", "application/json")
This matters because many real APIs require headers for:
- authentication
- versioning
- content negotiation
- tracing and debugging
- rate limiting or client identification
If you do not set the required headers, the server may reject your request or return unexpected results.
Mental Model
Think of an HTTP request like mailing a package.
- The URL is the destination address.
- The method (
GET,POST) is the type of delivery. - The body is the package contents.
- The headers are the labels on the box.
For example:
Authorizationis like a security badge.Accept: application/jsonis like saying, "Please reply in JSON format."User-Agentis like writing who sent the package.
In Go, you first prepare the box (http.NewRequest), then attach labels (req.Header.Set(...)), and only then send it (client.Do(req)).
Syntax and Examples
The basic syntax is:
req, err := http.NewRequest("GET", url, nil)
if err != nil {
// handle error
}
req.Header.Set("Header-Name", "value")
res, err := client.Do(req)
if err != nil {
// handle error
}
defer res.Body.Close()
Example: Add custom headers to a GET request
package main
import (
"fmt"
"net/http"
)
func main() {
client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.example.com/users", nil)
if err != nil {
fmt.Println("request creation error:", err)
return
}
req.Header.Set("Authorization", "Bearer my-token")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "my-go-app/1.0")
res, err := client.Do(req)
if err != nil {
fmt.Println("request error:", err)
return
}
defer res.Body.Close()
fmt.Println("Status:", res.Status)
}
Set vs Add
Go provides two common ways to insert headers:
req.Header.Set("Accept", "application/json")
req.Header.Add("Accept", "text/plain")
Setreplaces any existing value for that header.Addappends another value for the same header.
Use Set most of the time unless you intentionally want multiple values.
Step by Step Execution
Consider this example:
client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer abc123")
res, err := client.Do(req)
if err != nil {
return
}
defer res.Body.Close()
Here is what happens step by step:
-
client := &http.Client{}- Creates an HTTP client that can send requests.
-
http.NewRequest("GET", "https://api.example.com/data", nil)- Creates a new request object.
- The method is
GET. - The URL is
https://api.example.com/data. - The body is
nilbecause a simple GET request usually has no body.
-
req.Header.Set("Accept", "application/json")- Adds an
Acceptheader. - This tells the server that JSON is preferred.
- Adds an
-
req.Header.Set("Authorization", "Bearer abc123")- Adds an authorization token.
- Many APIs require this header to identify the caller.
-
client.Do(req)- Sends the request, including the headers.
- The server receives the method, URL, and header values.
-
defer res.Body.Close()- Ensures the response body is closed after use.
- This is important to avoid resource leaks.
The key idea is that the headers must be added before client.Do(req) is called.
Real World Use Cases
Custom request headers are used in many practical situations:
Calling authenticated APIs
req.Header.Set("Authorization", "Bearer "+token)
Used when talking to services like GitHub, Stripe, or internal company APIs.
Asking for JSON responses
req.Header.Set("Accept", "application/json")
Useful when an API can return multiple formats.
Sending API keys
req.Header.Set("X-API-Key", apiKey)
Some services use custom headers instead of bearer tokens.
Identifying your client
req.Header.Set("User-Agent", "inventory-sync/2.1")
Helpful for logs, monitoring, or service rules.
Tracing requests across services
req.Header.Set("X-Request-ID", requestID)
Common in distributed systems and microservices.
Real Codebase Usage
In real Go projects, developers usually do more than just set one header manually.
Validation before sending requests
A common pattern is to validate required input first:
if token == "" {
return fmt.Errorf("missing token")
}
Then build the request and set headers.
Helper functions
Teams often wrap request creation in reusable functions:
func newAPIRequest(method, url, token string) (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
return req, nil
}
This keeps header logic consistent.
Guard clauses and early returns
Instead of deeply nested code, Go codebases often use early error checks:
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
return err
}
Shared clients
A single http.Client is often reused instead of creating a new one for every request.
var client = &http.Client{}
This is more common in production code.
Configuration-driven headers
Some headers come from config or environment variables:
req.Header.Set("X-API-Key", os.Getenv("API_KEY"))
This keeps secrets out of source code.
Common Mistakes
Here are common beginner mistakes when setting headers in Go.
1. Forgetting to set headers on the request
Broken code:
client := &http.Client{}
res, err := client.Do(req)
req.Header.Set("Authorization", "Bearer token")
Problem:
- The request was already sent.
- Setting headers afterward does nothing for that request.
Fix:
req.Header.Set("Authorization", "Bearer token")
res, err := client.Do(req)
2. Ignoring errors from http.NewRequest
Broken code:
req, _ := http.NewRequest("GET", url, nil)
Problem:
- If request creation fails, you may get unexpected behavior later.
Fix:
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
3. Forgetting to close the response body
Broken code:
res, err := client.Do(req)
if err != nil {
return err
}
Problem:
- The response body stays open.
- This can leak resources.
Fix:
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
4. Using Add when Set is intended
Broken code:
req.Header.Add("Authorization", "Bearer token1")
req.Header.Add("Authorization", "Bearer token2")
Problem:
- You now have multiple authorization values.
- That is usually invalid.
Fix:
req.Header.Set("Authorization", "Bearer token")
5. Setting Content-Type on a GET request without a body
This is not always wrong, but it often has no purpose. Content-Type describes the request body format. For a plain GET with no body, Accept is often the more relevant header.
6. Misspelling header names or values
Example:
req.Header.Set("Authorisation", "Bearer token")
Many APIs expect exact standard names like Authorization. Use the documented header name from the API.
Comparisons
Here is how related HTTP request concepts compare in Go:
| Concept | Purpose | Example | Common Use |
|---|---|---|---|
req.Header.Set() | Set or replace a header value | req.Header.Set("Accept", "application/json") | Most common choice |
req.Header.Add() | Add another value for the same header | req.Header.Add("Cache-Control", "no-cache") | Multiple values when needed |
http.NewRequest() | Create a request object | http.NewRequest("GET", url, nil) | First step before sending |
client.Do(req) | Send the prepared request | client.Do(req) | Executes the request |
http.Get() | Quick GET helper | http.Get(url) | Simple requests without customization |
http.Get() vs http.NewRequest()
| Option | Best for | Limitation |
|---|---|---|
http.Get(url) | Very simple GET requests | Harder to customize headers before sending |
http.NewRequest() + client.Do() | Flexible and production-style requests | Slightly more code |
If you need custom headers, http.NewRequest() is usually the right choice.
Cheat Sheet
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer token")
req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
Quick rules
- Use
req.Header.Set(key, value)to set a header. - Set headers before calling
client.Do(req). - Use
Setto replace a value. - Use
Addto append another value. - Always check errors from
http.NewRequestandclient.Do. - Always close
res.Body. - Use
Acceptfor expected response format. - Use
Content-Typemainly when sending a request body.
Common headers
AuthorizationAcceptUser-AgentX-API-KeyX-Request-ID
Best practice
Prefer http.NewRequest() over http.Get() when you need custom headers or more control.
FAQ
How do I add headers to a GET request in Go?
Create the request with http.NewRequest(), then use req.Header.Set("Name", "Value") before sending it with client.Do(req).
Can I set headers with http.Get() in Go?
Not directly in a clean way. If you need custom headers, create a request manually with http.NewRequest().
What is the difference between Set and Add for headers in Go?
Set replaces any existing value. Add appends another value under the same header name.
Do I need Content-Type for a GET request?
Usually no, because GET requests often have no body. Accept is often more useful for GET requests.
Why is my Authorization header not being sent?
A common reason is setting the header after calling client.Do(req). Headers must be added before sending the request.
Are header names case-sensitive in Go?
Go handles headers in a canonical form, and HTTP header names are generally treated case-insensitively. Still, use standard names like Authorization and Accept for clarity.
Should I reuse http.Client in Go?
Yes, in most real applications you should reuse clients instead of creating a new one for every request.
How do I send multiple headers in Go?
Call req.Header.Set() multiple times, once for each header you want to include.
Mini Project
Description
Build a small Go program that fetches user data from an API endpoint using a custom Authorization header and an Accept header. This demonstrates the full request flow: creating a request, setting headers, sending it, and handling the response safely.
Goal
Create a GET request in Go that includes custom headers and prints the HTTP status returned by the server.
Requirements
- Create an
http.Clientand build a GET request withhttp.NewRequest - Add at least two headers:
AuthorizationandAccept - Send the request using
client.Do(req) - Check for errors when creating and sending the request
- Close the response body after the request completes
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.