写在最前

x. 示例项目

x. book_management

book_management/

├── main.go # 程序入口

├── book.go # 图书结构和方法

├── user.go # 用户结构和方法

├── library.go # 图书馆管理逻辑

├── storage.go # 数据存储

└── menu.go # 用户界面菜单

book.go

package main

import (
	"fmt"
	"time"
)

// Book 结构体表示一本图书
type Book struct {
	ID        int       // 图书ID
	Title     string    // 书名
	Author    string    // 作者
	ISBN      string    // ISBN号
	Available bool      // 是否可借
	Borrower  string    // 借阅者姓名(如果被借出)
	DueDate   time.Time // 应归还日期
}

// NewBook 创建新图书
func NewBook(id int, title, author, isbn string) *Book {
	return &Book{
		ID:        id,
		Title:     title,
		Author:    author,
		ISBN:      isbn,
		Available: true,
		Borrower:  "",
		DueDate:   time.Time{},
	}
}

// Borrow 借阅图书
func (b *Book) Borrow(borrower string, days int) error {
	if !b.Available {
		return fmt.Errorf("书籍《%s》已被借出", b.Title)
	}

	b.Available = false
	b.Borrower = borrower
	b.DueDate = time.Now().AddDate(0, 0, days)
	return nil
}

// Return 归还图书
func (b *Book) Return() {
	b.Available = true
	b.Borrower = ""
	b.DueDate = time.Time{}
}

// Display 显示图书信息
func (b *Book) Display() {
	status := "可借阅"
	if !b.Available {
		status = fmt.Sprintf("已借出(借阅人:%s,应归还:%s)",
			b.Borrower, b.DueDate.Format("2006-01-02"))
	}

	fmt.Printf("ID: %d | 《%s》- %s | ISBN: %s | 状态: %s\n",
		b.ID, b.Title, b.Author, b.ISBN, status)
}

user.go

package main

import "fmt"

// User 结构体表示系统用户
type User struct {
	ID       int    // 用户ID
	Name     string // 姓名
	Email    string // 邮箱
	Borrowed []int  // 借阅的图书ID列表
}

// NewUser 创建新用户
func NewUser(id int, name, email string) *User {
	return &User{
		ID:       id,
		Name:     name,
		Email:    email,
		Borrowed: make([]int, 0),
	}
}

// BorrowBook 用户借书
func (u *User) BorrowBook(bookID int) {
	u.Borrowed = append(u.Borrowed, bookID)
}

// ReturnBook 用户还书
func (u *User) ReturnBook(bookID int) {
	for i, id := range u.Borrowed {
		if id == bookID {
			u.Borrowed = append(u.Borrowed[:i], u.Borrowed[i+1:]...)
			break
		}
	}
}

// Display 显示用户信息
func (u *User) Display() {
	fmt.Printf("用户ID: %d | 姓名: %s | 邮箱: %s | 借阅数量: %d\n",
		u.ID, u.Name, u.Email, len(u.Borrowed))
}

storage.go

package main

import (
	"encoding/json"
	"os"
	"sync"
)

// Storage 管理数据存储
type Storage struct {
	Books    map[int]*Book // 图书数据
	Users    map[int]*User // 用户数据
	nextBookID int         // 下一个图书ID
	nextUserID int         // 下一个用户ID
	mu       sync.RWMutex  // 读写锁,保证并发安全
}

// NewStorage 创建存储实例
func NewStorage() *Storage {
	return &Storage{
		Books:      make(map[int]*Book),
		Users:      make(map[int]*User),
		nextBookID: 1,
		nextUserID: 1,
	}
}

// SaveToFile 保存数据到文件
func (s *Storage) SaveToFile(filename string) error {
	s.mu.RLock()
	defer s.mu.RUnlock()

	data := struct {
		Books      map[int]*Book `json:"books"`
		Users      map[int]*User `json:"users"`
		NextBookID int           `json:"next_book_id"`
		NextUserID int           `json:"next_user_id"`
	}{
		Books:      s.Books,
		Users:      s.Users,
		NextBookID: s.nextBookID,
		NextUserID: s.nextUserID,
	}

	file, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filename, file, 0644)
}

// LoadFromFile 从文件加载数据
func (s *Storage) LoadFromFile(filename string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	file, err := os.ReadFile(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // 文件不存在时返回nil,使用初始数据
		}
		return err
	}

	var data struct {
		Books      map[int]*Book `json:"books"`
		Users      map[int]*User `json:"users"`
		NextBookID int           `json:"next_book_id"`
		NextUserID int           `json:"next_user_id"`
	}

	if err := json.Unmarshal(file, &data); err != nil {
		return err
	}

	s.Books = data.Books
	s.Users = data.Users
	s.nextBookID = data.NextBookID
	s.nextUserID = data.NextUserID

	return nil
}

