Skip to content
AgileLogics
7 min read

Making HTTP Requests

Prerequisites: Working with dates & time

The net/http package includes a full HTTP client. The typical flow is:

  1. Create an *http.Client with a timeout — the default client (http.Get) has no timeout and can hang forever on a slow server.
  2. Send the request and check the error. An error here means the request failed at the network level (DNS, connection refused, timeout).
  3. Always close the response body with defer resp.Body.Close(), or the underlying connection can't be reused.
  4. Check resp.StatusCode. A 404 or 500 is not an error from Go's point of view — you must check it yourself.
  5. Decode the body, often with json.NewDecoder(resp.Body).Decode(&v).

Syntax

client := &http.Client{Timeout: 10 * time.Second}

resp, err := client.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

// Full control over method, headers and body:
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err = client.Do(req)

Example

This example starts a local test server with net/http/httptest so it runs without internet access. In real code, the URL would point at a real API.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/http/httptest"
    "strings"
    "time"
)

type Todo struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

func fetchTodo(client *http.Client, url string) (Todo, error) {
    resp, err := client.Get(url)
    if err != nil {
        return Todo{}, fmt.Errorf("get %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
        return Todo{}, fmt.Errorf("unexpected status %s: %s",
            resp.Status, strings.TrimSpace(string(body)))
    }

    var t Todo
    if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
        return Todo{}, fmt.Errorf("decode todo: %w", err)
    }
    return t, nil
}

func main() {
    // A stand-in API that only knows about todo 1.
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/todos/1" {
            http.NotFound(w, r)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprint(w, `{"id": 1, "title": "Learn Go", "done": false}`)
    }))
    defer srv.Close()

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

    todo, err := fetchTodo(client, srv.URL+"/todos/1")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", todo)

    _, err = fetchTodo(client, srv.URL+"/todos/99")
    fmt.Println(err)
}

Expected output

{ID:1 Title:Learn Go Done:false}
unexpected status 404 Not Found: 404 page not found

io.LimitReader stops a misbehaving server from making you read a huge error body into memory.

Common mistakes

  • Using http.Get or &http.Client{} in production code without a timeout.
  • Forgetting defer resp.Body.Close(), which leaks connections. (Only close it after checking err — when err != nil, resp is nil.)
  • Treating err == nil as success. Check the status code as well.
  • Creating a new http.Client for every request. Clients are safe for concurrent use and reuse connections; create one and share it.

Practice

Write a reusable helper getJSON(client *http.Client, url string, target any) error that performs a GET request, returns an error for any non-200 status, and decodes the JSON body into target. Use it against a test server that returns {"name": "gopher", "age": 14}.

Show solution
package main

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

func getJSON(client *http.Client, url string, target any) error {
    resp, err := client.Get(url)
    if err != nil {
        return fmt.Errorf("get %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("get %s: unexpected status %s", url, resp.Status)
    }
    if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
        return fmt.Errorf("decode %s: %w", url, err)
    }
    return nil
}

func main() {
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, `{"name": "gopher", "age": 14}`)
    }))
    defer srv.Close()

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

    var person struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    if err := getJSON(client, srv.URL, &person); err != nil {
        log.Fatal(err)
    }
    fmt.Println(person.Name, person.Age) // gopher 14
}