Recommended Free Tools
The smallest useful Go HTTP server has three parts: a handler that writes a response, a ServeMux that chooses a handler for each request, and a server that accepts connections. Start with an explicit mux and the standard library, then add request limits, workload-appropriate timeouts, graceful shutdown, and boundary tests before exposing the service to real traffic.
The mental model: handler, mux, and server
A handler implements ServeHTTP(http.ResponseWriter, *http.Request). It reads the request and writes status, headers, and a response body. A function with the same inputs can be adapted with http.HandlerFunc, which is why small endpoints are often declared as functions.
A mux (multiplexer) matches the incoming method and path to a handler. http.NewServeMux() creates an explicit router. Passing that mux to a server keeps route registration visible instead of relying on package-global state. The server accepts connections, applies its read/write policies, and invokes the mux.
A minimal, runnable server
This example targets Go 1.22 or newer and listens on a local development port.
#1 Best Overall
package main
import (
"fmt"
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello from Go")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
log.Println("listening on http://localhost:8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
Run it with go run ., then request http://localhost:8080/. ListenAndServe blocks while the server is running. It returns a non-nil error when serving stops; in this simple program, logging an unexpected return is appropriate.
Default mux versus an explicit mux
If you pass nil as the handler, ListenAndServe uses the package-level http.DefaultServeMux. Code can register routes with http.HandleFunc, but global registration makes dependencies harder to see and can cause conflicts in tests or larger programs. An explicit mux, as above, gives each server its own route table.
Plain HTTP versus HTTPS
For local development, plain HTTP is convenient. For an externally exposed service, configure TLS with ListenAndServeTLS or the corresponding http.Server method and provide certificate and private-key material. The standard library does not automatically obtain certificates for you.
Use an http.Server when you need control
The convenience function hides important operational settings. A configured server lets you select timeout and header policies and gives you a lifecycle object for shutdown.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MiB
}
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
Add errors and time to the imports. The 10-second read/write and 1 MiB header values shown in Go’s package documentation are illustrative settings, not universal defaults. Choose values from your request sizes, client behavior, proxy limits, and latency objectives.
What each timeout covers
ReadHeaderTimeout: maximum time to read request headers. It is useful against clients that connect and then send headers very slowly.ReadTimeout: maximum time to read the entire request, including its body. A short value can reject legitimate slow uploads.WriteTimeout: maximum time spent writing a response. Streaming endpoints may need a different design or policy.IdleTimeout: time to wait for the next request on an existing keep-alive connection.
For timeout fields, zero or a negative value has documented no-timeout semantics. “More aggressive” is not automatically safer: a value that protects one API can break uploads, long polls, or slow but valid clients. Measure or reason about each route and set policies accordingly.
Header bytes are not body bytes
MaxHeaderBytes limits request headers and the request line. It does not limit a JSON document, multipart upload, or other request body. Body limits must be enforced while the handler reads the body.
Limit request bodies per route
Use http.MaxBytesReader before decoding or copying an incoming body. The handler chooses the limit because an avatar upload and a small JSON command have different requirements.
func createUser(w http.ResponseWriter, r *http.Request) {
const maxBody = 1 << 20 // 1 MiB
r.Body = http.MaxBytesReader(w, r.Body, maxBody)
defer r.Body.Close()
var input struct {
Name string `json:"name"`
}
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&input); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"name":%q}`+"n", input.Name)
}
Import encoding/json along with errors and net/http. When the limit is exceeded, reads return an *http.MaxBytesError. Handle that case separately so clients receive a useful 413 response rather than an opaque decoding error. Consider rejecting trailing JSON values, validating fields, and applying stricter limits to especially expensive operations.
Routing and the Go 1.22 change
ServeMux pattern syntax and matching changed significantly in Go 1.22. Method-qualified patterns and wildcard segments behave according to the version of the standard library you compile with. For example, patterns that use newer method or wildcard syntax should be checked against the Go 1.22 package documentation before deployment.
Escaped path segments and invalid patterns also have compatibility implications. If you are migrating an older application and need the pre-1.22 behavior, the documented compatibility switch is GODEBUG=httpmuxgo121=1, read at process startup. Treat that as a migration aid, not a reason to mix old and new assumptions indefinitely. State your target Go version in examples, CI, and deployment images.
Graceful shutdown that actually waits
Closing a process immediately can truncate responses and interrupt in-flight work. Handle an interrupt or termination signal, call Shutdown with a bounded context, and wait for it to return.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func run() error {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
serverErr := make(chan error, 1)
go func() {
serverErr <- srv.ListenAndServe()
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(stop)
select {
case err := <-serverErr:
if !errors.Is(err, http.ErrServerClosed) {
return err
}
case <-stop:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return err
}
// Shutdown has completed; the serving goroutine can now finish.
if err := <-serverErr; !errors.Is(err, http.ErrServerClosed) {
return err
}
}
return nil
}
In a complete program, call log.Fatal(run()) from main. Shutdown closes listeners and idle connections, then waits for active connections to become idle until the context deadline. Once shutdown begins, ListenAndServe returns http.ErrServerClosed; that is the expected path, not a startup failure.
Upgraded and hijacked connections
Shutdown does not close or wait for hijacked connections, such as WebSockets. Track those connections separately and signal their handlers to stop. A shutdown deadline should be long enough for normal requests but finite enough for the process supervisor to recover a stuck instance.
Test at the HTTP boundary
The net/http/httptest package lets you test handlers and complete request/response behavior without binding your production port.
Rank #4
func TestHome(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
req := httptest.NewRequest(http.MethodGet, "http://example.test/", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
if got := rec.Body.String(); got != "hello from Gon" {
t.Fatalf("body = %q", got)
}
}
For more realistic tests, httptest.NewServer(mux) starts an in-process HTTP server and returns a client-facing URL; close it with defer ts.Close(). Assert status, headers, body, redirects, and error responses. Configure test-server behavior before first use, because changing its configuration after requests begin is unsafe or ineffective.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsProduction checklist
- Use an explicit mux and document the Go version whose routing rules you rely on.
- Set header, read, write, and idle policies based on actual endpoint behavior.
- Apply
MaxBytesReaderto every route that accepts an untrusted body. - Validate content type, decoded fields, authentication, and authorization separately from size checks.
- Return
http.ErrServerClosedas an expected shutdown result and wait forShutdown. - Coordinate WebSocket or other hijacked connections outside
Shutdown. - Test handlers with
httptest, including oversized bodies and slow-client timeout behavior. - Use TLS and a certificate strategy appropriate for the environment rather than assuming the standard library provisions one.
Common failures and fixes
“Address already in use”
Another process owns the port. Stop it, choose another port such as :8081, or configure the listener explicitly. This is a bind-time error, not a handler problem.
The server exits immediately with no useful response
Check the error returned by ListenAndServe. A startup error should be logged. During intentional shutdown, compare it with http.ErrServerClosed and do not report that expected value as a crash.
Large uploads fail unexpectedly
Inspect both policies: MaxHeaderBytes does not limit the body, while MaxBytesReader does. Also check ReadTimeout; it includes body reading and can be too short for slow clients.
Legitimate clients see timeouts
Identify which phase timed out. Increase only the relevant setting, or give upload/streaming endpoints a different architecture and policy. Do not copy documentation example values without considering payload size and latency.
Free tools Windows power users keep installed
One-click scans. No signup required.
Best Value
A route changes behavior after upgrading Go
Review ServeMux 1.22 pattern and escaping rules, run route tests under the target toolchain, and use GODEBUG=httpmuxgo121=1 only as a deliberate compatibility step during migration.
Or skip the browser setup
If your Go service also needs website screenshots for previews, reports, or automated checks, ScreenshotNeo provides a single HTTP request instead of maintaining browser setup. It accepts cookie and consent banners like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and lets you turn those steps off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.
Use the API from Go or any HTTP client. The complete endpoint and options are documented at ScreenshotNeo’s API documentation.
package main
import (
"os"
"io"
"log"
"net/http"
"net/url"
)
func main() {
q := url.Values{}
q.Set("access_key", os.Getenv("SCREENSHOTNEO_API_KEY"))
q.Set("url", "https://stripe.com")
res, err := http.Get("https://api.screenshotneo.com/v1/shot?" + q.Encode())
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
log.Fatalf("ScreenshotNeo returned %s", res.Status)
}
f, err := os.Create("shot.webp")
if err != nil {
log.Fatal(err)
}
defer f.Close()
if _, err := io.Copy(f, res.Body); err != nil {
log.Fatal(err)
}
}
ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets or custom viewports, retina scale, PNG/JPEG/WebP, PDF paper and page options, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, ad/tracker/request blocking, custom headers/cookies/user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed public image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, an OpenAPI specification, and compatible parameter names used by other screenshot APIs.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing provides two months free, and every feature is on every plan. Sign up free for 1,000 screenshots a month with no card.
Frequently Asked Questions
Should a small Go service use ListenAndServe or http.Server?
Use ListenAndServe for a genuinely local or disposable program. Use http.Server when you need explicit timeouts, header limits, TLS configuration, or graceful shutdown.
Does Shutdown stop WebSocket connections?
No. Hijacked connections are outside Shutdown’s close-and-wait behavior and require their own coordination.
What Go version do these routing examples require?
The article’s examples target Go 1.22 or newer, whose ServeMux patterns and matching differ significantly from earlier releases.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