// AddBook 添加图书
func (s *Storage) AddBook(title, author, isbn string) *Book {
	s.mu.Lock()
	defer s.mu.Unlock()

	book := NewBook(s.nextBookID, title, author, isbn)
	s.Books[s.nextBookID] = book
	s.nextBookID++

	return book
}

// AddUser 添加用户
func (s *Storage) AddUser(name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user := NewUser(s.nextUserID, name, email)
	s.Users[s.nextUserID] = user
	s.nextUserID++

	return user
}

// GetBook 获取图书
func (s *Storage) GetBook(id int) (*Book, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	book, exists := s.Books[id]
	return book, exists
}

// GetUser 获取用户
func (s *Storage) GetUser(id int) (*User, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	user, exists := s.Users[id]
	return user, exists
}

// GetAllBooks 获取所有图书
func (s *Storage) GetAllBooks() []*Book {
	s.mu.RLock()
	defer s.mu.RUnlock()

	books := make([]*Book, 0, len(s.Books))
	for _, book := range s.Books {
		books = append(books, book)
	}
	return books
}

// GetAllUsers 获取所有用户
func (s *Storage) GetAllUsers() []*User {
	s.mu.RLock()
	defer s.mu.RUnlock()

	users := make([]*User, 0, len(s.Users))
	for _, user := range s.Users {
		users = append(users, user)
	}
	return users
}

library.go

package main

import (
	"fmt"
	"strings"
)

// Library 图书馆管理系统
type Library struct {
	storage *Storage
}

// NewLibrary 创建图书馆实例
func NewLibrary() *Library {
	library := &Library{
		storage: NewStorage(),
	}

	// 加载已有数据
	if err := library.storage.LoadFromFile("library_data.json"); err != nil {
		fmt.Printf("加载数据失败: %v\n", err)
		fmt.Println("将使用初始空数据")
	}

	return library
}

// AddBook 添加图书
func (l *Library) AddBook(title, author, isbn string) {
	book := l.storage.AddBook(title, author, isbn)
	fmt.Printf("✅ 添加成功!图书ID: %d\n", book.ID)
	l.SaveData()
}

// AddUser 添加用户
func (l *Library) AddUser(name, email string) {
	user := l.storage.AddUser(name, email)
	fmt.Printf("✅ 用户添加成功!用户ID: %d\n", user.ID)
	l.SaveData()
}

// ListBooks 列出所有图书
func (l *Library) ListBooks() {
	books := l.storage.GetAllBooks()

	if len(books) == 0 {
		fmt.Println("📚 图书馆暂无图书")
		return
	}

	fmt.Printf("📚 图书列表(共 %d 本):\n", len(books))
	for _, book := range books {
		book.Display()
	}
}

// ListUsers 列出所有用户
func (l *Library) ListUsers() {
	users := l.storage.GetAllUsers()

	if len(users) == 0 {
		fmt.Println("👤 暂无用户")
		return
	}

	fmt.Printf("👤 用户列表(共 %d 人):\n", len(users))
	for _, user := range users {
		user.Display()
	}
}

// BorrowBook 借阅图书
func (l *Library) BorrowBook(bookID, userID, days int) {
	book, bookExists := l.storage.GetBook(bookID)
	if !bookExists {
		fmt.Printf("❌ 图书ID %d 不存在\n", bookID)
		return
	}

	user, userExists := l.storage.GetUser(userID)
	if !userExists {
		fmt.Printf("❌ 用户ID %d 不存在\n", userID)
		return
	}

	if err := book.Borrow(user.Name, days); err != nil {
		fmt.Printf("❌ 借阅失败: %v\n", err)
		return
	}

	user.BorrowBook(bookID)
	fmt.Println("✅ 借阅成功!")
	l.SaveData()
}

// ReturnBook 归还图书
func (l *Library) ReturnBook(bookID int) {
	book, exists := l.storage.GetBook(bookID)
	if !exists {
		fmt.Printf("❌ 图书ID %d 不存在\n", bookID)
		return
	}

	if book.Available {
		fmt.Println("❌ 这本书没有被借出")
		return
	}

	// 找到借阅者并更新其借阅记录
	for _, user := range l.storage.GetAllUsers() {
		for _, borrowedID := range user.Borrowed {
			if borrowedID == bookID {
				user.ReturnBook(bookID)
				break
			}
		}
	}

	book.Return()
	fmt.Println("✅ 归还成功!")
	l.SaveData()
}

