HTTP Requests in Go: Only The Most Useful Libraries
Shrijith Venkatramana

Shrijith Venkatramana @shrsv

About: Founder @ hexmos.com

Joined:
Jan 4, 2023

HTTP Requests in Go: Only The Most Useful Libraries

Publish Date: Jul 21
15 0

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}
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment