项目实战:构建 RESTful API
本章将综合前面所学知识,从零构建一个结构清晰、功能完整的 RESTful API 项目。
项目结构与分层设计
Go 项目分层架构
Go 社区推荐的目录结构遵循”关注点分离”原则:
myapp/
├── cmd/
│ └── api/
│ └── main.go # 程序入口
├── internal/ # 私有包,外部不可导入
│ ├── handler/ # HTTP 处理层(Controller)
│ ├── service/ # 业务逻辑层
│ ├── repository/ # 数据访问层(DAO)
│ ├── model/ # 数据模型
│ ├── middleware/ # 中间件
│ └── config/ # 配置管理
├── pkg/ # 可被外部导入的公共包
├── go.mod
├── go.sum
├── config.yaml # 配置文件
└── Makefile # 构建脚本internal/ 是 Go 的特殊目录,编译器会强制阻止外部包导入其中的代码。这确保了你的内部实现细节不会被其他项目依赖。
数据模型
// internal/model/todo.go
package model
import "time"
type Todo struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:200;not null"`
Completed bool `json:"completed" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}Repository 层 —— 数据访问
// internal/repository/todo.go
package repository
import (
"myapp/internal/model"
"gorm.io/gorm"
)
type TodoRepository struct {
db *gorm.DB
}
func NewTodoRepository(db *gorm.DB) *TodoRepository {
return &TodoRepository{db: db}
}
func (r *TodoRepository) Create(todo *model.Todo) error {
return r.db.Create(todo).Error
}
func (r *TodoRepository) GetAll() ([]model.Todo, error) {
var todos []model.Todo
err := r.db.Order("created_at DESC").Find(&todos).Error
return todos, err
}
func (r *TodoRepository) GetByID(id uint) (*model.Todo, error) {
var todo model.Todo
err := r.db.First(&todo, id).Error
return &todo, err
}
func (r *TodoRepository) Update(todo *model.Todo) error {
return r.db.Save(todo).Error
}
func (r *TodoRepository) Delete(id uint) error {
return r.db.Delete(&model.Todo{}, id).Error
}Service 层 —— 业务逻辑
// internal/service/todo.go
package service
import (
"errors"
"myapp/internal/model"
"myapp/internal/repository"
)
type TodoService struct {
repo *repository.TodoRepository
}
func NewTodoService(repo *repository.TodoRepository) *TodoService {
return &TodoService{repo: repo}
}
func (s *TodoService) Create(title string) (*model.Todo, error) {
if title == "" {
return nil, errors.New("标题不能为空")
}
todo := &model.Todo{Title: title}
if err := s.repo.Create(todo); err != nil {
return nil, err
}
return todo, nil
}
func (s *TodoService) GetAll() ([]model.Todo, error) {
return s.repo.GetAll()
}
func (s *TodoService) Toggle(id uint) (*model.Todo, error) {
todo, err := s.repo.GetByID(id)
if err != nil {
return nil, errors.New("Todo 不存在")
}
todo.Completed = !todo.Completed
s.repo.Update(todo)
return todo, nil
}
func (s *TodoService) Delete(id uint) error {
return s.repo.Delete(id)
}Handler 层 —— 请求处理
// internal/handler/todo.go
package handler
import (
"encoding/json"
"myapp/internal/model"
"myapp/internal/service"
"net/http"
"strconv"
)
type TodoHandler struct {
svc *service.TodoService
}
func NewTodoHandler(svc *service.TodoService) *TodoHandler {
return &TodoHandler{svc: svc}
}
type CreateRequest struct {
Title string `json:"title"`
}
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
todo, err := h.svc.Create(req.Title)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(todo)
}
func (h *TodoHandler) GetAll(w http.ResponseWriter, r *http.Request) {
todos, err := h.svc.GetAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(todos)
}
func (h *TodoHandler) Toggle(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseUint(r.PathValue("id"), 10, 64)
todo, err := h.svc.Toggle(uint(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(todo)
}
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseUint(r.PathValue("id"), 10, 64)
if err := h.svc.Delete(uint(id)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}路由注册
// cmd/api/main.go
func main() {
// ... 初始化 db、repo、svc、handler ...
mux := http.NewServeMux()
// 注册路由
mux.HandleFunc("POST /api/todos", todoHandler.Create)
mux.HandleFunc("GET /api/todos", todoHandler.GetAll)
mux.HandleFunc("PATCH /api/todos/{id}", todoHandler.Toggle)
mux.HandleFunc("DELETE /api/todos/{id}", todoHandler.Delete)
}优雅关闭
func main() {
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 启动服务器(非阻塞)
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("服务器错误: %v", err)
}
}()
// 监听中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("收到关闭信号,开始优雅关闭...")
// 给 5 秒超时处理完正在进行的请求
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("服务器强制关闭: %v", err)
}
log.Println("服务器已关闭")
}练习题
练习 1
基于本章的分层架构,为 Todo 项目添加一个”按标题搜索”的功能,要求贯穿 Repository → Service → Handler 三层。
参考答案 (2 个标签)
分层架构 RESTful API
Repository 层:
func (r *TodoRepository) Search(keyword string) ([]model.Todo, error) {
var todos []model.Todo
err := r.db.Where("title LIKE ?", "%"+keyword+"%").Order("created_at DESC").Find(&todos).Error
return todos, err
}Service 层:
func (s *TodoService) Search(keyword string) ([]model.Todo, error) {
if keyword == "" {
return s.repo.GetAll()
}
return s.repo.Search(keyword)
}Handler 层:
func (h *TodoHandler) Search(w http.ResponseWriter, r *http.Request) {
keyword := r.URL.Query().Get("q")
todos, err := h.svc.Search(keyword)
// ... 处理响应
}路由:
mux.HandleFunc("GET /api/todos/search", todoHandler.Search)