// SearchBooks 搜索图书
func (l *Library) SearchBooks(keyword string) {
	books := l.storage.GetAllBooks()
	results := make([]*Book, 0)

	keyword = strings.ToLower(keyword)

	for _, book := range books {
		if strings.Contains(strings.ToLower(book.Title), keyword) ||
			strings.Contains(strings.ToLower(book.Author), keyword) ||
			strings.Contains(strings.ToLower(book.ISBN), keyword) {
			results = append(results, book)
		}
	}

	if len(results) == 0 {
		fmt.Printf("🔍 未找到包含 \"%s\" 的图书\n", keyword)
		return
	}

	fmt.Printf("🔍 找到 %d 本相关图书:\n", len(results))
	for _, book := range results {
		book.Display()
	}
}

// SaveData 保存数据
func (l *Library) SaveData() {
	if err := l.storage.SaveToFile("library_data.json"); err != nil {
		fmt.Printf("⚠️  保存数据失败: %v\n", err)
	} else {
		fmt.Println("💾 数据已自动保存")
	}
}
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// Menu 管理用户界面
type Menu struct {
	library *Library
	scanner *bufio.Scanner
}

// NewMenu 创建菜单
func NewMenu(library *Library) *Menu {
	return &Menu{
		library: library,
		scanner: bufio.NewScanner(os.Stdin),
	}
}

// Run 运行主菜单
func (m *Menu) Run() {
	for {
		m.showMainMenu()
		choice := m.getInput("请选择操作: ")

		switch choice {
		case "1":
			m.addBook()
		case "2":
			m.library.ListBooks()
		case "3":
			m.addUser()
		case "4":
			m.library.ListUsers()
		case "5":
			m.borrowBook()
		case "6":
			m.returnBook()
		case "7":
			m.searchBooks()
		case "8":
			fmt.Println("谢谢使用!再见!")
			m.library.SaveData()
			return
		default:
			fmt.Println("无效选择,请重新输入")
		}

		fmt.Println()
	}
}

func (m *Menu) showMainMenu() {
	fmt.Println("\n===== 图书管理系统 =====")
	fmt.Println("1. 添加图书")
	fmt.Println("2. 查看所有图书")
	fmt.Println("3. 添加用户")
	fmt.Println("4. 查看所有用户")
	fmt.Println("5. 借阅图书")
	fmt.Println("6. 归还图书")
	fmt.Println("7. 搜索图书")
	fmt.Println("8. 退出系统")
	fmt.Println("======================")
}

func (m *Menu) addBook() {
	fmt.Println("\n--- 添加图书 ---")
	title := m.getInput("书名: ")
	author := m.getInput("作者: ")
	isbn := m.getInput("ISBN: ")

	if title == "" || author == "" {
		fmt.Println("❌ 书名和作者不能为空")
		return
	}

	m.library.AddBook(title, author, isbn)
}

func (m *Menu) addUser() {
	fmt.Println("\n--- 添加用户 ---")
	name := m.getInput("姓名: ")
	email := m.getInput("邮箱: ")

	if name == "" {
		fmt.Println("❌ 姓名不能为空")
		return
	}

	m.library.AddUser(name, email)
}

func (m *Menu) borrowBook() {
	fmt.Println("\n--- 借阅图书 ---")

	bookIDStr := m.getInput("图书ID: ")
	bookID, err := strconv.Atoi(bookIDStr)
	if err != nil {
		fmt.Println("❌ 请输入有效的图书ID(数字)")
		return
	}

	userIDStr := m.getInput("用户ID: ")
	userID, err := strconv.Atoi(userIDStr)
	if err != nil {
		fmt.Println("❌ 请输入有效的用户ID(数字)")
		return
	}

	daysStr := m.getInput("借阅天数: ")
	days, err := strconv.Atoi(daysStr)
	if err != nil || days <= 0 {
		fmt.Println("❌ 请输入有效的借阅天数(正整数)")
		return
	}

	m.library.BorrowBook(bookID, userID, days)
}

func (m *Menu) returnBook() {
	fmt.Println("\n--- 归还图书 ---")
	bookIDStr := m.getInput("图书ID: ")
	bookID, err := strconv.Atoi(bookIDStr)
	if err != nil {
		fmt.Println("❌ 请输入有效的图书ID(数字)")
		return
	}

	m.library.ReturnBook(bookID)
}

