1. HTTP Server with net/http
Go has a built-in HTTP server in the standard library, powerful enough for production — no need to use a framework.
1.1. Basic HTTP Server
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// Handler function
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status": "ok"}`)
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
1.2. ServeMux (Go 1.22+ Enhanced Routing)
func main() {
mux := http.NewServeMux()
// Go 1.22+: Method patterns và path parameters
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Static files
mux.Handle("GET /static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("./static")),
))
// Exact match vs prefix
mux.HandleFunc("GET /api/", apiNotFound) // Prefix match
mux.HandleFunc("GET /api/health", apiHealth) // Exact match (priority)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("Server starting on :8080")
log.Fatal(server.ListenAndServe())
}
func getUser(w http.ResponseWriter, r *http.Request) {
// Go 1.22+: Path parameter
id := r.PathValue("id")
// Query parameters
page := r.URL.Query().Get("page")
fmt.Fprintf(w, "User ID: %s, Page: %s", id, page)
}
1.3. Handler Interface
// http.Handler interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// Struct-based handler
type UserHandler struct {
service *UserService
}
func NewUserHandler(service *UserService) *UserHandler {
return &UserHandler{service: service}
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.list(w, r)
case http.MethodPost:
h.create(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *UserHandler) list(w http.ResponseWriter, r *http.Request) {
users, err := h.service.ListUsers(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, users)
}
func (h *UserHandler) create(w http.ResponseWriter, r *http.Request) {
var input CreateUserInput
if err := readJSON(r, &input); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
user, err := h.service.CreateUser(r.Context(), input)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, user)
}
1.4. Middleware Pattern
// Middleware: func(http.Handler) http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter để capture status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s %d %s",
r.Method, r.URL.Path, wrapped.statusCode, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
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 == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token, set user in context
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Chain middlewares
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
// Apply middleware chain
handler := recoveryMiddleware(
loggingMiddleware(
corsMiddleware(
mux,
),
),
)
http.ListenAndServe(":8080", handler)
}
2. JSON Handling
2.1. Structure Tags
import "encoding/json"
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // Bỏ qua khi marshal
Age int `json:"age,omitempty"` // Bỏ qua nếu zero value
Role string `json:"role,omitempty"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` // Pointer + omitempty cho nullable
}
// json tags:
// `json:"field_name"` - đổi tên field
// `json:"-"` - bỏ qua field
// `json:"name,omitempty"` - bỏ qua nếu zero value
// `json:",string"` - marshal number thành string
2.2. Marshal (Struct → JSON)
func main() {
user := User{
ID: 1,
Name: "Alice",
Email: "[email protected]",
Age: 30,
}
// Marshal → []byte
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// {"id":1,"name":"Alice","email":"[email protected]","age":30,"created_at":"0001-01-01T00:00:00Z"}
// MarshalIndent → pretty print
pretty, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(pretty))
// Encode trực tiếp vào io.Writer (hiệu quả hơn cho HTTP)
json.NewEncoder(os.Stdout).Encode(user)
}
2.3. Unmarshal (JSON → Struct)
func main() {
jsonStr := `{"id": 1, "name": "Alice", "email": "[email protected]", "age": 30}`
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", user)
// Decode từ io.Reader (HTTP request body)
// json.NewDecoder(r.Body).Decode(&user)
// Dynamic JSON (không biết structure)
var data map[string]any
json.Unmarshal([]byte(jsonStr), &data)
fmt.Println(data["name"].(string)) // Type assertion cần thiết
// JSON array
jsonArray := `[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]`
var users []User
json.Unmarshal([]byte(jsonArray), &users)
}
2.4. Custom Marshal/Unmarshal
// Custom time format
type CustomTime struct {
time.Time
}
const timeFormat = "2006-01-02 15:04:05"
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Format(timeFormat))
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse(timeFormat, s)
if err != nil {
return err
}
ct.Time = t
return nil
}
// Enum type
type Status string
const (
StatusActive Status = "active"
StatusInactive Status = "inactive"
)
func (s *Status) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
switch Status(str) {
case StatusActive, StatusInactive:
*s = Status(str)
return nil
default:
return fmt.Errorf("invalid status: %s", str)
}
}
2.5. JSON Helper Functions
// Reusable JSON helpers cho HTTP handlers
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func readJSON(r *http.Request, dst any) error {
// Limit body size
r.Body = http.MaxBytesReader(nil, r.Body, 1_048_576) // 1MB
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // Strict mode
if err := dec.Decode(dst); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
// Check for extra data
if dec.More() {
return errors.New("body must contain single JSON object")
}
return nil
}
type APIResponse struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func writeSuccess(w http.ResponseWriter, data any) {
writeJSON(w, http.StatusOK, APIResponse{Success: true, Data: data})
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, APIResponse{Success: false, Error: message})
}
3. HTTP Client
import (
"context"
"io"
"net/http"
"time"
)
// ⚠️ KHÔNG dùng http.DefaultClient trong production (không có timeout)
// Custom client với timeouts
var client = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
// GET request
func fetchUser(ctx context.Context, id int) (*User, error) {
url := fmt.Sprintf("https://api.example.com/users/%d", id)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &user, nil
}
// POST request
func createUser(ctx context.Context, input CreateUserInput) (*User, error) {
body, err := json.Marshal(input)
if err != nil {
return nil, fmt.Errorf("marshal input: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.example.com/users", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &user, nil
}
4. Graceful Shutdown
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
mux.HandleFunc("GET /api/users", listUsersHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server trong goroutine
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down gracefully...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server stopped")
}
5. Summary
- net/http: Production-ready HTTP server built-in
- Go 1.22+ ServeMux: Method patterns, path parameters — no router library needed
- Middleware:
func(http.Handler) http.Handlerpattern. pattern - encoding/json: Structure tags, Marshal/Unmarshal, custom serialization
- HTTP Client: Always set timeout, use context, reuse client
- Graceful shutdown: Signal handling + server.Shutdown(ctx)
Next article: Gin Framework & REST API — build REST API production-ready with Gin.