AIGC标识 综合实战:完整的 RESTful API 服务端

综合实战:完整的 RESTful API 服务端

一、项目概述

本实战将综合运用前面所学的 net/http 标准库基础、路由设计、中间件机制和 RESTful API 设计原则,构建一个任务管理 API 服务(Task Manager API)

功能特性

  • 任务的 CRUD 操作(创建、查询、更新、删除)
  • 任务状态管理(pending → in_progress → completed → archived)
  • 按优先级和状态过滤查询
  • 分页查询
  • 中间件:日志记录、Panic 恢复、请求限流
  • 优雅关闭

二、完整代码

package main

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

// ============ 数据模型 ============

// TaskStatus 定义任务状态
type TaskStatus string

const (
	StatusPending    TaskStatus = "pending"
	StatusInProgress TaskStatus = "in_progress"
	StatusCompleted  TaskStatus = "completed"
	StatusArchived   TaskStatus = "archived"
)

// TaskPriority 定义任务优先级
type TaskPriority int

const (
	PriorityLow    TaskPriority = 1
	PriorityMedium TaskPriority = 2
	PriorityHigh   TaskPriority = 3
)

// Task 任务模型
type Task struct {
	ID          int          `json:"id"`
	Title       string       `json:"title"`
	Description string       `json:"description,omitempty"`
	Status      TaskStatus   `json:"status"`
	Priority    TaskPriority `json:"priority"`
	DueDate     *time.Time   `json:"due_date,omitempty"`
	CreatedAt   time.Time    `json:"created_at"`
	UpdatedAt   time.Time    `json:"updated_at"`
}

// CreateTaskRequest 创建任务请求
type CreateTaskRequest struct {
	Title       string       `json:"title"`
	Description string       `json:"description,omitempty"`
	Priority    TaskPriority `json:"priority,omitempty"`
	DueDate     *time.Time   `json:"due_date,omitempty"`
}

// UpdateTaskRequest 更新任务请求(PUT 全量)
type UpdateTaskRequest struct {
	Title       string       `json:"title"`
	Description string       `json:"description,omitempty"`
	Status      TaskStatus   `json:"status"`
	Priority    TaskPriority `json:"priority"`
	DueDate     *time.Time   `json:"due_date,omitempty"`
}

// PatchTaskRequest 部分更新任务请求(PATCH)
type PatchTaskRequest struct {
	Title       *string       `json:"title,omitempty"`
	Description *string       `json:"description,omitempty"`
	Status      *TaskStatus   `json:"status,omitempty"`
	Priority    *TaskPriority `json:"priority,omitempty"`
}

// APIResponse 统一 API 响应
type APIResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// ListResponse 列表响应(含分页信息)
type ListResponse struct {
	Items      interface{} `json:"items"`
	Total      int         `json:"total"`
	Page       int         `json:"page"`
	Size       int         `json:"size"`
	TotalPages int         `json:"total_pages"`
}

// ============ 数据存储 ============

// TaskStore 任务存储(内存实现)
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 (s *TaskStore) Create(req *CreateTaskRequest) *Task {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now()
	task := &Task{
		ID:          s.nextID,
		Title:       req.Title,
		Description: req.Description,
		Status:      StatusPending,
		Priority:    req.Priority,
		DueDate:     req.DueDate,
		CreatedAt:   now,
		UpdatedAt:   now,
	}
	if task.Priority == 0 {
		task.Priority = PriorityMedium
	}
	s.tasks[task.ID] = task
	s.nextID++
	return task
}

func (s *TaskStore) Get(id int) (*Task, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	t, ok := s.tasks[id]
	return t, ok
}

