Go: Go HTTP Services
Last updated: 2026-08-26
The
net/httppackage 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
http.ListenAndServeStarts the service- The
Handlerinterface and theHandlerFuncadapter ServeMuxroute registration- Go 1.22 Enhanced Routing: Method + Path Pattern + Path Parameters
- JSON استجابة encoding
- Request Parameter Parsing (Query Parameters, Form Parameters, Path Parameters)
- Static File Service
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:
// 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
// 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 |
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
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))
}
Test:
$ 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
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
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)
}
(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
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))
}
Test:
$ 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
5. Requests and Responses
▶ Example: Query Parameters, Forms, JSON
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))
}
▶ Example: JSON Response Utility Function
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))
}
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
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))
}
7. Complete Example: REST-Style Notes API
// 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))
}
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
Handler and HandlerFunc?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.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.ListenAndServe and ListenAndServeTLS?http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux).خادم.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.nil as the second argument to http.ListenAndServe?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.📖 Summary
http.ListenAndServestarts an HTTP serviceHandlerinterface +HandlerFuncadapter- Go 1.22 Enhanced Routing:
"METHOD /path/{param}"syntax r.PathValue("name")retrieves the path parameter- JSON استجابة: Set
Content-Type+json.NewEncoder - Request Parsing: Query Parameters, Forms, JSON Body
- Static file service:
http.FileServer - Status code: Use
http.Status*constants
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Create a simple HTTP service and register three routes:
GET /timereturns the current time as JSON,GET /healthreturns{"حالة": "ok"}, andGET /versionreturns the version number. Use the enhanced routing syntax in Go 1.22. -
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.
-
Challenge (Difficulty: ⭐⭐⭐): Implement a URL shortening service. Requirements: (1)
POST /shortenaccepts 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-raceto verify concurrency safety; (5) Use an RWMutex to protect the statistics counter.