cliente-http-go-consumir-apis

HTTP Client in Go for Consuming APIs and Handling JSON

  • 4 min

An HTTP client in Go is a program that sends requests to other web services and processes their responses.

If in the previous article we set up our own server, now we are on the other side. We are going to consume an API, read JSON, and manage common real-life failures, which always appear when a network is involved.

Because it is one thing to call http.Get in an example and quite another to write code that doesn’t hang forever if the remote server decides to take the afternoon off.

The quick way: http.Get

For a simple request we can use http.Get.

package main

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

func main() {
    resp, err := http.Get("https://api.example.com/status")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(body))
}
Copied!

Do not forget to close resp.Body. For an HTTP/1 connection to be reusable, we must normally read the body to the end and close it. In the example, io.ReadAll already does the first part.

Check the status code

That there is no network error does not mean the response is correct.

resp, err := http.Get("https://api.example.com/users")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected response: %s", resp.Status)
}
Copied!

A 404, a 500, or a 401 are not transport errors. They are valid HTTP responses that we must interpret.

Decode JSON

The usual thing is that an API returns JSON. We can decode it directly from the response body.

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type Usuario struct {
    ID     int    `json:"id"`
    Nombre string `json:"nombre"`
    Email  string `json:"email"`
}

var clienteHTTP = &http.Client{Timeout: 5 * time.Second}

func obtenerUsuario(id int) (*Usuario, error) {
    url := fmt.Sprintf("https://api.example.com/users/%d", id)

    resp, err := clienteHTTP.Get(url)
    if err != nil {
        return nil, fmt.Errorf("query user %d: %w", id, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("query user %d: response %s", id, resp.Status)
    }

    var usuario Usuario
    err = json.NewDecoder(resp.Body).Decode(&usuario)
    if err != nil {
        return nil, fmt.Errorf("decode user %d: %w", id, err)
    }

    return &usuario, nil
}
Copied!

Notice that we return (*Usuario, error). It is the usual pattern in Go: result and error, without sweeping anything under the rug.

Use a client with a timeout

http.Get uses the default client, which does not have a global time limit. For production code, it is advisable to create and reuse our own http.Client.

cliente := &http.Client{
    Timeout: 5 * time.Second,
}

resp, err := cliente.Get("https://api.example.com/users")
Copied!

That limit includes the connection, redirects, and reading the body. To control specific phases we can configure the Transport or use a context with a deadline.

func nuevoCliente() *http.Client {
    return &http.Client{
        Timeout: 5 * time.Second,
    }
}
Copied!

Clients and their transports are safe for concurrent use and should be reused to take advantage of the connection pool.

Requests with headers

When we need headers, tokens, or different methods, we use http.NewRequestWithContext. The context allows canceling the request if the caller finishes or reaches its deadline.

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users", nil)
if err != nil {
    return err
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

resp, err := cliente.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()
Copied!

cliente.Do(req) is the general form. It allows using any method, adding headers, and propagating cancellation.

Send JSON with POST

To send data, we encode a structure to JSON.

type CrearUsuario struct {
    Nombre string `json:"nombre"`
    Email  string `json:"email"`
}

datos := CrearUsuario{
    Nombre: "Ada",
    Email:  "[email protected]",
}

body, err := json.Marshal(datos)
if err != nil {
    return err
}

req, err := http.NewRequestWithContext(
    ctx,
    http.MethodPost,
    "https://api.example.com/users",
    bytes.NewReader(body),
)
if err != nil {
    return err
}

req.Header.Set("Content-Type", "application/json")

resp, err := cliente.Do(req)
if err != nil {
    return fmt.Errorf("create user: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
    return fmt.Errorf("create user: response %s", resp.Status)
}
Copied!

Here bytes.NewReader(body) converts the JSON []byte into something the request can read as a body.