前言
基于Gin框架编写的Web API,实现简单的CRUD功能,数据存放在MongoDB,并设置Redis缓存。
代码需要简单的分模块组织。
go mod init buildginapp
代码参考自《Building Distributed Application in Gin》
定义数据模型
- 代码文件:
buildginapp/models/recipe.go
package models import ( "time" "go.mongodb.org/mongo-driver/bson/primitive" ) type Recipe struct { ID primitive.ObjectID `json:"id" bson:"_id"` Name string `json:"name" bson:"name"` Tags []string `json:"tags" bson:"tags"` Ingredients []string `json:"ingredients" bson:"ingredients"` Instructions []string `json:"instructions" bson:"instructions"` PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` }
设计API
http method | resource | description |
GET | /recipes |
返回一列recipe数据 |
POST | /recipes |
创建新食谱 |
PUT | /recipes/{id} |
更新一个已存在的食谱 |
DELETE | /recipes/{id} |
删除一个已存在的食谱 |
GET | /recipes/search?tag=X |
根据标签查询食谱 |
编写API方法
- 代码文件:
buildapp/handlers/handler.go
package handlers import ( "context" "encoding/json" "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" "github.com/go-redis/redis" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "buildginapp/models" ) type RecipesHandler struct { collection *mongo.Collection ctx context.Context redisClient *redis.Client } func NewRecipesHandler(ctx context.Context, collection *mongo.Collection, redisClient *redis.Client) *RecipesHandler { return &RecipesHandler{ collection: collection, ctx: ctx, redisClient: redisClient, } } // GET /recipes func (handler *RecipesHandler) ListRecipesHandler(c *gin.Context) { // 先查redis, 无则查MongoDB val, err := handler.redisClient.Get("recipes").Result() if err == redis.Nil { log.Println("Request to MongoDB") cur, err := handler.collection.Find(handler.ctx, bson.M{}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } defer cur.Close(handler.ctx) recipes := make([]models.Recipe, 0) for cur.Next(handler.ctx) { var recipe models.Recipe cur.Decode(&recipe) recipes = append(recipes, recipe) } // 将查询结果存到redis中, 过期时间为1小时 // 数据量很多的时候,这会是一个大Key,可能有一定的性能隐患 data, _ := json.Marshal(recipes) handler.redisClient.Set("recipes", string(data), 3600*time.Second) c.JSON(http.StatusOK, recipes) } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } else { // 如果从redis中查询到了,那么直接返回redis的查询结果 log.Println("Request to redis") recipes := make([]models.Recipe, 0) json.Unmarshal([]byte(val), &recipes) c.JSON(http.StatusOK, recipes) } } // POST /recipes func (handler *RecipesHandler) NewRecipeHandler(c *gin.Context) { var recipe models.Recipe if err := c.ShouldBindJSON(&recipe); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } recipe.ID = primitive.NewObjectID() recipe.PublishedAt = time.Now() _, err := handler.collection.InsertOne(handler.ctx, recipe) if err != nil { fmt.Println(err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Error while inserting a new recipe", }) return } log.Println("RRemove data from redis") handler.redisClient.Del("recipes") c.JSON(http.StatusOK, recipe) } // PUT /recipes/:id func (handler *RecipesHandler) UpdateRecipeHandler(c *gin.Context) { id := c.Param("id") var recipe models.Recipe if err := c.ShouldBindJSON(&recipe); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } objectId, _ := primitive.ObjectIDFromHex(id) _, err := handler.collection.UpdateOne(handler.ctx, bson.M{ "_id": objectId, }, bson.D{{"$set", bson.D{ {"name", recipe.Name}, {"instructions", recipe.Instructions}, {"ingredients", recipe.Ingredients}, {"tags", recipe.Tags}, }}}) if err != nil { fmt.Println(err) c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "update success", }) } // DELETE /recipes/:id func (handler *RecipesHandler) DeleteRecipeHandler(c *gin.Context) { id := c.Param("id") objectId, _ := primitive.ObjectIDFromHex(id) _, err := handler.collection.DeleteOne(handler.ctx, bson.M{ "_id": objectId, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "delete success", }) } // GET /recipes/:id func (handler *RecipesHandler) GetOneRecipeHandler(c *gin.Context) { id := c.Param("id") objectId, _ := primitive.ObjectIDFromHex(id) cur := handler.collection.FindOne(handler.ctx, bson.M{ "_id": objectId, }) var recipe models.Recipe err := cur.Decode(&recipe) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, recipe) }
main.go
- 代码文件:
buildginapp/main.go
package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "github.com/go-redis/redis" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" "buildginapp/handlers" ) var err error var client *mongo.Client var recipesHandler *handlers.RecipesHandler func init() { ctx := context.Background() // demo这个数据库需要先创建 var url string = "mongodb://root:123456@192.168.0.20:27017/demo?authSource=admin&maxPoolSize=20" client, err = mongo.Connect(ctx, options.Client().ApplyURI(url)) if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { log.Fatal("connect ot mongodb failed: ", err) } log.Println("Connected to MongoDB") collection := client.Database("demo").Collection("recipes") redisClient := redis.NewClient(&redis.Options{ Addr: "192.168.0.20:6379", Password: "", DB: 0, }) status := redisClient.Ping() log.Println("redis ping: ", status) recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient) } func main() { gin.SetMode(gin.ReleaseMode) router := gin.Default() router.GET("/recipes", recipesHandler.ListRecipesHandler) router.POST("/recipes", recipesHandler.NewRecipeHandler) router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) router.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) router.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) // 优雅关闭web服务 srv := &http.Server{ Addr: "127.0.0.1:8080", Handler: router, } go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal("listen failed, ", err) } }() defer func() { if err = client.Disconnect(context.TODO()); err != nil { panic(err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) <-quit ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("server shutdown failed, err: %v\n", err) } select { case <-ctx.Done(): log.Println("timeout of 2 seconds") } log.Println("server shutdown") }
参考
- https://www.mongodb.com/docs/drivers/go/current/quick-start/
- https://redis.uptrace.dev/guide/go-redis.html
附录
准备数据
package main import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" ) type Recipe struct { ID string `json:"id"` Name string `json:"name"` Tags []string `json:"tags"` Ingredients []string `json:"ingredients"` Instructions []string `json:"instructions"` PublishedAt time.Time `json:"publishedAt"` } // 保存recipes var recipes []Recipe var ctx context.Context var err error var client *mongo.Client func init() { recipes = make([]Recipe, 0) // 读取当前目录下的json文件 file, _ := ioutil.ReadFile("recipes.json") _ = json.Unmarshal([]byte(file), &recipes) ctx = context.Background() // demo这个数据库可能需要先创建 client, err = mongo.Connect(ctx, options.Client().ApplyURI("mongodb://root:123456@192.168.0.20:27017/demo?authSource=admin")) if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { log.Fatal("connect ot mongodb failed: ", err) } log.Println("Connected to MongoDB") var listOfRecipes []interface{} for _, recipe := range recipes { listOfRecipes = append(listOfRecipes, recipe) } collection := client.Database("demo").Collection("recipes") insertManyResult, err := collection.InsertMany(ctx, listOfRecipes) if err != nil { log.Fatal(err) } log.Println("Inserted recipes: ", len(insertManyResult.InsertedIDs)) } func main() { fmt.Println("insert many data to mongodb") }
- 也可以使用
mongoimport
将json数据直接插入到数据库中
mongoimport --username admin --password password --authenticationDatabase admin \ --db demo --collection recipes --file recipes.json --jsonArray
python测试
import requests import json def post_test(data): url = "http://127.0.0.1:8080/recipes" resp = requests.post(url=url, data=json.dumps(data)) # print("post test") print(resp.text) def get_test(): url = "http://127.0.0.1:8080/recipes" resp = requests.get(url) # print("get test") print(resp.text) def put_test(data, id): url = f"http://127.0.0.1:8080/recipes/{id}" # data["id"] = id resp = requests.put(url=url, data=json.dumps(data)) # print("put test") print(json.loads(resp.text)) def delete_test(id): url = f"http://127.0.0.1:8080/recipes/{id}" resp = requests.delete(url=url) print("delete test") print(resp.text) def get_test_search(id): url = f"http://127.0.0.1:8080/recipes/{id}" resp = requests.get(url=url) print("get test search") print(resp.text) if __name__ == "__main__": data1 = { "name": "Homemade Pizza", "tags": ["italian", "pizza", "dinner"], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated", ], "instructions": [ "Step 1.", "Step 2.", "Step 3.", ], } data2 = { "name": "西红柿炒鸡蛋", "tags": ["家常菜", "新手必会"], "ingredients": [ "2个鸡蛋", "1个番茄, 切片", "葱花, 蒜瓣等", ], "instructions": [ "步骤1", "步骤2", "步骤3", ], } data3 = { "name": "蒸蛋", "tags": ["家常菜", "新手必会"], "ingredients": [ "2个鸡蛋", "葱花, 蒜瓣等", ], "instructions": [ "步骤1", "步骤2", "步骤3", "步骤4", ], } # post_test(data1) # post_test(data2) # put_test(data2, id="62b7d298bb2ffa932f0d213d") # get_test_search(id="62b6e5746202e6a3c26b0afb") # delete_test(id="123456") get_test()