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))
}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)
}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
}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")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,
}
}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()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)
}Here bytes.NewReader(body) converts the JSON []byte into something the request can read as a body.