func (m *Menu) searchBooks() {
	fmt.Println("\n--- 搜索图书 ---")
	keyword := m.getInput("请输入书名、作者或ISBN关键字: ")

	if keyword == "" {
		fmt.Println("❌ 搜索关键字不能为空")
		return
	}

	m.library.SearchBooks(keyword)
}

func (m *Menu) getInput(prompt string) string {
	fmt.Print(prompt)
	m.scanner.Scan()
	return strings.TrimSpace(m.scanner.Text())
}

main.go

package main

import "fmt"

func main() {
	fmt.Println("📚 欢迎使用图书管理系统")
	fmt.Println("========================")

	// 初始化图书馆系统
	library := NewLibrary()

	// 初始化菜单
	menu := NewMenu(library)

	// 运行系统
	menu.Run()
}

x. todo_app

todo_app/

├── main.go # 程序入口

├── handlers/ # HTTP处理器

│ └── todo.go

├── models/ # 数据模型

│ └── todo.go

├── storage/ # 数据存储

│ └── database.go

├── routes/ # 路由定义

│ └── routes.go

├── config/ # 配置文件

│ └── config.go

├── middleware/ # 中间件

│ └── auth.go

├── go.mod

└── go.sum

go.mod

module todo_app

go 1.25

main.go

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
	"todo_app/config"
	"todo_app/handlers"
	"todo_app/middleware"
	"todo_app/routes"
	"todo_app/storage"
)

func main() {
	fmt.Println("🚀 启动待办事项管理系统...")

	// 加载配置
	cfg := config.LoadConfig()
	cfg.Print()

	// 初始化数据库
	db := storage.NewDatabase()

	// 加载已有数据
	if err := db.LoadFromFile(cfg.DataFile); err != nil {
		log.Printf("⚠️  加载数据失败: %v", err)
	} else {
		todos, _ := db.GetAllTodos()
		fmt.Printf("📊 已加载 %d 条待办事项\n", len(todos))
	}

	// 初始化处理器
	todoHandler := handlers.NewTodoHandler(db)

	// 创建路由
	mux := http.NewServeMux()
	routes.SetupRoutes(mux, todoHandler)

	// 包装中间件
	handler := middleware.Logging(mux)
	if cfg.EnableCORS {
		handler = middleware.CORS(handler)
	}

	// 创建HTTP服务器
	server := &http.Server{
		Addr:         ":" + cfg.Port,
		Handler:      handler,
		ReadTimeout:  time.Duration(cfg.ReadTimeout) * time.Second,
		WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
	}

	// 优雅关闭
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		fmt.Printf("🌐 服务器启动在 http://localhost:%s\n", cfg.Port)
		fmt.Println("📝 API接口:")
		fmt.Println("  GET    /api/todos            # 获取所有待办")
		fmt.Println("  POST   /api/todos            # 创建待办")
		fmt.Println("  GET    /api/todos/{id}       # 获取单个待办")
		fmt.Println("  PUT    /api/todos/{id}       # 更新待办")
		fmt.Println("  DELETE /api/todos/{id}       # 删除待办")
		fmt.Println("  PATCH  /api/todos/{id}/toggle # 切换状态")
		fmt.Println("🔍 按 Ctrl+C 停止服务器")

		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("服务器启动失败: %v", err)
		}
	}()

	// 等待退出信号
	<-quit
	fmt.Println("\n🛑 正在停止服务器...")

	// 保存数据
	if err := db.SaveToFile(cfg.DataFile); err != nil {
		log.Printf("保存数据失败: %v", err)
	} else {
		fmt.Println("💾 数据已保存")
	}

	// 优雅关闭服务器
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("服务器关闭失败: %v", err)
	}

	fmt.Println("👋 服务器已安全关闭")
}

config/config.go

package config

import (
	"flag"
	"fmt"
)

type Config struct {
	Port         string
	DataFile     string
	EnableCORS   bool
	ReadTimeout  int
	WriteTimeout int
}

func LoadConfig() *Config {
	cfg := &Config{}

	// 命令行参数
	flag.StringVar(&cfg.Port, "port", "8080", "服务器端口")
	flag.StringVar(&cfg.DataFile, "data", "./todos.json", "数据文件路径")
	flag.BoolVar(&cfg.EnableCORS, "cors", true, "启用CORS")
	flag.IntVar(&cfg.ReadTimeout, "read-timeout", 10, "读取超时时间(秒)")
	flag.IntVar(&cfg.WriteTimeout, "write-timeout", 10, "写入超时时间(秒)")

	flag.Parse()

	return cfg
}