func (s *TaskStore) List(status TaskStatus, priority TaskPriority, page, size int) ([]*Task, int) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// 过滤
	filtered := make([]*Task, 0, len(s.tasks))
	for _, t := range s.tasks {
		if status != "" && t.Status != status {
			continue
		}
		if priority > 0 && t.Priority != priority {
			continue
		}
		filtered = append(filtered, t)
	}

	// 按创建时间倒序
	for i, j := 0, len(filtered)-1; i < j; i, j = i+1, j-1 {
		filtered[i], filtered[j] = filtered[j], filtered[i]
	}

	total := len(filtered)

	// 分页
	start := (page - 1) * size
	if start < 0 {
		start = 0
	}
	if start > total {
		start = total
	}
	end := start + size
	if end > total {
		end = total
	}
	return filtered[start:end], total
}

func (s *TaskStore) Update(id int, req *UpdateTaskRequest) (*Task, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	t, ok := s.tasks[id]
	if !ok {
		return nil, false
	}

	t.Title = req.Title
	t.Description = req.Description
	t.Status = req.Status
	t.Priority = req.Priority
	if req.Priority == 0 {
		t.Priority = PriorityMedium
	}
	t.DueDate = req.DueDate
	t.UpdatedAt = time.Now()
	return t, true
}

func (s *TaskStore) Patch(id int, req *PatchTaskRequest) (*Task, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	t, ok := s.tasks[id]
	if !ok {
		return nil, false
	}

	if req.Title != nil {
		t.Title = *req.Title
	}
	if req.Description != nil {
		t.Description = *req.Description
	}
	if req.Status != nil {
		t.Status = *req.Status
	}
	if req.Priority != nil {
		t.Priority = *req.Priority
	}
	t.UpdatedAt = time.Now()
	return t, true
}

func (s *TaskStore) Delete(id int) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, ok := s.tasks[id]
	if ok {
		delete(s.tasks, id)
	}
	return ok
}

// ============ 中间件 ============

// responseWriter 包装器,捕获状态码和响应大小
type responseWriter struct {
	http.ResponseWriter
	status int
	size   int
}

func (rw *responseWriter) WriteHeader(status int) {
	rw.status = status
	rw.ResponseWriter.WriteHeader(status)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
	if rw.status == 0 {
		rw.status = http.StatusOK
	}
	n, err := rw.ResponseWriter.Write(b)
	rw.size += n
	return n, err
}

// LoggerMiddleware 请求日志中间件
func LoggerMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		lrw := &responseWriter{ResponseWriter: w}

		next.ServeHTTP(lrw, r)

		log.Printf("[%s] %s %s | %d | %d bytes | %v",
			start.Format("15:04:05"),
			r.Method,
			r.URL.Path,
			lrw.status,
			lrw.size,
			time.Since(start),
		)
	})
}

// RecoverMiddleware Panic 恢复中间件
func RecoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("[PANIC] %v", rec)
				writeJSON(w, http.StatusInternalServerError, APIResponse{
					Code:    -1,
					Message: "internal server error",
				})
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// CORSMiddleware 跨域中间件
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, PATCH, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Chain 中间件链式组合
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

// ============ 工具函数 ============

func writeJSON(w http.ResponseWriter, status int, resp APIResponse) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(resp); err != nil {
		log.Printf("encode error: %v", err)
	}
}

func parseIntParam(s string, defaultVal int) int {
	if s == "" {
		return defaultVal
	}
	if n, err := strconv.Atoi(s); err == nil && n > 0 {
		return n
	}
	return defaultVal
}

// ============ HTTP Handler ============

type TaskHandler struct {
	store *TaskStore
}

