How to Use Go’s net/http Package
Learn Go’s net/http for HTTP clients, servers, handlers, timeouts, redirects, testing, and production-safe connection reuse.
Direct answer: Go’s net/http package covers both sides of HTTP. Client code builds requests and reads responses with http.Client; server code receives requests through an http.Handler and writes responses with http.ResponseWriter. For a quick GET, use http.Get. For production code, create a request with a context, send it through a reusable http.Client, check the status code, read the body, and close it. On the server, register handlers on a mux and run a configured http.Server.
The package documentation describes net/http as providing HTTP client and server implementations. See the official package documentation and the package source documentation for version-specific details.
1. Make an HTTP request in Go
Use http.Get when you need a simple GET without custom headers, a request body, or cancellation:
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Fatalf("unexpected status: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
A non-2xx response is not a Client.Do error. Always inspect resp.StatusCode separately from the returned error. Close every response body when finished so persistent connections can be reused.
Requests with context, headers, and a body
package main
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://example.com/api/items",
strings.NewReader(`{"name":"gopher"}`),
)
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("HTTP %s: %s", resp.Status, data))
}
fmt.Println(string(data))
}
A request context controls connection acquisition, transmission, response-header waiting, and response-body reading. Cancel it when the operation that started the request is canceled.
Reuse clients and transports
http.Client and http.Transport are safe for concurrent use. Keep reusable instances instead of constructing one for every request:
var transport = &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
}
var client = &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
}
The client owns higher-level policy such as redirects and cookies. The transport controls lower-level networking, proxies, TLS, keep-alives, compression, and connection reuse. Call transport.CloseIdleConnections() when your application deliberately needs to release idle connections.
2. Build an HTTP server with handlers
An http.Handler receives a ResponseWriter and a Request. The following complete program uses an explicit mux and server configuration:
package main
import (
"fmt"
"html"
"log"
"net/http"
"time"
)
func home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "hello from %s\\n", html.EscapeString(r.URL.Path))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Printf("listening on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
Run it with go run ., then request http://localhost:8080/. The official Writing Web Applications tutorial shows the minimal HandleFunc and ListenAndServe pattern; an explicit http.Server makes timeout and header limits visible for real services.
Routing and untrusted input
Request paths, query values, headers, and bodies are untrusted. Validate methods and required fields, cap body sizes with http.MaxBytesReader, and escape data before inserting it into HTML. Validate r.Host when your service should answer only for known hostnames; the package documentation warns handlers to check that the host is authoritative for the application.
3. Redirects, cookies, TLS, and HTTP/2
The default client follows redirects. Set CheckRedirect when redirects should be rejected, limited, or logged:
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) > 5 {
return fmt.Errorf("too many redirects")
}
return nil
},
}
When sensitive headers are involved, treat cross-domain redirects as a trust decision. Go’s security guidance describes stripping sensitive headers on cross-domain redirects as defense in depth; do not rely on redirect defaults instead of validating destinations.
Use a CookieJar when the client needs cookie persistence. Configure TLS through the transport only when you understand the security impact; avoid disabling certificate verification in production. Default transports and servers automatically enable HTTP/2 over HTTPS in documented cases. A custom transport may require explicit protocol configuration, so check the documentation for the Go version you support.
4. Request and response edge cases
- Successful transport, failed application:
err == nilmeans the exchange completed, not that the server accepted the request. Check the status code. - Large responses: do not read unbounded data from an untrusted endpoint. Wrap the body with
io.LimitReaderor decode with an explicit size policy. - Empty bodies: a 204 response normally has no body. Handle that before JSON decoding.
- Retries: retry only operations that are safe or explicitly idempotent. Use capped backoff and stop on context cancellation.
- Streaming: process the body incrementally instead of calling
io.ReadAllwhen responses can be large. - Connection reuse: always close bodies. A body can be closed after a bounded read when you intentionally abandon a response.
- Server shutdown: use
Server.Shutdown(ctx)during deployment so active requests can finish within a deadline.
5. Testing with net/http/httptest
net/http/httptest lets you test handlers without opening a real listening socket. The package provides request and recorder helpers, plus test servers. See the httptest documentation.
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHome(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.test/", nil)
rec := httptest.NewRecorder()
home(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("got status %d", rec.Code)
}
if got := rec.Body.String(); got == "" {
t.Fatal("expected response body")
}
}
Use a test server when client code must exercise redirects, cookies, headers, or real request serialization. Keep external services out of unit tests unless the test is intentionally an integration test.
6. Performance, reliability, and cost considerations
- Performance: reuse clients and transports, keep connections alive, stream large bodies, and avoid unnecessary redirects or retries.
- Reliability: set client deadlines and server read/write/idle timeouts. Propagate contexts through every outbound call.
- Resource limits: bound request headers and body sizes, and cap response reads from untrusted services.
- Protocol behavior: default HTTPS components support HTTP/2 as documented; custom transports can change that behavior.
- Cost:
net/httpis part of Go’s standard library. Your direct costs come from the services, bandwidth, compute, and storage around the requests; the package itself has no separate usage fee.
7. Troubleshooting common net/http errors
| Symptom | Likely cause | Fix |
|---|---|---|
context deadline exceeded |
The deadline expired during connection, transfer, or body reading. | Inspect which phase is slow, set a realistic deadline, and cancel work that no longer matters. |
connection refused |
No process is listening at the address or the port is blocked. | Verify Addr, process health, container networking, and firewall rules. |
| Unexpected 401/403/404/500 | The HTTP exchange succeeded but the application rejected or failed the request. | Check StatusCode, response headers, body, URL, authentication, and method. |
| Too many open connections | Response bodies are not closed or clients are created repeatedly. | Defer resp.Body.Close() after a successful request and reuse clients/transports. |
| Redirect sent credentials unexpectedly | Sensitive headers were used across a redirect boundary. | Restrict redirects and validate destination hosts before sending credentials. |
| Server hangs on slow clients | Read or write timeouts are absent or too generous. | Configure ReadTimeout, WriteTimeout, and IdleTimeout for the workload. |
| HTML contains broken or unsafe user data | Unescaped request values were written into HTML. | Validate input and use context-appropriate escaping such as html.EscapeString. |
8. Or skip the browser setup
If your Go service needs screenshots rather than general HTTP responses, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing result.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Go:
package main
import (
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "https://api.screenshotneo.com/v1/shot?access_key=YOUR_API_KEY&url=https%3A%2F%2Fstripe.com", nil)
if err != nil { panic(err) }
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(resp.Status) }
out, err := os.Create("shot.webp")
if err != nil { panic(err) }
defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil { panic(err) }
}
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for all options, including full-page and element captures, dark mode, device presets, custom viewports, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone, geolocation, transparency, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, usage, and the OpenAPI specification. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan. Create a free ScreenshotNeo account.
9. FAQ
Should I use http.Get or http.Client?
Use http.Get for a small, simple GET. Use a reusable configured client when you need context, headers, methods, bodies, redirects, cookies, or transport settings.
Does a 404 make Client.Do return an error?
No. A completed HTTP exchange with a 404 normally returns a response and a nil error. Check the status code yourself.
Why must I close resp.Body?
The caller owns the response body. Closing it releases resources and allows connection reuse where possible.
When should I configure an explicit http.Server?
Use one for production services so address, handler, read/write/idle timeouts, and header limits are visible and reviewable.
How do I test a handler without starting the application?
Use httptest.NewRequest and httptest.NewRecorder, or create an httptest.Server for client-facing integration behavior.


