package api
import (
"encoding/json"
"encoding/xml"
"net/http"
"strings"
)
const (
mimeJSON = "application/json"
mimeXML = "application/xml"
)
func bestMatch(accept string) string {
for _, part := range strings.Split(accept, ",") {
media := strings.TrimSpace(part)
if i := strings.IndexByte(media, ';'); i >= 0 {
media = strings.TrimSpace(media[:i])
}
switch media {
case mimeXML, "text/xml":
return mimeXML
case mimeJSON:
return mimeJSON
case "*/*", "":
return mimeJSON
}
}
return mimeJSON
}
func Respond(w http.ResponseWriter, r *http.Request, status int, payload interface{}) error {
media := bestMatch(r.Header.Get("Accept"))
w.Header().Set("Content-Type", media+"; charset=utf-8")
w.WriteHeader(status)
if media == mimeXML {
return xml.NewEncoder(w).Encode(payload)
}
return json.NewEncoder(w).Encode(payload)
}
package api
import (
"encoding/xml"
"net/http"
)
type Book struct {
XMLName xml.Name `json:"-" xml:"book"`
ID int `json:"id" xml:"id"`
Title string `json:"title" xml:"title"`
Author string `json:"author" xml:"author"`
}
type apiError struct {
XMLName xml.Name `json:"-" xml:"error"`
Message string `json:"message" xml:"message"`
Code int `json:"code" xml:"code"`
}
func BookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
renderError(w, r, http.StatusMethodNotAllowed, "only GET is supported")
return
}
book := Book{ID: 42, Title: "The Go Programming Language", Author: "Donovan & Kernighan"}
if err := Respond(w, r, http.StatusOK, book); err != nil {
renderError(w, r, http.StatusInternalServerError, "failed to encode response")
}
}
func renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
_ = Respond(w, r, status, apiError{Message: msg, Code: status})
}
This snippet shows how to build an HTTP handler in Go that inspects the client's Accept header and serializes the same response payload as either JSON or XML. Content negotiation is a core part of HTTP: the server exposes one resource, and the representation is chosen at request time based on what the client asks for. Rather than duplicating handler logic per format, the negotiation is factored into a small reusable helper so every endpoint stays format-agnostic.
In negotiate.go, the exported Respond function is the entry point. It parses the Accept header with bestMatch, which splits the header on commas, strips any ;q= quality parameters, and returns the first media type the server actually supports. The lookup is deliberately simple and preference-ordered: an explicit match on application/xml or text/xml picks XML, application/json picks JSON, and a wildcard */* or empty header falls back to a sensible default (application/json). This mirrors how most REST APIs behave and avoids the complexity of full RFC 7231 quality-value sorting while still handling the common cases correctly.
Once a media type is chosen, Respond sets the Content-Type header, writes the status code with WriteHeader, and encodes using either json.NewEncoder or xml.NewEncoder streamed directly to the http.ResponseWriter. Streaming the encoder avoids building an intermediate buffer. Note the ordering pitfall it sidesteps: headers and status must be written before any body bytes, because the first Write implicitly flushes a 200.
In handler.go, BookHandler demonstrates the payoff. The Book struct carries both json and xml struct tags, plus an XMLName field so XML output gets a proper root element. The handler builds its data once and calls Respond, never branching on format itself. renderError shows the same helper serializing an error envelope, so error responses honor negotiation too.
The trade-off is that this hand-rolled matcher ignores q weighting and less common media types; for richer needs a library handles the full grammar. But for a service that speaks JSON and XML, this keeps handlers clean, testable, and free of per-format duplication.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
package api
import (
"io"
"net/http"
"os"
Safe multipart uploads using temp files (bounded memory)
Share this code
Here's the card — post it anywhere.