
How to Make an HTTP Server in Go
Building HTTP servers is one of the fundamental skills for any backend developer, and Go makes this incredibly straightforward with its built-in `net/http` package. Go’s standard library provides everything you need to create production-ready web servers without external dependencies, offering excellent performance, concurrent request handling, and a clean API. Whether you’re building microservices, REST APIs, or full web applications, understanding Go’s HTTP server capabilities will help you create efficient, scalable solutions.
How HTTP Servers Work in Go
Go’s HTTP server implementation is built around a few core concepts that make it both powerful and simple to use. The `net/http` package provides the `http.Server` struct and various handler functions that work together to process incoming requests.
The basic flow works like this: when a request comes in, Go’s HTTP server matches the URL path to a registered handler function using a multiplexer (mux). The handler receives an `http.ResponseWriter` for sending the response and an `*http.Request` containing all the request data. Go automatically handles the TCP connection management, HTTP protocol parsing, and concurrent request processing through goroutines.
What makes Go particularly good for HTTP servers is its built-in concurrency model. Each incoming request is handled in its own goroutine, meaning you can handle thousands of concurrent connections without the overhead of traditional threading models. The garbage collector is optimized for server workloads, and the runtime includes a highly efficient network poller.
Step-by-Step Implementation Guide
Let’s start with the simplest possible HTTP server and then build up to more complex examples.
Basic HTTP Server
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
This creates a server that listens on port 8080 and responds with “Hello, World!” to any request. The `http.HandleFunc` registers a handler for the root path, and `http.ListenAndServe` starts the server.
More Advanced Server with Multiple Routes
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, "
Welcome to Go HTTP Server
“) } func apiUsersHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(“Content-Type”, “application/json”) users := []User{ {ID: 1, Name: “Alice”}, {ID: 2, Name: “Bob”}, } json.NewEncoder(w).Encode(users) } func healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(“Content-Type”, “application/json”) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{“status”: “healthy”, “timestamp”: “%s”}`, time.Now().Format(time.RFC3339)) } func main() { mux := http.NewServeMux() mux.HandleFunc(“/”, homeHandler) mux.HandleFunc(“/api/users”, apiUsersHandler) mux.HandleFunc(“/health”, healthHandler) server := &http.Server{ Addr: “:8080”, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } fmt.Println(“Server starting on :8080”) log.Fatal(server.ListenAndServe()) }
This example shows proper server configuration with timeouts, multiple routes, JSON responses, and structured handlers.
Server with Middleware
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "API endpoint hit at %s", time.Now().Format(time.RFC3339))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", apiHandler)
// Chain middleware
handler := loggingMiddleware(corsMiddleware(mux))
server := &http.Server{
Addr: ":8080",
Handler: handler,
}
fmt.Println("Server with middleware starting on :8080")
log.Fatal(server.ListenAndServe())
}
Real-World Examples and Use Cases
REST API Server
Here’s a practical example of a REST API for managing tasks:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
}
type TaskStore struct {
mu sync.RWMutex
tasks map[int]*Task
nextID int
}
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make(map[int]*Task),
nextID: 1,
}
}
func (ts *TaskStore) Create(task *Task) {
ts.mu.Lock()
defer ts.mu.Unlock()
task.ID = ts.nextID
task.CreatedAt = time.Now()
ts.tasks[task.ID] = task
ts.nextID++
}
func (ts *TaskStore) GetAll() []*Task {
ts.mu.RLock()
defer ts.mu.RUnlock()
tasks := make([]*Task, 0, len(ts.tasks))
for _, task := range ts.tasks {
tasks = append(tasks, task)
}
return tasks
}
func (ts *TaskStore) Get(id int) (*Task, bool) {
ts.mu.RLock()
defer ts.mu.RUnlock()
task, exists := ts.tasks[id]
return task, exists
}
func (ts *TaskStore) Update(id int, updated *Task) bool {
ts.mu.Lock()
defer ts.mu.Unlock()
if _, exists := ts.tasks[id]; !exists {
return false
}
updated.ID = id
ts.tasks[id] = updated
return true
}
func (ts *TaskStore) Delete(id int) bool {
ts.mu.Lock()
defer ts.mu.Unlock()
if _, exists := ts.tasks[id]; !exists {
return false
}
delete(ts.tasks, id)
return true
}
func (ts *TaskStore) tasksHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
tasks := ts.GetAll()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
case http.MethodPost:
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
ts.Create(&task)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(task)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (ts *TaskStore) taskHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/api/tasks/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid task ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
task, exists := ts.Get(id)
if !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
case http.MethodPut:
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if !ts.Update(id, &task) {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
case http.MethodDelete:
if !ts.Delete(id) {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func main() {
store := NewTaskStore()
http.HandleFunc("/api/tasks", store.tasksHandler)
http.HandleFunc("/api/tasks/", store.taskHandler)
server := &http.Server{
Addr: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
fmt.Println("Task API server starting on :8080")
log.Fatal(server.ListenAndServe())
}
File Server with Custom Logic
package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
func customFileServer(root string) http.Handler {
fs := http.FileServer(http.Dir(root))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security: prevent directory traversal
if strings.Contains(r.URL.Path, "..") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
// Check if file exists
fullPath := filepath.Join(root, r.URL.Path)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// Add custom headers
w.Header().Set("Cache-Control", "public, max-age=3600")
// Log file access
log.Printf("Serving file: %s", r.URL.Path)
fs.ServeHTTP(w, r)
})
}
func main() {
// Create a simple uploads directory structure
os.MkdirAll("./uploads", 0755)
http.Handle("/files/", http.StripPrefix("/files/", customFileServer("./uploads")))
http.HandleFunc("/upload", uploadHandler)
fmt.Println("File server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse multipart form
err := r.ParseMultipartForm(10 << 20) // 10 MB limit
if err != nil {
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "Unable to get file", http.StatusBadRequest)
return
}
defer file.Close()
// Create destination file
dst, err := os.Create(filepath.Join("./uploads", header.Filename))
if err != nil {
http.Error(w, "Unable to create file", http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy file content
if _, err := dst.ReadFrom(file); err != nil {
http.Error(w, "Unable to save file", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "File uploaded successfully: %s", header.Filename)
}
Comparison with Alternatives
Feature | Go net/http | Node.js Express | Python Flask | Java Spring Boot |
---|---|---|---|---|
Setup Complexity | Very Low | Low | Low | Medium |
Performance | Excellent | Good | Fair | Good |
Memory Usage | Low | Medium | Medium | High |
Concurrency | Excellent (goroutines) | Good (event loop) | Limited (GIL) | Good (threads) |
Standard Library | Comprehensive | Basic | Basic | Extensive |
External Dependencies | None required | Required | Required | Many |
Binary Size | Small | N/A | N/A | Large |
Performance Benchmarks
Based on various benchmarks, here’s how Go’s HTTP server typically performs:
- Can handle 50,000+ concurrent connections on modest hardware
- Average response time under 1ms for simple endpoints
- Memory usage around 2-8 KB per goroutine vs 2MB per thread in traditional models
- CPU efficiency is excellent due to Go’s runtime scheduler
- Startup time is typically under 100ms for most applications
For high-performance hosting scenarios, consider deploying on robust infrastructure like VPS or dedicated servers to maximize Go’s performance potential.
Best Practices and Common Pitfalls
Security Best Practices
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
)
func secureServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", secureHandler)
// Security headers middleware
secureMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// Rate limiting would go here in production
next.ServeHTTP(w, r)
})
}
server := &http.Server{
Addr: ":8443",
Handler: secureMiddleware(mux),
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
},
}
fmt.Println("Secure server starting on :8443")
log.Fatal(server.ListenAndServeTLS("server.crt", "server.key"))
}
func secureHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Secure connection established!")
}
Graceful Shutdown
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func gracefulServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate some work
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "Request completed at %s", time.Now().Format(time.RFC3339))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server in a goroutine
go func() {
fmt.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("Server shutting down...")
// Create a deadline for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Attempt graceful shutdown
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
fmt.Println("Server exited")
}
func main() {
gracefulServer()
}
Common Pitfalls to Avoid
- Not setting timeouts: Always configure ReadTimeout, WriteTimeout, and IdleTimeout to prevent resource exhaustion
- Ignoring context cancellation: Use context.Context for long-running operations to handle client disconnections
- Not handling panics: Implement panic recovery middleware to prevent server crashes
- Memory leaks in handlers: Be careful with goroutines spawned in handlers; ensure they’re properly cleaned up
- Not validating input: Always validate and sanitize user input, especially in URL parameters and JSON payloads
- Blocking operations in handlers: Avoid blocking I/O operations that could hang requests
Performance Optimization Tips
package main
import (
"fmt"
"net/http"
"runtime"
"sync"
)
var (
// Connection pooling for database connections
responsePool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
)
func optimizedHandler(w http.ResponseWriter, r *http.Request) {
// Reuse buffers
buf := responsePool.Get().([]byte)
defer responsePool.Put(buf)
// Efficient response writing
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Optimized response")
}
func main() {
// Set GOMAXPROCS to match available CPUs
runtime.GOMAXPROCS(runtime.NumCPU())
http.HandleFunc("/", optimizedHandler)
server := &http.Server{
Addr: ":8080",
// Tune these based on your needs
MaxHeaderBytes: 1 << 20, // 1 MB
}
fmt.Println("Optimized server starting on :8080")
server.ListenAndServe()
}
Monitoring and Debugging
package main
import (
"expvar"
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"time"
)
var (
requests = expvar.NewInt("requests")
errors = expvar.NewInt("errors")
)
func monitoringMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
requests.Add(1)
defer func() {
if err := recover(); err != nil {
errors.Add(1)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
// Log slow requests
duration := time.Since(start)
if duration > 100*time.Millisecond {
fmt.Printf("Slow request: %s %s took %v\n", r.Method, r.URL.Path, duration)
}
})
}
func main() {
// Expose runtime metrics
expvar.Publish("goroutines", expvar.Func(func() interface{} {
return runtime.NumGoroutine()
}))
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
// Enable pprof endpoint for debugging
mux.Handle("/debug/", http.DefaultServeMux)
handler := monitoringMiddleware(mux)
fmt.Println("Server with monitoring starting on :8080")
fmt.Println("Metrics available at /debug/vars")
fmt.Println("Profiling available at /debug/pprof/")
http.ListenAndServe(":8080", handler)
}
Go’s built-in HTTP server capabilities make it an excellent choice for building everything from simple APIs to complex web applications. The standard library provides robust, production-ready functionality without requiring external frameworks, while still offering the flexibility to add additional layers when needed. The combination of excellent performance, built-in concurrency, and straightforward APIs makes Go HTTP servers particularly well-suited for microservices, API gateways, and high-throughput web applications.
For comprehensive documentation and advanced usage patterns, check out the official Go net/http package documentation and the Go web application tutorial.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.