Gin 与 MongoDB 集成的简易 RESTful API 设计
项目初始化
使用 Go Modules 初始化项目:
go mod init apirecipe
本示例基于 Gin 框架构建 Web API,结合 MongoDB 存储数据,并引入 Redis 实现缓存机制,提升读取性能。
定义数据结构
文件路径:apirecipe/entity/recipe.go
package entity
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type CookingRecipe struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title string `json:"title" bson:"title"`
Categories []string `json:"categories" bson:"categories"`
Components []string `json:"components" bson:"components"`
Steps []string `json:"steps" bson:"steps"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
}
该结构体映射 MongoDB 文档字段,包含食谱标题、分类标签、所需材料、制作步骤及创建时间。
API 接口设计
| HTTP 方法 | 路径 | 说明 |
|---|---|---|
| GET | /recipes | 获取所有食谱列表 |
| POST | /recipes | 新增一个食谱 |
| PUT | /recipes/:id | 更新指定 ID 的食谱 |
| DELETE | /recipes/:id | 删除指定 ID 的食谱 |
| GET | /recipes/:id | 查询单个食谱详情 |
实现处理逻辑
文件路径:apirecipe/service/recipe_service.go
package service
import (
"context"
"encoding/json"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"apirecipe/entity"
)
type RecipeService struct {
dbCollection *mongo.Collection
appContext context.Context
cacheClient *redis.Client
}
func NewRecipeService(ctx context.Context, collection *mongo.Collection, rdb *redis.Client) *RecipeService {
return &RecipeService{
dbCollection: collection,
appContext: ctx,
cacheClient: rdb,
}
}
// GET /recipes - 获取全部食谱(优先从缓存读取)
func (s *RecipeService) FetchAll(c *gin.Context) {
val, err := s.cacheClient.Get(s.appContext, "all_recipes").Result()
if err == redis.Nil {
log.Println("Cache miss: querying database")
cursor, e := s.dbCollection.Find(s.appContext, bson.M{})
if e != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": e.Error()})
return
}
defer cursor.Close(s.appContext)
var recipes []entity.CookingRecipe
for cursor.Next(s.appContext) {
var item entity.CookingRecipe
_ = cursor.Decode(&item)
recipes = append(recipes, item)
}
data, _ := json.Marshal(recipes)
s.cacheClient.Set(s.appContext, "all_recipes", data, 1*time.Hour)
c.JSON(http.StatusOK, recipes)
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
} else {
log.Println("Cache hit: serving from Redis")
var recipes []entity.CookingRecipe
json.Unmarshal([]byte(val), &recipes)
c.JSON(http.StatusOK, recipes)
}
}
// POST /recipes - 创建新食谱
func (s *RecipeService) CreateOne(c *gin.Context) {
var recipe entity.CookingRecipe
if err := c.ShouldBindJSON(&recipe); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
recipe.ID = primitive.NewObjectID()
recipe.CreatedAt = time.Now()
_, err := s.dbCollection.InsertOne(s.appContext, recipe)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert recipe"})
return
}
// 清除缓存
s.cacheClient.Del(s.appContext, "all_recipes")
c.JSON(http.StatusCreated, recipe)
}
// PUT /recipes/:id - 更新现有食谱
func (s *RecipeService) ModifyOne(c *gin.Context) {
id := c.Param("id")
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
var updated entity.CookingRecipe
if err := c.ShouldBindJSON(&updated); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
update := bson.M{
"$set": bson.M{
"title": updated.Title,
"components": updated.Components,
"steps": updated.Steps,
"categories": updated.Categories,
},
}
_, e := s.dbCollection.UpdateOne(s.appContext, bson.M{"_id": objectID}, update)
if e != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": e.Error()})
return
}
s.cacheClient.Del(s.appContext, "all_recipes")
c.JSON(http.StatusOK, gin.H{"message": "update successful"})
}
// DELETE /recipes/:id - 删除食谱
func (s *RecipeService) RemoveOne(c *gin.Context) {
id := c.Param("id")
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
_, e := s.dbCollection.DeleteOne(s.appContext, bson.M{"_id": objectID})
if e != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": e.Error()})
return
}
s.cacheClient.Del(s.appContext, "all_recipes")
c.JSON(http.StatusOK, gin.H{"message": "deletion successful"})
}
// GET /recipes/:id - 查询单个食谱
func (s *RecipeService) FindByID(c *gin.Context) {
id := c.Param("id")
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
var recipe entity.CookingRecipe
err = s.dbCollection.FindOne(s.appContext, bson.M{"_id": objectID}).Decode(&recipe)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Recipe not found"})
return
}
c.JSON(http.StatusOK, recipe)
}
主程序入口
文件路径:apirecipe/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"apirecipe/service"
)
var dbClient *mongo.Client
var recipeSvc *service.RecipeService
func setupDatabase() {
ctx := context.Background()
url := "mongodb://root:123456@192.168.0.20:27017/demo?authSource=admin"
client, err := mongo.Connect(ctx, options.Client().ApplyURI(url))
if err != nil {
log.Fatal("MongoDB connection failed:", err)
}
if err = client.Ping(ctx, readpref.Primary()); err != nil {
log.Fatal("Cannot reach MongoDB:", err)
}
log.Println("Connected to MongoDB")
collection := client.Database("demo").Collection("recipes")
rdb := redis.NewClient(&redis.Options{
Addr: "192.168.0.20:6379",
DB: 0,
})
if _, pingErr := rdb.Ping(ctx).Result(); pingErr != nil {
log.Fatal("Redis connection failed:", pingErr)
}
log.Println("Connected to Redis")
recipeSvc = service.NewRecipeService(ctx, collection, rdb)
}
func gracefulShutdown(server *http.Server) {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
<-interrupt
log.Println("Shutting down server...")
timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(timeoutCtx); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
select {
case <-timeoutCtx.Done():
if timeoutCtx.Err() == context.DeadlineExceeded {
log.Println("Graceful shutdown timed out")
}
}
log.Println("Server stopped")
}
func main() {
gin.SetMode(gin.ReleaseMode)
setupDatabase()
defer func() {
if err := dbClient.Disconnect(context.TODO()); err != nil {
panic(err)
}
}()
router := gin.Default()
router.GET("/recipes", recipeSvc.FetchAll)
router.POST("/recipes", recipeSvc.CreateOne)
router.PUT("/recipes/:id", recipeSvc.ModifyOne)
router.DELETE("/recipes/:id", recipeSvc.RemoveOne)
router.GET("/recipes/:id", recipeSvc.FindByID)
server := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server start error: %v", err)
}
}()
gracefulShutdown(server)
}
服务支持优雅关闭,在接收到中断信号时会等待正在进行的请求完成后再退出。