当前位置:首页 > 技术 > 正文内容

Gin 与 MongoDB 集成的简易 RESTful API 设计

访客 技术 2026年9月22日 12

项目初始化

使用 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)
}

服务支持优雅关闭,在接收到中断信号时会等待正在进行的请求完成后再退出。

标签: GinMongoDBRedis

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。