Go: Go HTTP Services

Last updated: 2026-08-26

The net/http package in the Go standard library is fully featured, allowing you to build production-grade web services without the need for third-party frameworks.

When your team decides to "use only the standard library without introducing any web frameworks," can you write clear routes and وسيط just like Gin or Echo? In this lesson, you'll master all the core technologies of Go HTTP services.

1. You will learn



2. A True Story of a Backend Engineer

(1) Pain Point: A simple API—we chose Gin, but three months later, we hit a roadblock during the upgrade

Alice is a member of the واجهة خلفية team, and she needs to set up a REST API for user management:

"I wrote three routes using the Gin إطار عمل: GET /users, POST /users, and GET /users/:id. But three months later, Go 1.22 was released, and the standard library added native support for طريقة and path parameters. Now I want to remove the Gin dependency, but I'd have to change all the handler signatures—gin.Context vs. http.ResponseWriter. My boss said, 'It's not worth refactoring hundreds of lines of code just to eliminate one dependency.'"

Her decision at the time:

GO
// Gin dependency version (wanted to migrate three months later)
r := gin.Default()
r.GET("/users", listUsers)               // gin.Context
r.POST("/users", createUser)             // gin.Context
r.GET("/users/:id", getUser)             // gin.Context
// Want to migrate to the standard library? All handler signatures must change!

(2) Solution for Go 1.22: Native routing in the standard library

GO
// Standard library version (Go 1.22+, no dependencies required)
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name سلسلة `json:"name"`
}

var users = []User{{ID: 1, Name: "Alice"}}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 enhanced routing: طريقة + path pattern + path parameters
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("GET /users/{id}", getUser)

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// Standard handler signature: http.ResponseWriter + *http.Request
func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Path parameter!
    // Look up user...
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}

(3) Performance: Gin Style vs. Standard Library (Go 1.22)

Feature Gin (third-party) Standard Library (Go < 1.22) Standard Library (Go 1.22+)
Path Parameter :id ❌ Must be parsed manually {id}
Method Routing ❌ Check within Handler "GET /path"
JSON Response c.JSON() Manually Set Header Manually Set Header
Dependencies 1 external package 0 0
Performance Slightly slower (reflection) Native Native
💡 Tip: The enhanced routing in Go 1.22's net/http is sufficient for most web projects. If you don't need framework-specific features (such as automatic binding/validation or a rich middleware ecosystem), give priority to the standard library.



3. HTTP Basics

▶ Example: The Simplest HTTP Service

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ Try it Yourself

Test:

BASH
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!

(2) Core Types

Type Description
http.ResponseWriter Interface for writing HTTP responses
*http.Request HTTP request, including URL, headers, body, and form
http.Handler Interface: ServeHTTP(w, r)
http.HandlerFunc Function adapter: Converts a regular function into a Handler
http.ServeMux Route multiplexer

(3) A Detailed Explanation of the Handler Interface

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

// Method 1: Implement the Handler interface
type Greeter struct {
    Greeting string
}

func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}

// Method 2: HandlerFunc adapter
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    mux := http.NewServeMux()

    // Struct Handler
    mux.Handle("/greet", &Greeter{Greeting: "Welcome"})

    // Function Handler (HandlerFunc automatic conversion)
    mux.HandleFunc("/hello", helloHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}


4. Go 1.22: Enhanced Routing

▶ Example: Method + Path Pattern + Path Parameter

GO 📖 Display only
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type Item struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

var items = []Item{
    {ID: 1, Name: "Laptop", Price: 999.99},
    {ID: 2, Name: "Mouse", Price: 29.99},
}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 method + path pattern
    mux.HandleFunc("GET /items", listItems)
    mux.HandleFunc("POST /items", createItem)
    mux.HandleFunc("GET /items/{id}", getItem)
    mux.HandleFunc("PUT /items/{id}", updateItem)
    mux.HandleFunc("DELETE /items/{id}", deleteItem)

    // Wildcard suffix: path prefix matching
    mux.HandleFunc("GET /items/{path...}", wildcardHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}

func listItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    var item Item
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    item.ID = len(items) + 1
    items = append(items, item)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(item)
}

func getItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Look up item...
    _ = id
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items[0])
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // Update logic...
    w.WriteHeader(http.StatusNoContent)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // Delete logic...
    w.WriteHeader(http.StatusNoContent)
}

func wildcardHandler(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    w.Header().Set("Content-Type", "text/plain")
    http.Error(w, "Not found: "+path, http.StatusNotFound)
}
61 logic lines (exceeds 40-line limit, display only)

(2) Comparison of Routing Modes in Go 1.22

