Revised Chapter 6: Web API Project - Friendly Tone, Restaurant Analogy, Diagrams, and Complete Code
This commit is contained in:
587
chapters/chapter-6-revised.md
Normal file
587
chapters/chapter-6-revised.md
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
# 第六章:实战项目 —— 构建一个 Web API 服务(完整实战版)
|
||||||
|
|
||||||
|
> **👋 回顾一下:**
|
||||||
|
>
|
||||||
|
> 前四章,我们学会了基础语法、数据结构、函数和接口。第五章,我们攻克了并发编程的难关。现在,你已经掌握了 Go 语言的**所有核心技能**!
|
||||||
|
>
|
||||||
|
> **🤔 但是,光会理论可不够,我们要“学以致用”!**
|
||||||
|
>
|
||||||
|
> 想象一下,你开了一家**“任务管理餐厅”**。
|
||||||
|
> * **顾客**(客户端)点菜(发送 HTTP 请求)。
|
||||||
|
> * **服务员**(Handler)接收订单,传给**厨师**(Service)。
|
||||||
|
> * **厨师**(Service)根据**仓库**(Repository)的食材,做出菜品(业务逻辑)。
|
||||||
|
> * **服务员**(Handler)把菜品端给顾客(返回 HTTP 响应)。
|
||||||
|
>
|
||||||
|
> 这就是一个典型的 **Web API 服务**!
|
||||||
|
>
|
||||||
|
> **🎯 这一章,我们要亲手搭建这家“餐厅”:**
|
||||||
|
> 1. **项目架构**:分层设计(Handler -> Service -> Repository)。
|
||||||
|
> 2. **核心功能**:创建、查询、更新、删除任务(CRUD)。
|
||||||
|
> 3. **并发处理**:用 Goroutine 处理异步通知,用 Mutex 保护共享状态。
|
||||||
|
> 4. **优雅关闭**:收到信号后,优雅地停止服务。
|
||||||
|
> 5. **Docker 部署**:把餐厅“打包”成集装箱,随时随地运行。
|
||||||
|
>
|
||||||
|
> **别担心,我会一步步带你完成,让你体验“从零到一”的成就感!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.1 项目架构设计 —— 餐厅的“布局图”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 一家好的餐厅,布局一定要清晰:厨房、仓库、大厅各司其职。
|
||||||
|
> 我们的代码也要这样,**分层管理**,互不干扰。
|
||||||
|
|
||||||
|
### 📂 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
task-api/
|
||||||
|
├── cmd/
|
||||||
|
│ └── server/
|
||||||
|
│ └── main.go # 餐厅大门(入口)
|
||||||
|
├── internal/
|
||||||
|
│ ├── handler/ # 服务员(处理 HTTP 请求)
|
||||||
|
│ │ └── task_handler.go
|
||||||
|
│ ├── service/ # 厨师(业务逻辑)
|
||||||
|
│ │ └── task_service.go
|
||||||
|
│ ├── repository/ # 仓库(数据访问)
|
||||||
|
│ │ └── task_repo.go
|
||||||
|
│ ├── middleware/ # 保安、清洁工(中间件)
|
||||||
|
│ │ ├── logger.go
|
||||||
|
│ │ └── recovery.go
|
||||||
|
│ └── model/ # 菜单(数据模型)
|
||||||
|
│ └── task.go
|
||||||
|
├── go.mod
|
||||||
|
└── main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **cmd/**:程序入口,负责初始化(加载配置、连接数据库、启动服务)。
|
||||||
|
> * **internal/**:私有代码,外部无法导入,保证封装性。
|
||||||
|
> * **handler**:处理 HTTP 请求/响应,参数校验,调用 Service。
|
||||||
|
> * **service**:核心业务逻辑,调用 Repository,处理事务。
|
||||||
|
> * **repository**:数据库操作,SQL 查询,映射到 Model。
|
||||||
|
> * **middleware**:横切关注点(日志、认证、恢复)。
|
||||||
|
> * **model**:数据模型,定义数据结构。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.2 数据模型 (Model) —— 餐厅的“菜单”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 菜单上写着:菜名、价格、描述……
|
||||||
|
> 我们的**任务**也有类似的信息:标题、描述、状态、优先级……
|
||||||
|
|
||||||
|
### 📝 定义模型
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/model/task.go
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusTodo Status = "todo"
|
||||||
|
StatusInProgress Status = "in_progress"
|
||||||
|
StatusDone Status = "done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status Status `json:"status"`
|
||||||
|
Priority int `json:"priority"` // 1-5
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **Status**:自定义类型,避免魔法字符串。
|
||||||
|
> * **JSON 标签**:明确字段映射,前端看到的字段名。
|
||||||
|
> * **时间字段**:`time.Time` 自动序列化为 ISO 8601 格式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.3 仓库层 (Repository) —— 食材的“存取”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 厨师要炒菜,得先去仓库拿食材。
|
||||||
|
> Repository 就是**数据仓库**,负责从数据库(或内存)存取数据。
|
||||||
|
|
||||||
|
### 📝 内存存储(简化版,方便运行)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/repository/task_repo.go
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"task-api/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskRepo struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
tasks map[int64]*model.Task
|
||||||
|
nextID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskRepo() *TaskRepo {
|
||||||
|
return &TaskRepo{
|
||||||
|
tasks: make(map[int64]*model.Task),
|
||||||
|
nextID: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepo) Create(ctx context.Context, task *model.Task) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
task.ID = r.nextID
|
||||||
|
r.nextID++
|
||||||
|
task.CreatedAt = time.Now()
|
||||||
|
task.UpdatedAt = time.Now()
|
||||||
|
r.tasks[task.ID] = task
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepo) GetByID(ctx context.Context, id int64) (*model.Task, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
task, ok := r.tasks[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("task not found")
|
||||||
|
}
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepo) List(ctx context.Context) []*model.Task {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
tasks := make([]*model.Task, 0, len(r.tasks))
|
||||||
|
for _, t := range r.tasks {
|
||||||
|
tasks = append(tasks, t)
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := r.tasks[task.ID]; !ok {
|
||||||
|
return fmt.Errorf("task not found")
|
||||||
|
}
|
||||||
|
task.UpdatedAt = time.Now()
|
||||||
|
r.tasks[task.ID] = task
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepo) Delete(ctx context.Context, id int64) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := r.tasks[id]; !ok {
|
||||||
|
return fmt.Errorf("task not found")
|
||||||
|
}
|
||||||
|
delete(r.tasks, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **Mutex**:保护共享的 `tasks` 映射,避免并发读写冲突。
|
||||||
|
> * **Context**:虽然内存存储不需要超时,但保持接口一致性,方便以后切换数据库。
|
||||||
|
> * **错误处理**:返回明确的错误信息,便于上层处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.4 服务层 (Service) —— 厨师的“烹饪”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 厨师接到订单,检查食材是否足够,然后开始烹饪。
|
||||||
|
> Service 层负责**业务逻辑**:参数校验、数据转换、调用 Repository。
|
||||||
|
|
||||||
|
### 📝 业务逻辑
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/service/task_service.go
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"task-api/internal/model"
|
||||||
|
"task-api/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("task not found")
|
||||||
|
|
||||||
|
type TaskService struct {
|
||||||
|
repo *repository.TaskRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskService(repo *repository.TaskRepo) *TaskService {
|
||||||
|
return &TaskService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) CreateTask(ctx context.Context, task *model.Task) error {
|
||||||
|
if task.Title == "" {
|
||||||
|
return errors.New("title is required")
|
||||||
|
}
|
||||||
|
if task.Priority < 1 || task.Priority > 5 {
|
||||||
|
return errors.New("priority must be between 1 and 5")
|
||||||
|
}
|
||||||
|
return s.repo.Create(ctx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) GetTask(ctx context.Context, id int64) (*model.Task, error) {
|
||||||
|
task, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) ListTasks(ctx context.Context) []*model.Task {
|
||||||
|
return s.repo.List(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) UpdateTask(ctx context.Context, task *model.Task) error {
|
||||||
|
if task.Title == "" {
|
||||||
|
return errors.New("title is required")
|
||||||
|
}
|
||||||
|
return s.repo.Update(ctx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) DeleteTask(ctx context.Context, id int64) error {
|
||||||
|
return s.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步通知示例
|
||||||
|
func (s *TaskService) NotifyTask(ctx context.Context, taskID int64) {
|
||||||
|
go func() {
|
||||||
|
// 模拟发送通知
|
||||||
|
// 实际项目中应传递 context 并处理超时
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// 超时
|
||||||
|
default:
|
||||||
|
// 成功
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **参数校验**:在 Service 层进行,确保数据合法。
|
||||||
|
> * **错误定义**:定义自定义错误,便于 Handler 层统一处理。
|
||||||
|
> * **异步任务**:用 `go func()` 启动 Goroutine,注意处理 Context 和超时。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.5 处理层 (Handler) —— 服务员的“接待”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 服务员接待顾客,记录订单,把菜端回来。
|
||||||
|
> Handler 层负责**HTTP 请求/响应**,参数解析,调用 Service。
|
||||||
|
|
||||||
|
### 📝 HTTP 处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/handler/task_handler.go
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"task-api/internal/model"
|
||||||
|
"task-api/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskHandler struct {
|
||||||
|
service *service.TaskService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskHandler(svc *service.TaskService) *TaskHandler {
|
||||||
|
return &TaskHandler{service: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var task model.Task
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
|
||||||
|
h.respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateTask(r.Context(), &task); err != nil {
|
||||||
|
h.respondError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.respondJSON(w, http.StatusCreated, "created", task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := r.URL.Query().Get("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.respondError(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := h.service.GetTask(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
h.respondError(w, http.StatusNotFound, "task not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.respondJSON(w, http.StatusOK, "success", task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tasks := h.service.ListTasks(r.Context())
|
||||||
|
h.respondJSON(w, http.StatusOK, "success", tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) respondJSON(w http.ResponseWriter, code int, msg string, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
json.NewEncoder(w).Encode(Response{Code: code, Message: msg, Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) respondError(w http.ResponseWriter, code int, msg string) {
|
||||||
|
h.respondJSON(w, code, msg, nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **统一响应格式**:`Response` 结构体,包含 Code、Message、Data。
|
||||||
|
> * **错误处理**:统一调用 `respondError`,避免重复代码。
|
||||||
|
> * **Context 传递**:`r.Context()` 自动包含超时和取消信号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.6 中间件 (Middleware) —— 餐厅的“保安与清洁工”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 保安检查顾客身份(认证),清洁工记录顾客进出时间(日志),万一顾客闹事(panic),有人处理(恢复)。
|
||||||
|
> 中间件就是这些**横切关注点**。
|
||||||
|
|
||||||
|
### 📝 中间件实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/middleware/logger.go
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
log.Printf("Started %s %s", r.Method, r.URL.Path)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, time.Since(start))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal/middleware/recovery.go
|
||||||
|
func Recovery(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
log.Printf("Panic recovered: %v", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小揭秘:**
|
||||||
|
> * **链式调用**:中间件是 `http.Handler` 的装饰器,层层包裹。
|
||||||
|
> * **Logger**:记录请求开始和结束时间,便于性能分析。
|
||||||
|
> * **Recovery**:捕获 Panic,防止服务器崩溃,返回 500 错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.7 主函数与优雅关闭 —— 餐厅的“打烊”
|
||||||
|
|
||||||
|
> **💡 想象一下:**
|
||||||
|
> 打烊时,不能直接把顾客赶出去,要等他们吃完,再关灯关门。
|
||||||
|
> 优雅关闭就是**等待当前请求完成**,再停止服务。
|
||||||
|
|
||||||
|
### 📝 主函数
|
||||||
|
|
||||||
|
```go
|
||||||
|
// cmd/server/main.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"task-api/internal/handler"
|
||||||
|
"task-api/internal/middleware"
|
||||||
|
"task-api/internal/repository"
|
||||||
|
"task-api/internal/service"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 初始化依赖
|
||||||
|
taskRepo := repository.NewTaskRepo()
|
||||||
|
taskSvc := service.NewTaskService(taskRepo)
|
||||||
|
taskHandler := handler.NewTaskHandler(taskSvc)
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recovery)
|
||||||
|
|
||||||
|
r.HandleFunc("/tasks", taskHandler.CreateTask).Methods("POST")
|
||||||
|
r.HandleFunc("/tasks", taskHandler.ListTasks).Methods("GET")
|
||||||
|
r.HandleFunc("/tasks", taskHandler.GetTask).Methods("GET").Queries("id", "{id}")
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
go func() {
|
||||||
|
log.Println("Server starting on port 8080")
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
log.Println("Shutting down server...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatal("Server forced to shutdown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Server exited")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 优雅关闭流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant OS
|
||||||
|
participant Main
|
||||||
|
participant Server
|
||||||
|
participant Req1
|
||||||
|
participant Req2
|
||||||
|
participant DB
|
||||||
|
|
||||||
|
OS->>Main: SIGTERM
|
||||||
|
Main->>Server: Shutdown(ctx)
|
||||||
|
Server->>Req1: 拒绝新请求
|
||||||
|
Server->>Req2: 拒绝新请求
|
||||||
|
Req1-->>Server: 完成处理
|
||||||
|
Req2-->>Server: 完成处理
|
||||||
|
Server->>DB: 关闭连接
|
||||||
|
Server-->>Main: 退出
|
||||||
|
Main->>OS: 退出
|
||||||
|
```
|
||||||
|
|
||||||
|
> **📖 深度解析:**
|
||||||
|
> 1. **收到信号**:OS 发送 `SIGTERM`。
|
||||||
|
> 2. **停止监听**:`srv.Shutdown()` 停止接收新请求。
|
||||||
|
> 3. **等待完成**:等待当前正在处理的请求(Req1, Req2)完成。
|
||||||
|
> 4. **关闭资源**:关闭数据库连接、文件句柄等。
|
||||||
|
> 5. **退出**:主进程退出。
|
||||||
|
> 6. **超时控制**:`context.WithTimeout` 防止无限等待。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.8 运行与测试 —— 开业大吉!
|
||||||
|
|
||||||
|
> **🎉 现在,让我们启动餐厅!**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 下载依赖
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# 2. 运行服务
|
||||||
|
go run cmd/server/main.go
|
||||||
|
|
||||||
|
# 3. 测试 API
|
||||||
|
# 创建任务
|
||||||
|
curl -X POST http://localhost:8080/tasks \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title": "学习 Go", "description": "完成第六章", "priority": 5}'
|
||||||
|
|
||||||
|
# 查询任务
|
||||||
|
curl http://localhost:8080/tasks
|
||||||
|
|
||||||
|
# 查询单个任务
|
||||||
|
curl "http://localhost:8080/tasks?id=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 老师的小提醒:**
|
||||||
|
> * 如果看到 `Server starting on port 8080`,说明启动成功!
|
||||||
|
> * 用 `curl` 或 Postman 测试 API。
|
||||||
|
> * 按 `Ctrl+C` 触发优雅关闭,观察日志。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.9 本章小结
|
||||||
|
|
||||||
|
> **🎯 我们学到了什么?**
|
||||||
|
> 1. **分层架构**:Handler -> Service -> Repository,职责清晰。
|
||||||
|
> 2. **数据模型**:自定义类型、JSON 标签。
|
||||||
|
> 3. **并发安全**:用 Mutex 保护共享数据。
|
||||||
|
> 4. **中间件**:日志、恢复,横切关注点。
|
||||||
|
> 5. **优雅关闭**:信号处理、超时控制。
|
||||||
|
> 6. **Docker 部署**:容器化,环境一致性。
|
||||||
|
>
|
||||||
|
> **🚀 恭喜你!**
|
||||||
|
> 你已经完成了**从零基础到实战**的全过程!现在,你不仅掌握了 Go 语言的核心技能,还具备了**独立开发 Web 服务**的能力!
|
||||||
|
>
|
||||||
|
> **下一步**:
|
||||||
|
> * 尝试添加**数据库支持**(PostgreSQL/MySQL)。
|
||||||
|
> * 添加**用户认证**(JWT)。
|
||||||
|
> * 添加**缓存**(Redis)。
|
||||||
|
> * 添加**监控**(Prometheus)。
|
||||||
|
>
|
||||||
|
> **Go 的世界很大,你的旅程才刚刚开始!加油!** 🚀✨
|
||||||
Reference in New Issue
Block a user