func (c *Config) Print() {
	fmt.Println("=== 配置信息 ===")
	fmt.Printf("端口: %s\n", c.Port)
	fmt.Printf("数据文件: %s\n", c.DataFile)
	fmt.Printf("CORS: %v\n", c.EnableCORS)
	fmt.Printf("读取超时: %d秒\n", c.ReadTimeout)
	fmt.Printf("写入超时: %d秒\n", c.WriteTimeout)
	fmt.Println("===============")
}

handlers/todo.go

package handlers

import (
	"encoding/json"
	"net/http"
	"strconv"
	"todo_app/models"
	"todo_app/storage"
)

type TodoHandler struct {
	db *storage.Database
}

func NewTodoHandler(db *storage.Database) *TodoHandler {
	return &TodoHandler{db: db}
}

// GetAllTodos 获取所有待办事项
func (h *TodoHandler) GetAllTodos(w http.ResponseWriter, r *http.Request) {
	// 支持查询参数筛选
	status := r.URL.Query().Get("status")
	search := r.URL.Query().Get("search")

	var todos []models.Todo
	var err error

	if search != "" {
		todos, err = h.db.SearchTodos(search)
	} else if status != "" {
		completed, _ := strconv.ParseBool(status)
		todos, err = h.db.GetTodosByStatus(completed)
	} else {
		todos, err = h.db.GetAllTodos()
	}

	if err != nil {
		respondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}

	respondWithJSON(w, http.StatusOK, models.TodoResponse{
		Success: true,
		Message: "获取待办事项成功",
		Data:    todos,
	})
}

// GetTodoByID 根据ID获取待办事项
func (h *TodoHandler) GetTodoByID(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的ID")
		return
	}

	todo, err := h.db.GetTodoByID(id)
	if err != nil {
		respondWithError(w, http.StatusNotFound, err.Error())
		return
	}

	respondWithJSON(w, http.StatusOK, models.TodoResponse{
		Success: true,
		Message: "获取待办事项成功",
		Data:    todo,
	})
}

// CreateTodo 创建待办事项
func (h *TodoHandler) CreateTodo(w http.ResponseWriter, r *http.Request) {
	var todoCreate models.TodoCreate

	// 解析请求体
	if err := json.NewDecoder(r.Body).Decode(&todoCreate); err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的请求数据")
		return
	}
	defer r.Body.Close()

	// 简单的验证
	if todoCreate.Title == "" {
		respondWithError(w, http.StatusBadRequest, "标题不能为空")
		return
	}

	if todoCreate.Priority < 1 || todoCreate.Priority > 5 {
		todoCreate.Priority = 3 // 默认优先级
	}

	todo, err := h.db.CreateTodo(todoCreate)
	if err != nil {
		respondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}

	respondWithJSON(w, http.StatusCreated, models.TodoResponse{
		Success: true,
		Message: "待办事项创建成功",
		Data:    todo,
	})
}

// UpdateTodo 更新待办事项
func (h *TodoHandler) UpdateTodo(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的ID")
		return
	}

	var update models.TodoUpdate
	if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的请求数据")
		return
	}
	defer r.Body.Close()

	todo, err := h.db.UpdateTodo(id, update)
	if err != nil {
		respondWithError(w, http.StatusNotFound, err.Error())
		return
	}

	respondWithJSON(w, http.StatusOK, models.TodoResponse{
		Success: true,
		Message: "待办事项更新成功",
		Data:    todo,
	})
}

// DeleteTodo 删除待办事项
func (h *TodoHandler) DeleteTodo(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的ID")
		return
	}

	if err := h.db.DeleteTodo(id); err != nil {
		respondWithError(w, http.StatusNotFound, err.Error())
		return
	}

	respondWithJSON(w, http.StatusOK, models.TodoResponse{
		Success: true,
		Message: "待办事项删除成功",
	})
}

// ToggleTodo 切换完成状态
func (h *TodoHandler) ToggleTodo(w http.ResponseWriter, r *http.Request) {
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		respondWithError(w, http.StatusBadRequest, "无效的ID")
		return
	}

	todo, err := h.db.GetTodoByID(id)
	if err != nil {
		respondWithError(w, http.StatusNotFound, err.Error())
		return
	}

	completed := !todo.Completed
	_, err = h.db.UpdateTodo(id, models.TodoUpdate{Completed: &completed})
	if err != nil {
		respondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}

	respondWithJSON(w, http.StatusOK, models.TodoResponse{
		Success: true,
		Message: "待办事项状态更新成功",
		Data: map[string]bool{
			"completed": completed,
		},
	})
}

// 辅助函数:返回JSON响应
func respondWithJSON(w http.ResponseWriter, statusCode int, payload interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	json.NewEncoder(w).Encode(payload)
}

