Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a first of its kind tool for helping you automatically index API endpoints across all your repositories. LiveAPI helps you discover, understand and use APIs in large tech infrastructures with ease.
Go's simplicity and performance make it a favorite for building APIs and web services. But when it comes to making HTTP requests, the standard library can feel a bit bare-bones. That’s where third-party libraries come in, offering cleaner APIs, better error handling, and advanced features like retries or middleware. In this post, we’ll explore the top Go HTTP request libraries, compare their strengths, and show you working examples to help you pick the right one for your project.
Let’s dive into the libraries, their use cases, and how they stack up.
Why Go’s Standard Library Isn’t Always Enough
Go’s net/http
package is powerful but low-level. It gives you full control over HTTP requests, but you’ll often end up writing repetitive code for things like JSON parsing, retries, or timeouts. Third-party libraries simplify these tasks, often with cleaner syntax and built-in features like request cancellation or middleware support.
For example, net/http
requires you to manually set headers, handle response bodies, and deal with errors. Libraries like Req or Resty abstract this away, letting you focus on your app’s logic. Let’s break down the top libraries and see what they offer.
The Go-To Choice: net/http (The Standard Library)
The standard net/http
package is the foundation for all HTTP requests in Go. It’s lightweight, stable, and gives you complete control. But it’s verbose, and you’ll need to handle things like JSON encoding/decoding or timeouts yourself.
Here’s a simple GET request example using net/http
:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
client := &http.Client{}
req, err := http.NewRequest("GET", "https://jsonplaceholder.typicode.com/users/1", nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
var user User
if err := json.Unmarshal(body, &user); err != nil {
fmt.Println("Error unmarshaling JSON:", err)
return
}
fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Name:Leanne Graham}
}
Key points:
- Pros: No dependencies, highly customizable, part of Go’s core.
- Cons: Verbose, no built-in support for retries or JSON handling.
- Use case: When you need fine-grained control or want to avoid external dependencies.
Official net/http documentation
Resty: The Developer-Friendly Powerhouse
Resty is a popular choice for developers who want a simple API with powerful features like retries, middleware, and JSON support. It’s built on top of net/http
but abstracts away much of the boilerplate.
Here’s how you’d rewrite the previous example with Resty:
package main
import (
"fmt"
"github.com/go-resty/resty/v2"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
client := resty.New()
var user User
resp, err := client.R().
SetResult(&user).
Get("https://jsonplaceholder.typicode.com/users/1")
if err != nil {
fmt.Println("Error sending request:", err)
return
}
if resp.IsError() {
fmt.Println("Request failed with status:", resp.Status())
return
}
fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Name:Leanne Graham}
}
Key points:
- Pros: Clean syntax, built-in JSON parsing, retry support, middleware hooks.
-
Cons: Slightly heavier than
net/http
, adds a dependency. - Use case: Ideal for most projects needing quick setup and robust features.
Req: Modern and Fluent API
Req is a newer library with a focus on a fluent, chainable API. It’s designed to be intuitive and supports advanced features like automatic retries, custom transports, and JSON handling. It’s a great choice if you want a modern, expressive way to make HTTP requests.
Example of a POST request with Req:
package main
import (
"fmt"
"github.com/imroc/req/v3"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
type PostResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
client := req.NewClient()
var response PostResponse
resp, err := client.R().
SetBody(User{ID: 1, Name: "John Doe"}).
SetResult(&response).
Post("https://jsonplaceholder.typicode.com/users")
if err != nil {
fmt.Println("Error sending request:", err)
return
}
if resp.IsError() {
fmt.Println("Request failed with status:", resp.Status)
return
}
fmt.Printf("Response: %+v\n", response)
// Output: Response: {ID:1 Name:John Doe}
}
Key points:
- Pros: Fluent API, lightweight, supports retries and timeouts.
- Cons: Newer library, smaller community than Resty.
- Use case: Great for developers who love chainable APIs and modern syntax.
Heimdall: Built for Resilience
Heimdall is designed for resilience, with built-in support for retries, circuit breakers, and timeouts. It’s ideal for distributed systems where reliability is critical, like microservices.
Here’s a GET request with Heimdall’s retry mechanism:
package main
import (
"encoding/json"
"fmt"
"github.com/gojek/heimdall/v7/httpclient"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
client := httpclient.NewClient(httpclient.WithRetryCount(3), httpclient.WithRetrier(httpclient.NewConstantBackoff(100*time.Millisecond, 500*time.Millisecond)))
resp, err := client.Get("https://jsonplaceholder.typicode.com/users/1", nil)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Name:Leanne Graham}
}
Key points:
- Pros: Robust retry and circuit breaker support, ideal for microservices.
- Cons: Steeper learning curve, overkill for simple projects.
- Use case: Best for high-availability systems needing fault tolerance.
Comparing the Libraries: Which One Fits Your Needs?
Choosing the right library depends on your project’s requirements. Here’s a comparison table to help you decide:
Library | Ease of Use | Retries | JSON Handling | Middleware | Best For |
---|---|---|---|---|---|
net/http | Moderate | Manual | Manual | Manual | Low-level control, no deps |
Resty | High | Built-in | Built-in | Yes | General-purpose, simplicity |
Req | High | Built-in | Built-in | Yes | Modern, fluent API |
Heimdall | Moderate | Built-in | Manual | Yes | Resilient, distributed systems |
Key points:
- Use
net/http
for minimal dependencies or full control. - Pick Resty or Req for most projects needing simplicity and features.
- Go with Heimdall for microservices or fault-tolerant systems.
Handling Errors and Timeouts Like a Pro
HTTP requests can fail for many reasons—network issues, server errors, or timeouts. Most libraries handle this better than net/http
. For example, Resty and Req let you set timeouts and retries easily, while Heimdall adds circuit breakers to prevent cascading failures.
Here’s how to set a timeout and retry with Resty:
package main
import (
"fmt"
"github.com/go-resty/resty/v2"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
client := resty.New().
SetTimeout(5 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(500 * time.Millisecond)
var user User
resp, err := client.R().
SetResult(&user).
Get("https://jsonplaceholder.typicode.com/users/1")
if err != nil {
fmt.Println("Error sending request:", err)
return
}
if resp.IsError() {
fmt.Println("Request failed with status:", resp.Status())
return
}
fmt.Printf("User: %+v\n", user)
// Output: User: {ID:1 Name:Leanne Graham}
}
Key points:
- Timeouts: Prevent requests from hanging indefinitely.
- Retries: Handle transient failures (e.g., network blips).
- Circuit breakers (Heimdall): Stop requests to failing services.
Real-World Use Case: Building a Client for an API
Let’s say you’re building a client for a REST API that fetches user data and posts updates. Here’s how you’d do it with Req, which balances simplicity and power:
package main
import (
"fmt"
"github.com/imroc/req/v3"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
UserID int `json:"userId"`
}
func main() {
client := req.NewClient().
SetBaseURL("https://jsonplaceholder.typicode.com")
// Fetch user
var user User
_, err := client.R().
SetResult(&user).
Get("/users/1")
if err != nil {
fmt.Println("Error fetching user:", err)
return
}
fmt.Printf("User: %+v\n", user)
// Post data
var post Post
resp, err := client.R().
SetBody(Post{Title: "New Post", UserID: user.ID}).
SetResult(&post).
Post("/posts")
if err != nil {
fmt.Println("Error posting data:", err)
return
}
if resp.IsError() {
fmt.Println("Post failed with status:", resp.Status)
return
}
fmt.Printf("Posted: %+v\n", post)
// Output:
// User: {ID:1 Name:Leanne Graham}
// Posted: {ID:101 Title:New Post UserID:1}
}
Key points:
- Req’s SetBaseURL: Simplifies API calls with a base URL.
- Chaining: Makes the code readable and maintainable.
- Use case: Perfect for building API clients with minimal boilerplate.
Tips for Choosing and Using These Libraries
When picking an HTTP library, consider your project’s needs:
-
Small projects: Stick with
net/http
to avoid dependencies. - Medium projects: Use Resty or Req for simplicity and features.
- Microservices: Choose Heimdall for resilience.
- Always set timeouts: Prevent hanging requests.
- Log errors: Add logging to debug issues (Resty and Req support this).
- Test your client: Use mock servers (e.g., httpbin.org) to test edge cases.
Each library has trade-offs, but they all build on net/http
, so you can mix and match if needed. For example, you can use net/http
’s http.Client
with Resty or Req for custom transports.
What’s Next for Your HTTP Adventures
Picking the right HTTP library depends on your project’s complexity and goals. If you’re just starting out, Resty or Req will save you time with their simple APIs and built-in features. For critical systems, Heimdall’s resilience features are a game-changer. And if you’re a minimalist, net/http
is always there.
Experiment with these libraries in your next project. Try making a small API client with Req or add retries to an existing net/http
setup. The examples above are ready to run—copy them, tweak them, and see what works best for you. Go’s ecosystem is rich, and these libraries make HTTP requests a breeze.