Making HTTP Requests
Prerequisites: Working with dates & time
The net/http package includes a full HTTP client. The typical flow is:
- Create an
*http.Clientwith a timeout — the default client (http.Get) has no timeout and can hang forever on a slow server. - Send the request and check the error. An error here means the request failed at the network level (DNS, connection refused, timeout).
- Always close the response body with
defer resp.Body.Close(), or the underlying connection can't be reused. - Check
resp.StatusCode. A404or500is not an error from Go's point of view — you must check it yourself. - 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.Getor&http.Client{}in production code without a timeout. - Forgetting
defer resp.Body.Close(), which leaks connections. (Only close it after checkingerr— whenerr != nil,respisnil.) - Treating
err == nilas success. Check the status code as well. - Creating a new
http.Clientfor 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
}