// 辅助函数:返回错误响应
func respondWithError(w http.ResponseWriter, statusCode int, message string) {
	respondWithJSON(w, statusCode, models.TodoResponse{
		Success: false,
		Error:   message,
	})
}

middleware/cors.go

package middleware

import "net/http"

func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 设置CORS头
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		// 处理预检请求
		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

// Logging 日志中间件
func Logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 这里可以添加日志记录逻辑
		next.ServeHTTP(w, r)
	})
}

models/todo.go

package models

import (
	"time"
)

// Todo 待办事项模型
type Todo struct {
	ID          int       `json:"id"`
	Title       string    `json:"title"`
	Description string    `json:"description"`
	Completed   bool      `json:"completed"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
	DueDate     time.Time `json:"due_date,omitempty"` // omitempty: 空值时不输出
	Priority    int       `json:"priority"`           // 优先级 1-5,1最高
}

// TodoCreate 创建Todo的请求结构
type TodoCreate struct {
	Title       string    `json:"title" validate:"required,min=1,max=100"`
	Description string    `json:"description" validate:"max=500"`
	DueDate     time.Time `json:"due_date"`
	Priority    int       `json:"priority" validate:"min=1,max=5"`
}

// TodoUpdate 更新Todo的请求结构
type TodoUpdate struct {
	Title       *string    `json:"title,omitempty" validate:"omitempty,min=1,max=100"`
	Description *string    `json:"description,omitempty" validate:"omitempty,max=500"`
	Completed   *bool      `json:"completed,omitempty"`
	DueDate     *time.Time `json:"due_date,omitempty"`
	Priority    *int       `json:"priority,omitempty" validate:"omitempty,min=1,max=5"`
}

// TodoResponse API响应结构
type TodoResponse struct {
	Success bool        `json:"success"`
	Message string      `json:"message,omitempty"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
}

routes/routes.go

package routes

import (
	"net/http"
	"todo_app/handlers"
)

