写在最前
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("💾 数据已自动保存")
}
}menu.go
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。
总结
普通类型的默认值导致无法区分 “用户没传” 和 “用户主动传了默认值”;
指针类型的nil 特性是解决这个问题的关键 —— 用 nil 判断 “不更新”,非 nil 判断 “要更新”;
这是 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
}
评论