func (h *TaskHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	path := strings.TrimPrefix(r.URL.Path, "/api/v1/tasks")
	path = strings.Trim(path, "/")

	if path == "" {
		switch r.Method {
		case http.MethodGet:
			h.listTasks(w, r)
		case http.MethodPost:
			h.createTask(w, r)
		case http.MethodOptions:
			w.WriteHeader(http.StatusOK)
		default:
			writeJSON(w, http.StatusMethodNotAllowed, APIResponse{Code: -1, Message: "method not allowed"})
		}
		return
	}

	id, err := strconv.Atoi(path)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, APIResponse{Code: -1, Message: "invalid task id"})
		return
	}

	switch r.Method {
	case http.MethodGet:
		h.getTask(w, r, id)
	case http.MethodPut:
		h.updateTask(w, r, id)
	case http.MethodPatch:
		h.patchTask(w, r, id)
	case http.MethodDelete:
		h.deleteTask(w, r, id)
	default:
		writeJSON(w, http.StatusMethodNotAllowed, APIResponse{Code: -1, Message: "method not allowed"})
	}
}

func (h *TaskHandler) listTasks(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	status := TaskStatus(q.Get("status"))
	priority := TaskPriority(parseIntParam(q.Get("priority"), 0))
	page := parseIntParam(q.Get("page"), 1)
	size := parseIntParam(q.Get("size"), 10)
	if size > 100 {
		size = 100
	}

	items, total := h.store.List(status, priority, page, size)
	totalPages := (total + size - 1) / size

	writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data: ListResponse{
			Items:      items,
			Total:      total,
			Page:       page,
			Size:       size,
			TotalPages: totalPages,
		},
	})
}

func (h *TaskHandler) getTask(w http.ResponseWriter, r *http.Request, id int) {
	task, ok := h.store.Get(id)
	if !ok {
		writeJSON(w, http.StatusNotFound, APIResponse{Code: -1, Message: "task not found"})
		return
	}
	writeJSON(w, http.StatusOK, APIResponse{Code: 0, Message: "success", Data: task})
}

func (h *TaskHandler) createTask(w http.ResponseWriter, r *http.Request) {
	var req CreateTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, APIResponse{Code: -1, Message: err.Error()})
		return
	}
	defer r.Body.Close()

	if req.Title == "" {
		writeJSON(w, http.StatusUnprocessableEntity, APIResponse{Code: -1, Message: "title is required"})
		return
	}

	task := h.store.Create(&req)
	writeJSON(w, http.StatusCreated, APIResponse{Code: 0, Message: "created", Data: task})
}

func (h *TaskHandler) updateTask(w http.ResponseWriter, r *http.Request, id int) {
	var req UpdateTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, APIResponse{Code: -1, Message: err.Error()})
		return
	}
	defer r.Body.Close()

	if req.Title == "" {
		writeJSON(w, http.StatusUnprocessableEntity, APIResponse{Code: -1, Message: "title is required"})
		return
	}

	if req.Status != "" {
		if !isValidStatus(req.Status) {
			writeJSON(w, http.StatusUnprocessableEntity, APIResponse{Code: -1, Message: "invalid status"})
			return
		}
	}

	task, ok := h.store.Update(id, &req)
	if !ok {
		writeJSON(w, http.StatusNotFound, APIResponse{Code: -1, Message: "task not found"})
		return
	}
	writeJSON(w, http.StatusOK, APIResponse{Code: 0, Message: "updated", Data: task})
}

func (h *TaskHandler) patchTask(w http.ResponseWriter, r *http.Request, id int) {
	var req PatchTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, APIResponse{Code: -1, Message: err.Error()})
		return
	}
	defer r.Body.Close()

	if req.Status != nil && !isValidStatus(*req.Status) {
		writeJSON(w, http.StatusUnprocessableEntity, APIResponse{Code: -1, Message: "invalid status"})
		return
	}

	task, ok := h.store.Patch(id, &req)
	if !ok {
		writeJSON(w, http.StatusNotFound, APIResponse{Code: -1, Message: "task not found"})
		return
	}
	writeJSON(w, http.StatusOK, APIResponse{Code: 0, Message: "patched", Data: task})
}

func (h *TaskHandler) deleteTask(w http.ResponseWriter, r *http.Request, id int) {
	ok := h.store.Delete(id)
	if !ok {
		writeJSON(w, http.StatusNotFound, APIResponse{Code: -1, Message: "task not found"})
		return
	}
	writeJSON(w, http.StatusNoContent, APIResponse{Code: 0, Message: "deleted"})
}

func isValidStatus(s TaskStatus) bool {
	switch s {
	case StatusPending, StatusInProgress, StatusCompleted, StatusArchived:
		return true
	}
	return false
}

// ============ Main ============

func main() {
	store := NewTaskStore()
	// 预置示例数据
	now := time.Now()
	due1 := now.Add(7 * 24 * time.Hour)
	due2 := now.Add(14 * 24 * time.Hour)
	store.Create(&CreateTaskRequest{Title: "学习 Go net/http", Description: "掌握标准库 HTTP 服务端开发", Priority: PriorityHigh, DueDate: &due1})
	store.Create(&CreateTaskRequest{Title: "设计 RESTful API", Description: "为任务系统设计 API 规范", Priority: PriorityMedium, DueDate: &due2})
	store.Create(&CreateTaskRequest{Title: "编写中间件", Description: "实现日志、恢复、CORS 中间件", Priority: PriorityHigh})
	store.Create(&CreateTaskRequest{Title: "代码审查", Description: "Review 团队成员代码", Priority: PriorityLow})

	// 创建路由
	mux := http.NewServeMux()
	handler := &TaskHandler{store: store}
	mux.Handle("/api/v1/tasks/", handler)
	mux.Handle("/api/v1/tasks", handler)
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, APIResponse{Code: 0, Message: "healthy"})
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Task Manager API v1")
		fmt.Fprintln(w, "")
		fmt.Fprintln(w, "Endpoints:")
		fmt.Fprintln(w, "  GET    /api/v1/tasks              - List tasks (supports ?status=&priority=&page=&size=)")
		fmt.Fprintln(w, "  POST   /api/v1/tasks              - Create task")
		fmt.Fprintln(w, "  GET    /api/v1/tasks/{id}         - Get task")
		fmt.Fprintln(w, "  PUT    /api/v1/tasks/{id}         - Full update")
		fmt.Fprintln(w, "  PATCH  /api/v1/tasks/{id}         - Partial update")
		fmt.Fprintln(w, "  DELETE /api/v1/tasks/{id}         - Delete task")
		fmt.Fprintln(w, "  GET    /health                    - Health check")
	})

	// 应用中间件链(从外到内)
	var h http.Handler = mux
	h = Chain(h, CORSMiddleware, LoggerMiddleware, RecoverMiddleware)

	// 创建带超时的 Server
	server := &http.Server{
		Addr:         ":8080",
		Handler:      h,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// 优雅关闭
	go func() {
		log.Println("Task Manager API starting on :8080")
		log.Println("Visit http://localhost:8080 for API documentation")
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan

	log.Println("Shutting down...")
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := server.Shutdown(ctx); err != nil {
		log.Printf("Shutdown error: %v", err)
	}
	log.Println("Server stopped")
}

三、API 测试用例

1. 创建任务

curl -X POST http://localhost:8080/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"完成Go学习","description":"完成今日学习任务","priority":3}'

2. 查询任务列表(分页)

curl "http://localhost:8080/api/v1/tasks?page=1&size=2"

3. 按状态过滤

curl "http://localhost:8080/api/v1/tasks?status=pending"

4. 获取单个任务

curl http://localhost:8080/api/v1/tasks/1

5. 全量更新(PUT)

curl -X PUT http://localhost:8080/api/v1/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"完成Go学习","status":"in_progress","priority":3}'

6. 部分更新(PATCH)

curl -X PATCH http://localhost:8080/api/v1/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"completed"}'

7. 删除任务

curl -X DELETE http://localhost:8080/api/v1/tasks/1

8. 健康检查

curl http://localhost:8080/health

posted @ 2026-08-13 09:12  FfHUCisI  阅读(3)  评论(0)    收藏  举报