func SetupRoutes(mux *http.ServeMux, todoHandler *handlers.TodoHandler) {
	// 健康检查
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"status": "ok", "service": "todo-api"}`))
	})

	// API路由
	mux.HandleFunc("GET /api/todos", todoHandler.GetAllTodos)
	mux.HandleFunc("POST /api/todos", todoHandler.CreateTodo)
	mux.HandleFunc("GET /api/todos/{id}", todoHandler.GetTodoByID)
	mux.HandleFunc("PUT /api/todos/{id}", todoHandler.UpdateTodo)
	mux.HandleFunc("DELETE /api/todos/{id}", todoHandler.DeleteTodo)
	mux.HandleFunc("PATCH /api/todos/{id}/toggle", todoHandler.ToggleTodo)

	// 静态文件服务(可选,用于Web界面)
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/" {
			http.ServeFile(w, r, "static/index.html")
		} else {
			http.NotFound(w, r)
		}
	})
}

storage/database.go

package storage

import (
	"encoding/json"
	"fmt"
	"os"
	"sync"
	"time"
	"todo_app/models"
)

// Database 内存数据库
type Database struct {
	todos  map[int]models.Todo
	mu     sync.RWMutex
	nextID int
}

// NewDatabase 创建新的数据库实例
func NewDatabase() *Database {
	return &Database{
		todos:  make(map[int]models.Todo),
		nextID: 1,
	}
}

// GetAllTodos 获取所有待办事项
func (db *Database) GetAllTodos() ([]models.Todo, error) {
	db.mu.RLock()
	defer db.mu.RUnlock()

	todos := make([]models.Todo, 0, len(db.todos))
	for _, todo := range db.todos {
		todos = append(todos, todo)
	}
	return todos, nil
}

// GetTodoByID 根据ID获取待办事项
func (db *Database) GetTodoByID(id int) (*models.Todo, error) {
	db.mu.RLock()
	defer db.mu.RUnlock()

	todo, exists := db.todos[id]
	if !exists {
		return nil, fmt.Errorf("todo with id %d not found", id)
	}
	return &todo, nil
}

// CreateTodo 创建待办事项
func (db *Database) CreateTodo(todo models.TodoCreate) (*models.Todo, error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	newTodo := models.Todo{
		ID:          db.nextID,
		Title:       todo.Title,
		Description: todo.Description,
		Completed:   false,
		CreatedAt:   time.Now(),
		UpdatedAt:   time.Now(),
		DueDate:     todo.DueDate,
		Priority:    todo.Priority,
	}

	db.todos[db.nextID] = newTodo
	db.nextID++

	return &newTodo, nil
}

// UpdateTodo 更新待办事项
func (db *Database) UpdateTodo(id int, update models.TodoUpdate) (*models.Todo, error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	todo, exists := db.todos[id]
	if !exists {
		return nil, fmt.Errorf("todo with id %d not found", id)
	}

	// 更新字段(只更新提供的字段)
	if update.Title != nil {
		todo.Title = *update.Title
	}
	if update.Description != nil {
		todo.Description = *update.Description
	}
	if update.Completed != nil {
		todo.Completed = *update.Completed
	}
	if update.DueDate != nil {
		todo.DueDate = *update.DueDate
	}
	if update.Priority != nil {
		todo.Priority = *update.Priority
	}

	todo.UpdatedAt = time.Now()
	db.todos[id] = todo

	return &todo, nil
}

// DeleteTodo 删除待办事项
func (db *Database) DeleteTodo(id int) error {
	db.mu.Lock()
	defer db.mu.Unlock()

	if _, exists := db.todos[id]; !exists {
		return fmt.Errorf("todo with id %d not found", id)
	}

	delete(db.todos, id)
	return nil
}

// GetTodosByStatus 根据状态筛选
func (db *Database) GetTodosByStatus(completed bool) ([]models.Todo, error) {
	db.mu.RLock()
	defer db.mu.RUnlock()

	var filtered []models.Todo
	for _, todo := range db.todos {
		if todo.Completed == completed {
			filtered = append(filtered, todo)
		}
	}
	return filtered, nil
}

// SearchTodos 搜索待办事项
func (db *Database) SearchTodos(keyword string) ([]models.Todo, error) {
	db.mu.RLock()
	defer db.mu.RUnlock()

	var results []models.Todo
	for _, todo := range db.todos {
		if containsIgnoreCase(todo.Title, keyword) ||
			containsIgnoreCase(todo.Description, keyword) {
			results = append(results, todo)
		}
	}
	return results, nil
}

// SaveToFile 保存数据到文件
func (db *Database) SaveToFile(filename string) error {
	db.mu.RLock()
	defer db.mu.RUnlock()

	data := struct {
		Todos  map[int]models.Todo `json:"todos"`
		NextID int                 `json:"next_id"`
	}{
		Todos:  db.todos,
		NextID: db.nextID,
	}

	file, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filename, file, 0644)
}

// LoadFromFile 从文件加载数据
func (db *Database) LoadFromFile(filename string) error {
	db.mu.Lock()
	defer db.mu.Unlock()

	file, err := os.ReadFile(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // 文件不存在时返回nil
		}
		return err
	}

	var data struct {
		Todos  map[int]models.Todo `json:"todos"`
		NextID int                 `json:"next_id"`
	}

	if err := json.Unmarshal(file, &data); err != nil {
		return err
	}

	db.todos = data.Todos
	db.nextID = data.NextID
	return nil
}

// 辅助函数:不区分大小写包含检查
func containsIgnoreCase(s, substr string) bool {
	s = toLowerCase(s)
	substr = toLowerCase(substr)
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

func toLowerCase(s string) string {
	// 简化版,实际项目中应该使用 strings.ToLower
	bytes := []byte(s)
	for i := 0; i < len(bytes); i++ {
		if bytes[i] >= 'A' && bytes[i] <= 'Z' {
			bytes[i] += 32
		}
	}
	return string(bytes)
}

static/index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>待办事项管理系统</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .todo-form {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .todo-item {
            border: 1px solid #ddd;
            padding: 15px;
            margin-bottom: 10px;
            border-radius: 5px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .todo-item.completed {
            background-color: #e8f5e8;
            text-decoration: line-through;
        }
        button {
            padding: 8px 16px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
<h1>📝 待办事项管理系统</h1>

<div class="todo-form">
    <h3>添加新待办</h3>
    <input type="text" id="title" placeholder="标题" required>
    <input type="text" id="description" placeholder="描述">
    <button onclick="addTodo()">添加</button>
</div>

<h3>我的待办事项</h3>
<div id="todo-list"></div>

<script>
    const API_URL = 'http://localhost:8080/api/todos';

    // 获取所有待办
    async function loadTodos() {
        try {
            const response = await fetch(API_URL);
            const data = await response.json();
            displayTodos(data.data || []);
        } catch (error) {
            console.error('加载失败:', error);
        }
    }

    // 添加新待办
    async function addTodo() {
        const title = document.getElementById('title').value;
        const description = document.getElementById('description').value;

        if (!title) {
            alert('请输入标题');
            return;
        }

        try {
            await fetch(API_URL, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title, description, priority: 3 })
            });

            document.getElementById('title').value = '';
            document.getElementById('description').value = '';
            loadTodos();
        } catch (error) {
            console.error('添加失败:', error);
        }
    }

    // 切换完成状态
    async function toggleTodo(id) {
        try {
            await fetch(`${API_URL}/${id}/toggle`, {
                method: 'PATCH'
            });
            loadTodos();
        } catch (error) {
            console.error('更新失败:', error);
        }
    }

    // 删除待办
    async function deleteTodo(id) {
        if (!confirm('确定删除吗?')) return;

        try {
            await fetch(`${API_URL}/${id}`, {
                method: 'DELETE'
            });
            loadTodos();
        } catch (error) {
            console.error('删除失败:', error);
        }
    }

    // 显示待办列表
    function displayTodos(todos) {
        const container = document.getElementById('todo-list');
        container.innerHTML = '';

        todos.forEach(todo => {
            const todoElement = document.createElement('div');
            todoElement.className = `todo-item ${todo.completed ? 'completed' : ''}`;

            todoElement.innerHTML = `
                    <div>
                        <strong>${todo.title}</strong>
                        <p>${todo.description || ''}</p>
                        <small>优先级: ${todo.priority} | 创建: ${new Date(todo.created_at).toLocaleDateString()}</small>
                    </div>
                    <div>
                        <button onclick="toggleTodo(${todo.id})">
                            ${todo.completed ? '标记未完成' : '标记完成'}
                        </button>
                        <button onclick="deleteTodo(${todo.id})" style="background-color: #dc3545; margin-left: 8px;">
                            删除
                        </button>
                    </div>
                `;

            container.appendChild(todoElement);
        });
    }

    // 页面加载时获取数据
    window.onload = loadTodos;
</script>
</body>
</html>

x. 学习技巧

x. 指针字段

一句话记住

  • 普通类型(无*):零值 = 默认值 → 分不清 “没传” 和 “传了默认值”;

  • 指针类型(带*):零值 =nil → 用nil判断 “没传”,非nil判断 “传了(不管值是啥)”,完美解决可选字段更新的判断问题。

补充一个实战小细节(避坑)

在处理前端传参时,还要注意:

  • JSON 反序列化时,如果前端没传某个字段,指针字段会被设为 nil

  • 如果前端传了 null(比如 "title": null),指针字段也会是 nil(和 “没传” 等价);

  • 如果前端传了具体值(比如 "title": ""),指针字段会非 nil,指向这个空字符串。

这正好符合业务逻辑:只有前端明确传了非 null 的值,我们才更新该字段;没传 / 传 null 都不更新。

「指针和解引用」

todo.Title = *update.Title 需要的是 “东西”,不是 “门牌号”,所以必须用 * 从门牌号拿到东西,才能给 todo.Title

总结

  1. 普通类型的默认值导致无法区分 “用户没传” 和 “用户主动传了默认值”;

  2. 指针类型的nil 特性是解决这个问题的关键 —— 用 nil 判断 “不更新”,非 nil 判断 “要更新”;

  3. 这是 Go 处理「部分更新(PATCH)」的标准方案,你彻底吃透这个逻辑,后续写更新接口就不会踩 “误把默认值当用户输入” 的坑了。

type TodoUpdate struct {
	Title       *string    `json:"title,omitempty" validate:"omitempty,min=1,max=100"`
	Description *string    `json:"description,omitempty" validate:"omitempty,max=500"`
	Completed   *bool      `json:"completed,omitempty"`
	DueDate     *time.Time `json:"due_date,omitempty"`
	Priority    *int       `json:"priority,omitempty" validate:"omitempty,min=1,max=5"`
}


// UpdateTodo 更新待办事项
func (db *Database) UpdateTodo(id int, update models.TodoUpdate) (*models.Todo, error) {
	db.mu.Lock()
	defer db.mu.Unlock()

	todo, exists := db.todos[id]
	if !exists {
		return nil, fmt.Errorf("todo with id %d not found", id)
	}

	// 更新字段(只更新提供的字段)
	if update.Title != nil {
		todo.Title = *update.Title
	}
	if update.Description != nil {
		todo.Description = *update.Description
	}
	if update.Completed != nil {
		todo.Completed = *update.Completed
	}
	if update.DueDate != nil {
		todo.DueDate = *update.DueDate
	}
	if update.Priority != nil {
		todo.Priority = *update.Priority
	}

	todo.UpdatedAt = time.Now()
	db.todos[id] = todo

	return &todo, nil
}

写在最后