Mode Go < 1.22 Go 1.22+ Example
Method Matching Not supported (if statement within Handler) Supported "GET /items"
Path Parameter Not Supported {name} Syntax "GET /items/{id}"
Wildcard suffix Not supported {path...} "GET /static/{file...}"
Exact path /items "GET /items" Exact match for /items
Prefix Match /items/ "GET /items/" Matches /items/...

▶ Example: Route Priority

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Exact path > prefix path
    mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "items list")
    })
    mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
    })
    mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "featured items")
    })

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Try it Yourself

Test:

BASH
$ curl localhost:8080/items
items list
$ curl localhost:8080/items/42
item 42
$ curl localhost:8080/items/featured
featured items    # Exact match takes priority over {id} wildcard
🔥 Common Mistake: The order in which routes are registered does not matter—route matching in Go 1.22 is based on priority rules (exact > prefix > wildcard), not the order of registration. Higher-priority routes will override lower-priority matches, even if the latter were registered first.



5. Requests and Responses

▶ Example: Query Parameters, Forms, JSON

GO 📖 Display only
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Response struct {
    Method string      `json:"method"`
    Path   string      `json:"path"`
    Query  interface{} `json:"query,omitempty"`
    Form   interface{} `json:"form,omitempty"`
    JSON   interface{} `json:"json,omitempty"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    resp := Response{
        Method: r.Method,
        Path:   r.URL.Path,
    }

    // Query parameters
    if r.Method == http.MethodGet {
        resp.Query = r.URL.Query()
    }

    // Form data
    if r.Method == http.MethodPost {
        contentType := r.Header.Get("Content-Type")
        switch {
        case contentType == "application/x-www-form-urlencoded":
            r.ParseForm()
            resp.Form = r.Form
        case contentType == "application/json":
            var body interface{}
            json.NewDecoder(r.Body).Decode(&body)
            resp.JSON = body
        }
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)

    log.Print(http.ListenAndServe(":8080", mux))
}
42 logic lines (exceeds 40-line limit, display only)

▶ Example: JSON Response Utility Function

GO
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

// JSON response utility function
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func getProduct(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Simulate lookup
    if id != "1" {
        writeError(w, http.StatusNotFound, "product not found")
        return
    }
    writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /products/{id}", getProduct)

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Try it Yourself
100%
sequenceDiagram
    participant Client as HTTP Client
    participant Mux as ServeMux
    participant Handler as Handler
    
    Client->>Mux: GET /products/1
    Mux->>Mux: Route matching
    Mux->>Handler: ServeHTTP(w, r)
    Handler->>Handler: r.PathValue("id") → "1"
    Handler->>Handler: writeJSON(w, 200, product)
    Handler-->>Client: HTTP 200 + JSON body

(3) Quick Reference for HTTP Status Codes

Code Constant Purpose
200 http.StatusOK Success
201 http.StatusCreated Resource created successfully
204 http.StatusNoContent Success, but no response body
400 http.StatusBadRequest Client request error
401 http.StatusUnauthorized Unauthorized
403 http.StatusForbidden No permission
404 http.StatusNotFound Resource not found
500 http.StatusInternalServerError Internal server error


6. Static File Service

▶ Example: Static Files

GO
package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Static file service: /static/ prefix → ./static/ directory
    mux.Handle("GET /static/", http.StripPrefix("/static/",
        http.FileServer(http.Dir("./static"))))

    // Single file
    mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Try it Yourself

7. Complete Example: REST-Style Notes API

GO
// notes_api.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

type Note struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// ---------- Store ----------

type NoteStore struct {
    mu     sync.RWMutex
    notes  map[int]Note
    nextID int
}

func NewNoteStore() *NoteStore {
    return &NoteStore{
        notes:  make(map[int]Note),
        nextID: 1,
    }
}

func (s *NoteStore) Create(title, content string) Note {
    s.mu.Lock()
    defer s.mu.Unlock()
    n := Note{
        ID:        s.nextID,
        Title:     title,
        Content:   content,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    s.nextID++
    s.notes[n.ID] = n
    return n
}

func (s *NoteStore) List() []Note {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Note, 0, len(s.notes))
    for _, n := range s.notes {
        result = append(result, n)
    }
    return result
}

func (s *NoteStore) Get(id int) (Note, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    n, ok := s.notes[id]
    return n, ok
}

func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    n, ok := s.notes[id]
    if !ok {
        return Note{}, false
    }
    n.Title = title
    n.Content = content
    n.UpdatedAt = time.Now()
    s.notes[id] = n
    return n, true
}

func (s *NoteStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.notes[id]
    if !ok {
        return false
    }
    delete(s.notes, id)
    return true
}

// ---------- API ----------

type NotesAPI struct {
    store *NoteStore
}

func NewNotesAPI(store *NoteStore) *NotesAPI {
    return &NotesAPI{store: store}
}

func (api *NotesAPI) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /notes", api.ListNotes)
    mux.HandleFunc("POST /notes", api.CreateNote)
    mux.HandleFunc("GET /notes/{id}", api.GetNote)
    mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
    mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}

// Utility functions
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

// ---------- Handlers ----------

func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
    notes := api.store.List()
    writeJSON(w, http.StatusOK, notes)
}

func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }
    if input.Title == "" {
        writeError(w, http.StatusBadRequest, "title is required")
        return
    }

    note := api.store.Create(input.Title, input.Content)
    writeJSON(w, http.StatusCreated, note)
}

func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    note, ok := api.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    note, ok := api.store.Update(id, input.Title, input.Content)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    if !api.store.Delete(id) {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewNoteStore()
    api := NewNotesAPI(store)

    mux := http.NewServeMux()
    api.Register(mux)

    log.Println("Note API started on :8080")
    log.Println("Available endpoints:")
    log.Println("  GET    /notes       — List all notes")
    log.Println("  POST   /notes       — Create a note")
    log.Println("  GET    /notes/{id}  — Get a single note")
    log.Println("  PUT    /notes/{id}  — Update a note")
    log.Println("  DELETE /notes/{id}  — Delete a note")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
🔥 Common Mistake: http.Error(w, msg, code) does not set the Content-Type to JSON. If you're returning a JSON خطأ, use json.NewEncoder(w).Encode(errResp) and manually set the Header(). The standard library's http.Error() returns plain text.


❓ FAQ

Q How do the routing enhancements in Go 1.22 compare to Gin?
A They're sufficient for most scenarios. Go 1.22 supports طريقة matching, path parameters, and wildcards. Gin's added value lies in طلب binding/validation, its وسيط ecosystem, and خطأ handling. If your project doesn't require these features, the standard library is lighter and more secure (zero dependencies).
Q What is the difference between Handler and HandlerFunc?
A Handler is an interface (that requires implementing the ServeHTTP طريقة), while HandlerFunc is a دالة type adapter—it allows ordinary functions to automatically satisfy the Handler interface. The two are completely equivalent: mux.Handle("/path", handler) and mux.HandleFunc("/path", handlerFunc) have the same effect.
Q How do I retrieve path parameters?
A In Go 1.22 and later, use r.PathValue("name") to retrieve path parameters of type {name}. In older versions, you need to parse them manually from r.URL.Path or use a third-party library. The path parameter names must match the {name} in the route pattern.
Q What is the difference between ListenAndServe and ListenAndServeTLS?
A The former uses HTTP (port 80), while the latter uses HTTPS (port 443) and requires a certificate and private key file. HTTPS example: http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux).
Q How do I gracefully shut down an HTTP خادم?
A Use خادم.Shutdown(ctx) in conjunction with os/signal to catch termination signals. Shutdown will wait for all active connections to be processed before closing. Do not use خادم.Close()—it will forcefully terminate requests currently being processed.
Q What does it mean to pass nil as the second argument to http.ListenAndServe?
A Passing nil indicates that http.DefaultServeMux (the global default router) is used. It is recommended to explicitly create http.NewServeMux() to avoid polluting the global routing—especially when isolating routes during testing.
Q Does ServeMux support nested subroutes?
A The standard library's ServeMux does not support nested routes (subroute grouping). You can achieve a similar effect by manually registering routes with a common prefix, or by using a third-party routing library such as chi (which is extremely lightweight) to implement nesting.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Create a simple HTTP service and register three routes: GET /time returns the current time as JSON, GET /health returns {"حالة": "ok"}, and GET /version returns the version number. Use the enhanced routing syntax in Go 1.22.

  2. Advanced Problem (Difficulty ⭐⭐): Implement a to-do list API. Requirements: (1) Complete CRUD operations; (2) Use Go 1.22 طريقة and path-based routing; (3) JSON requests and responses; (4) In-memory storage (map + RWMutex protection); (5) Return appropriate HTTP حالة codes.

  3. Challenge (Difficulty: ⭐⭐⭐): Implement a URL shortening service. Requirements: (1) POST /shorten accepts a long URL and returns a short code (a 6-character random سلسلة); (2) GET /{code} should perform a 301 redirect to the original URL; (3) Access statistics: GET /stats/{code} should return the number of visits; (4) Use -race to verify concurrency safety; (5) Use an RWMutex to protect the statistics counter.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