Most Go applications consume external APIs. When you ask AI to write an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. client, it generates code that works against a happy-path test server but fails in production: no timeouts, no retry logic, unchecked status codes, leaked connections. These are the bugs you'll catch by reading the code AI gives you.
What AI generates vs what you need
AI gives you this:
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))Four problems:
- No timeout, hangs forever if server doesn't respond
- No
defer resp.Body.Close(), leaks connections - Ignores
io.ReadAllerror - Doesn't check
resp.StatusCode
What production code looks like:
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.example.com/users")
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return fmt.Errorf("decode failed: %w", err)
}http.Get() which uses http.DefaultClient, a global client with no timeout. A DNS lookup that hangs, a server that accepts connections but never responds, or a response that streams forever will block your goroutine indefinitely. Always create your own http.Client with a timeout.Production client configuration
| Setting | Default | What to set | Why |
|---|---|---|---|
Timeout | None | 10-30s | Total request deadline |
MaxIdleConns | 100 | 100+ | Connection pool size |
MaxIdleConnsPerHost | 2 | 10+ | Prevents connection thrashing to one host |
IdleConnTimeout | 90s | 90s | Clean up stale connections |
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}http.Client inside every function call. This prevents connection reuse, each request opens a new TCP connection and TLS handshake. Create the client once (usually at struct initialization) and reuse it across requests.Full-control requests with NewRequest
For POST requests, custom headers, or authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.:
data := map[string]string{"name": "Alice", "email": "alice@example.com"}
jsonData, _ := json.Marshal(data)
req, err := http.NewRequestWithContext(
ctx,
"POST",
"https://api.example.com/users",
bytes.NewReader(jsonData),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()Request body types
| Format | Content-Type | How to build body |
|---|---|---|
| JSON | application/json | json.Marshal() + bytes.NewReader() |
| Form | application/x-www-form-urlencoded | url.Values{}.Encode() + strings.NewReader() |
| Multipart | multipart/form-data | multipart.NewWriter() |
Context for timeouts and cancellation
context is how Go manages request lifecycle. When you ask AI to make HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. requests from within an HTTP handler, context propagation is critical.
func getUserFromAPI(ctx context.Context, userID string) (*User, error) {
// This request will be cancelled if the parent HTTP request is cancelled
req, err := http.NewRequestWithContext(ctx, "GET",
"https://api.example.com/users/"+userID, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("request timed out")
}
return nil, err
}
defer resp.Body.Close()
var user User
return &user, json.NewDecoder(resp.Body).Decode(&user)
}http.NewRequest instead of http.NewRequestWithContext. Without a context, you can't cancel in-flight requests when the parent handler times out. In an HTTP handler, always pass r.Context() to outgoing requests so they're cancelled if the client disconnects.Building a reusable APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. client
When you ask AI to build an API client, evaluate it against this pattern:
type APIClient struct {
baseURL string
httpClient *http.Client
apiKey string
}
func NewAPIClient(baseURL, apiKey string) *APIClient {
return &APIClient{
baseURL: baseURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *APIClient) do(ctx context.Context, method, path string, body, result any) error {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return fmt.Errorf("new request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
return fmt.Errorf("API error %d: %s", resp.StatusCode, errBody)
}
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
}
return nil
}Checklist for AI-generated HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. clients
| Check | What goes wrong without it |
|---|---|
| Client has timeout | Hangs forever on unresponsive servers |
| Client is reused, not created per-request | Connection pool wasted |
| Context passed to every request | Can't cancel on parent timeout |
| Response body always closed | Connection leak, eventual exhaustion |
| Status code checked before decoding | Tries to JSON-decode HTML error pages |
| Error bodies read with size limit | Malicious server sends infinite error response |