0

Bài 10: Thực hành tổng hợp - Xây dựng RESTful API quản lý Sản phẩm

Chúc mừng bạn đã đi đến bài cuối cùng! Ở bài này, chúng ta sẽ xâu chuỗi toàn bộ kiến thức từ Bài 1 đến Bài 9 để xây dựng một ứng dụng RESTful API thực tế quản lý thông tin Sản phẩm (Products).

Ứng dụng này sẽ được cấu trúc chuyên nghiệp, không dùng Database thực (để tập trung vào HTTP) mà dùng một slice bộ nhớ trong (In-memory) kết hợp với sync.Mutex để tránh lỗi Race Condition khi xử lý Concurrent Requests.

1. Phân tích yêu cầu và Cấu trúc dự án

Chúng ta sẽ xây dựng 4 endpoints chuẩn RESTful:

  • GET /api/products - Lấy danh sách sản phẩm.

  • POST /api/products - Tạo sản phẩm mới (Yêu cầu JSON).

  • GET /api/products/{id} - Lấy thông tin chi tiết một sản phẩm theo ID.

  • DELETE /api/products/{id} - Xóa một sản phẩm.

Các kiến thức được áp dụng:

  • Routing Go 1.22: Sử dụng HTTP Methods và Path Variables trực tiếp.

  • Middleware: Tạo Log Middleware để theo dõi API.

  • Request/Response: Parse JSON, trích xuất biến động, trả về JSON đồng nhất.

  • Graceful Shutdown: Khởi động và tắt server an toàn.

2. Triển khai Mã Nguồn Hoàn Chỉnh

Bạn có thể lưu toàn bộ mã nguồn dưới đây vào một file main.go và chạy thử.

Go

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"syscall"
	"time"
)

// --- 1. MODEL VÀ DATA STORE ---

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

// Giả lập Database trong RAM
var (
	products = []Product{
		{ID: 1, Name: "Laptop Dell", Price: 1200.5},
		{ID: 2, Name: "Bàn phím cơ", Price: 85.0},
	}
	nextID = 3
	// Dùng Mutex để an toàn khi nhiều Goroutine cùng thêm/xóa dữ liệu
	mu sync.Mutex
)

// Hàm helper để chuẩn hóa việc trả về JSON (Bài 5)
func respondJSON(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

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

// --- 2. CÁC HANDLER LOGIC ---

func getProductsHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()
	respondJSON(w, http.StatusOK, products)
}

func createProductHandler(w http.ResponseWriter, r *http.Request) {
	var p Product
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		respondError(w, http.StatusBadRequest, "Dữ liệu JSON không hợp lệ")
		return
	}

	if p.Name == "" || p.Price <= 0 {
		respondError(w, http.StatusBadRequest, "Tên hoặc giá sản phẩm không hợp lệ")
		return
	}

	mu.Lock()
	p.ID = nextID
	nextID++
	products = append(products, p)
	mu.Unlock()

	respondJSON(w, http.StatusCreated, p)
}

func getProductByIDHandler(w http.ResponseWriter, r *http.Request) {
	// Trích xuất Path Variable {id} (Bài 6)
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		respondError(w, http.StatusBadRequest, "ID phải là số nguyên")
		return
	}

	mu.Lock()
	defer mu.Unlock()
	for _, p := range products {
		if p.ID == id {
			respondJSON(w, http.StatusOK, p)
			return
		}
	}
	respondError(w, http.StatusNotFound, "Không tìm thấy sản phẩm")
}

func deleteProductHandler(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")
	id, _ := strconv.Atoi(idStr)

	mu.Lock()
	defer mu.Unlock()
	
	for i, p := range products {
		if p.ID == id {
			// Xóa phần tử khỏi slice
			products = append(products[:i], products[i+1:]...)
			respondJSON(w, http.StatusOK, map[string]string{"message": "Đã xóa thành công"})
			return
		}
	}
	respondError(w, http.StatusNotFound, "Không tìm thấy sản phẩm để xóa")
}

// --- 3. MIDDLEWARE ---

// Middleware ghi log mọi request (Bài 7)
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 main() {
	// --- 4. ROUTING ---
	mux := http.NewServeMux()

	// Đăng ký các endpoint chuẩn RESTful với sức mạnh Go 1.22
	mux.HandleFunc("GET /api/products", getProductsHandler)
	mux.HandleFunc("POST /api/products", createProductHandler)
	mux.HandleFunc("GET /api/products/{id}", getProductByIDHandler)
	mux.HandleFunc("DELETE /api/products/{id}", deleteProductHandler)

	// Bọc toàn bộ Router bằng Logging Middleware
	handler := LoggingMiddleware(mux)

	// --- 5. SERVER NÂNG CAO & GRACEFUL SHUTDOWN (Bài 9) ---
	srv := &http.Server{
		Addr:         ":8080",
		Handler:      handler,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	// Khởi động server trên goroutine
	go func() {
		log.Println("Server API đang chạy tại http://localhost:8080")
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Lỗi server: %v", err)
		}
	}()

	// Chờ tín hiệu ngắt (Ctrl+C hoặc Docker Stop)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Bắt đầu tắt server an toàn...")

	// Dành tối đa 10s để xử lý nốt các request còn tồn đọng
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Fatalf("Lỗi khi tắt server: %v", err)
	}

	log.Println("Đã tắt server hoàn toàn.")
}

3. Cách tương tác với API vừa tạo

Sau khi chạy go run main.go, bạn có thể dùng Terminal (câu lệnh curl) hoặc phần mềm Postman/Insomnia để kiểm tra:

Lấy danh sách:

Bash

curl http://localhost:8080/api/products

Tạo sản phẩm mới:

Bash

curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name": "Chuột Logitech", "price": 45.5}'

Lấy chi tiết (Thử với ID vừa tạo là 3):

Bash

curl http://localhost:8080/api/products/3

Xóa sản phẩm:

Bash

curl -X DELETE http://localhost:8080/api/products/1

Lời kết

Chúc mừng bạn! Bạn đã hoàn thành xuất sắc 10 bài học về package net/http trong Go. Với lượng kiến thức nền tảng vững chắc này, bạn đã đủ sức tự tin xây dựng các RESTful API thuần hiệu năng cao, viết custom Middleware bảo mật, và cấu hình server chuẩn Production mà không bị lệ thuộc vào bất kỳ framework bên thứ ba nào.

Khi làm việc với các hệ thống backend cho web hoặc app bán hàng, bạn có thể dễ dàng mở rộng cấu trúc này bằng cách kết nối MySQL/PostgreSQL thông qua package database/sql hoặc các ORM như GORM.

Chúc bạn áp dụng thành công những kiến thức này vào công việc và các dự án cá nhân sắp tới!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí