BLOG POSTS
How to Make an HTTP Server in Go

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.

Leave a reply

Your email address will not be published. Required fields are marked