Created
February 1, 2026 21:08
-
-
Save brandonhimpfen/c1f9398053063818a5737be743645bbf to your computer and use it in GitHub Desktop.
Go HTTP client example with context cancellation + sensible timeouts, returning response body safely (idiomatic net/http).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "io" | |
| "log" | |
| "net" | |
| "net/http" | |
| "time" | |
| ) | |
| // newHTTPClient returns an HTTP client with sensible defaults. | |
| // | |
| // Notes: | |
| // - http.Client.Timeout covers the full request (connect + redirects + reading body). | |
| // - Transport timeouts provide more granular control. | |
| func newHTTPClient() *http.Client { | |
| transport := &http.Transport{ | |
| Proxy: http.ProxyFromEnvironment, | |
| DialContext: (&net.Dialer{ | |
| Timeout: 5 * time.Second, // TCP connect timeout | |
| KeepAlive: 30 * time.Second, // keepalive | |
| }).DialContext, | |
| TLSHandshakeTimeout: 5 * time.Second, | |
| ResponseHeaderTimeout: 10 * time.Second, | |
| ExpectContinueTimeout: 1 * time.Second, | |
| IdleConnTimeout: 90 * time.Second, | |
| MaxIdleConns: 100, | |
| } | |
| return &http.Client{ | |
| Transport: transport, | |
| Timeout: 15 * time.Second, // hard upper bound for whole request | |
| } | |
| } | |
| // get performs a GET request with a context. | |
| // It returns the response body as bytes and fails if status is not 2xx. | |
| func get(ctx context.Context, client *http.Client, url string) ([]byte, error) { | |
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | |
| if err != nil { | |
| return nil, fmt.Errorf("new request: %w", err) | |
| } | |
| req.Header.Set("User-Agent", "go-http-client-timeout-context/1.0") | |
| req.Header.Set("Accept", "application/json") | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return nil, fmt.Errorf("do request: %w", err) | |
| } | |
| defer resp.Body.Close() | |
| // Optional but recommended: limit response body size | |
| const maxBodySize = 2 << 20 // 2MB | |
| body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize)) | |
| if err != nil { | |
| return nil, fmt.Errorf("read body: %w", err) | |
| } | |
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { | |
| return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(body)) | |
| } | |
| return body, nil | |
| } | |
| func main() { | |
| client := newHTTPClient() | |
| // Context timeout (cancels the request if it exceeds this time) | |
| ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | |
| defer cancel() | |
| url := "https://httpbin.org/json" | |
| body, err := get(ctx, client, url) | |
| if err != nil { | |
| log.Fatalf("request failed: %v", err) | |
| } | |
| fmt.Println(string(body)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment