Published on

เรียนรู้ Go - ทำ Todo API ต่อกับ MongoDB

เรียนรู้ Go - ทำ Todo API ต่อกับ MongoDB
เรียนรู้ Go - ทำ Todo API ต่อกับ MongoDB

อันยองงง วันนี้เราจะมาเขียน Go ต่อยอดจากบทความก่อน ๆ คือเดี๋ยววันนี้เจมส์จะเขียนให้ Connect กับ MongoDB database แล้วก็ปรับให้ API ต่าง ๆ ที่ตอนแรกข้อมูลเรียกจาก memory ให้ไปเรียกจาก MongoDB แทนกันครับ

ในส่วนนี้เจมส์จะต่อยอดจากบทความก่อนหน้านี้เน้อครับ เรียนรู้ Go - compile อัตโนมัติ ด้วย Air บน Docker

มาเริ่มกันเลย

สร้าง MongoDB ใน docker-compose.yaml

ขั้นแรกเดี๋ยวเราจะมาสร้าง MongoDB ใน docker-compose.yaml กันก่อนเลยครับ โดยเราจะปรับ docker-compose.yaml เป็นแบบนี้ครับ

docker-compose.yaml
version: "3.8"
services:
  database:
    image: mongo:8.0.0-rc9
    container_name: db.simple-todo
    restart: always
    ports:
      - 27017:27017
    env_file: .env
    volumes:
      - ./database/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh
      - ./resources/db:/data/db

จากนั้นเราจะสร้าง directory ชื่อ database ขึ้นมาและสร้างไฟล์ในนั้นชื่อ init-mongo.sh และใส่โค้ดดังนี้ครับ

database/init-mongo.sh
mongosh -- "$MONGO_INITDB_DATABASE" <<EOF
    var rootUser = '$MONGO_INITDB_ROOT_USERNAME';
    var rootPassword = '$MONGO_INITDB_ROOT_PASSWORD';
    var admin = db.getSiblingDB('admin');
    admin.auth(rootUser, rootPassword);
    var user = '$MONGO_INITDB_USERNAME';
    var passwd = '$MONGO_INITDB_PASSWORD';
    db.createUser({ user: user, pwd: passwd, roles: ["readWrite"] });
EOF

จากนั้นให้สร้างไฟล์ .env ที่ root project ครับ และใส่โค้ดดังนี้ เพื่อตั้งค่า MongoDB

.env
MONGO_INITDB_DATABASE=simple-todo
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=123456
MONGO_INITDB_USERNAME=root
MONGO_INITDB_PASSWORD=123456

MONGO_INITDB_DATABASE คือชื่อ database MONGO_INITDB_USERNAME คือชื่อ username (สำหรับเข้า database) MONGO_INITDB_PASSWORD คือชื่อ password (สำหรับเข้า database)

ในตอนนี้หากเราสั่ง docker-compose up ก็จะสามารถสร้าง MongoDB ขึ้นมาได้แล้ว

เอา service api ใส่ใน docker-compose.yaml

ขั้นต่อไปเดี๋ยวเราจะเอา service api มาใส่ใน docker-compose.yaml กันครับ โดยโค้ดของ docker-compose.yaml จะเป็นดังนี้ครับ

docker-compose.yaml
version: "3.8"
services:
  database:
    image: mongo:8.0.0-rc9
    container_name: db.simple-todo
    restart: always
    ports:
      - 27017:27017
    env_file: .env
    volumes:
      - ./database/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh
      - ./resources/db:/data/db

  api:
    container_name: api.simple-todo
    ports:
      - 2000:2000
    build:
      context: ./
      dockerfile: dev.Dockerfile
    env_file: .env
    environment:
      - PORT=2000
    volumes:
      - ./:/app
    depends_on:
      - database

ซึ่งถ้าใครทำมาจากบทความก่อนหน้านี้จะมีไฟล์ dev.Dockerfile อยู่แล้ว แต่ถ้าใครพึ่งมาอ่านบทความนี้ โค้ดในส่วน dev.Dockerfileจะเป็นดังนี้ครับ

dev.Dockerfile
FROM golang:1.22-alpine

WORKDIR /app

RUN go install github.com/air-verse/air@latest

COPY go.mod go.sum ./
RUN go mod download

CMD ["air"]

หากคุณผู้อ่านไม่ได้อ่านบทความก่อน ๆ มา มาอ่านบทความนี้เลย สิ่งที่ต้องทำเพิ่มคือ init project go โดยใช้คำสั่ง go mod init simple-todo และ Install gin web framework โดยใช้คำสั่ง go get -u github.com/gin-gonic/gin

ลองทำ GET /todos โดยดึงข้อมูลจาก MongoDB

ในขั้นตอนต่อไปเดี๋ยวเรามาทำให้ในส่วน GET /todos โดยจะเรียกข้อมูลจากใน MongoDB กันครับ

ขั้นแรกให้เราติดตั้ง MongoDB Go Driver ก่อนครับ โดยใช้คำสั่ง

go get go.mongodb.org/mongo-driver/mongo

จากนั้นให้เราปรับไฟล์ .env โดยเพิ่ม config ด้านล่างเข้าไป

.env
MONGO_SERVER=mongodb://root:123456@database:27017/simple-todo

ขั้นต่อมาให้สร้าง directory ชื่อ config ขึ้นมาครับ จากนั้นให้สร้างไฟล์ชื่อ config.go ใน directory นั้นและใส่โค้ดดังนี้ครับ

config/config.go
package config

import (
	"context"
	"log"
	"os"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

var Client *mongo.Client = CreateMongoClient()

func CreateMongoClient() *mongo.Client {
	mongoURI := os.Getenv("MONGO_SERVER")

	if mongoURI == "" {
		log.Fatal("[ConnectDB] - Error: MONGO_SERVER is not set in .env file")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
	if err != nil {
		log.Fatal("[ConnectDB] - Error: Connection error!")
	}

	// ตรวจสอบการเชื่อมต่อด้วย Ping
	err = client.Ping(ctx, nil)
	if err != nil {
		log.Fatal("[ConnectDB] - Error: Ping error!")
	}

	log.Println("[ConnectDB] - DEBUG: Connected to MongoDB!")

	return client
}

func OpenCollection(client *mongo.Client, collectionName string) *mongo.Collection {
	var database = os.Getenv("MONGO_INITDB_DATABASE")

	return client.Database(database).Collection(collectionName)
}

ซึ่งจะเป็น config เกี่ยวกับการ connect database และเกี่ยวกับ เรียกใช้ collection

จากนั้นเดี๋ยวเราจะมาสร้าง models ครับ โดยเราจะเอา Todo struct ที่เราเคยสร้างไว้ในบทความแรกมาแก้ไขนิดหน่อยครับ

เราจะเพิ่ม ในส่วนที่เป็น bson เข้าไป หรือใครที่พึ่งเข้ามาอ่านบทความนี้ ก็สามารถ copy code ด้านล่างไปใช้ได้เลยครับ

เดี๋ยวเราจะมาสร้าง directory ชื่อ models และสร้าง models.go ด้านใน โดยใส่โค้ดลงไปดังนี้ครับ

models/models.go
package models

import "go.mongodb.org/mongo-driver/bson/primitive"

type Todo struct {
	Id     primitive.ObjectID `bson:"_id"`
	Task   string             `json:"task" binding:"required" bson:"task"`
	Status string             `json:"status" bson:"status"`
}

เราจะมาแก้ไขไฟล์ main.go ใหม่เป็นดังนี้ครับ

main.go
package main

import (
	"net/http"
	"simple-todo/controllers"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/mongo"
)

var Client *mongo.Client

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"message": "Server is running",
		})
	})

	r.GET("/todos", controllers.GetTodos)
	r.Run(":2000")
}

จากนั้นเราจะมาสร้าง directory ชื่อ controllers และสร้างไฟล์ชื่อ todoController.go โดยใส่โค้ดลงไปดังนี้ครับ

controllers/todoController.go
package controllers

import (
	"context"
	"net/http"
	"simple-todo/config"
	"simple-todo/models"
	"time"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
)

var todoCollection *mongo.Collection = config.OpenCollection(config.Client, "todos")

func GetTodos(c *gin.Context) {
	// สร้าง context สำหรับการดึงข้อมูล
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cursor, err := todoCollection.Find(ctx, bson.M{})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"FindError": err.Error()})
		return
	}
	defer cursor.Close(ctx)

	var todos []models.Todo
	for cursor.Next(ctx) {
		var todo models.Todo

		err := cursor.Decode(&todo)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"Decode Error": err.Error()})
			return
		}

		todos = append(todos, todo)
	}

	if todos == nil {
		todos = []models.Todo{}
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todos":   todos,
	})
}

คือเราจะให้ GET /todos เรียก function GetTodos ใน todoController.go ซึ่งใน GetTods เราจะ find ขึ้นมูลใน todoCollection แล้ว return กลับไปครับ

เมื่อคุณผู้อ่านลอง GET ไปที่ http://localhost:2000/todos จะพบว่าได้ return ดังนี้

{
  "success": true,
  "todos": []
}

ลองทำ POST /todos ให้เพิ่มข้อมูลลงใน MongoDB

ในขั้นต่อมา เดี๋ยวเรามาทำในส่วนของ POST /todos กันบ้างครับ โดยจะทำให้เพิ่มข้อมูล แล้ว insert ข้อมูลใส่ใน MongoDB ครับ

โดยใน main.go เราจะเพิ่ม route สำหรับ POST todos โดยโค้ดใหม่จะเป็นดังโค้ดด้านล่างนี้ครับ

main.go
r.POST("/todos", controllers.CreateTodo)

จากนั้นเราจะเพิ่ม function CreateTodo เข้ามาใน todoController.go ครับ โดยจะเพิ่มโค้ดด้านล่างเข้าไปครับ

controllers/todoController.go
func CreateTodo(c *gin.Context) {
	var todo models.Todo

	// ผูกข้อมูลจาก request body กับ struct User
	if err := c.ShouldBindJSON(&todo); err != nil {
		// ถ้ามีข้อผิดพลาดในการผูกข้อมูล
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	todo.Id = primitive.NewObjectID()
	todo.Status = "todo"

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	_, err := todoCollection.InsertOne(ctx, todo)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"success": true,
		"todo":    todo,
	})
}

ด้านบนต้อง import "go.mongodb.org/mongo-driver/bson/primitive" เพิ่มเข้ามาด้วยเน้อครับ

ในส่วน CreateTodo เราจะรับ params ชื่อ task เข้ามาเพื่อให้เพิ่มงานนั้น ๆ ลงไป โดยเราจะกำหนด status เริ่มต้นให้เป็น todo ซึ่งเมื่อ insert ค่า todo เรียบร้อยแล้วเราจะ return ข้อมูล todo สุดที่เพิ่ม return กลับออกไป

เราลองมายิง POST ไปที่ http://localhost:2000/todos โดยส่ง task ไป จะพบว่าเมื่อเรายิง post request เรียบร้อย จะได้ response success และค่า todo ตามที่เราได้สร้าง ซึ่งเมื่อไปดูใน MongoDB ของเรา จะพบว่ามีข้อมูลอยู่ใน MongoDB เรียบร้อยแล้ว

ลองทำ GET /todos/:id ดึงข้อมูลจาก id

เพื่อไม่เป็นการเสียเวลาเรามาต่อกันที่ การดึงข้อมูล todo ด้วย id กันครับ

เราจะเพิ่ม route ใน main.go ต่อครับ

main.go
r.GET("/todos/:id", controllers.GetTodoById)

จากนั้นเราจะเพิ่ม function GetTodoById ใน todoController.go โดยจะเพิ่ม

controllers/todoController.go
func GetTodoById(c *gin.Context) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	var todo models.Todo
	err := todoCollection.FindOne(ctx, bson.M{"_id": objId}).Decode(&todo)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error ": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todo":    todo,
	})
}

ใน function GetTodoById เราจะรับ param id เข้ามา และเอาไป findOne ใน todoCollection แล้ว return ผลลัพธ์ที่ได้ออกไปครับ

ลองทำ DELETE /todos/:id ลบข้อมูลจาก id

มาต่อกันที่การลบ Task ด้วย id กันครับ

เราจะเพิ่ม route ใน main.go สำหรับ DELETE เข้าไปครับ

main.go
r.DELETE("/todos/:id", controllers.DeleteTodo)

จากนั้นเราจะมาเพิ่ม function DeleteTodo ใน todoController.go กันครับ โดยจะเพิ่ม

controllers/todoController.go
func DeleteTodo(c *gin.Context) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	deleteResult, err := todoCollection.DeleteOne(ctx, bson.M{"_id": objId})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	if deleteResult.DeletedCount == 0 {
		msg := fmt.Sprintf("No todo with id : %v was found, no deletion occurred.", id)
		c.JSON(http.StatusBadRequest, gin.H{"error": msg})
		return
	}

	c.JSON(http.StatusNoContent, gin.H{
		"success": true,
		"id":      objId,
	})
}

ด้านบนต้อง import "fmt" เพิ่มเข้ามาด้วยเน้อครับ

ซึ่งการทำงานใน function DeleteTodo ก็คือรับ param id เข้ามา และเอาไปเรียกคำสั่ง DeleteOne ใน todoController โดยส่ง id ที่จะลบเข้าไปด้วย

ลองทำ PATCH /todos/:id แก้ไขข้อมูลด้วย id

เดี๋ยวเราจะมาทำแก้ไขข้อมูลจาก id โดยจะส่ง task หรือ status ที่ต้องการแก้ไขเข้ามาด้วยครับ ซึ่ง status เราจะให้ส่งได้แค่ todo, doing หรือ done เท่านั้น

ในส่วนนี้เราจะลง github.com/go-playground/validator/v10 เพิ่มเติมครับ

go get github.com/go-playground/validator/v10

ขั้นตอนถัดมา เราจะเพิ่ม route ใน main.go เข้าไปดังนี้ครับ

main.go
r.PATCH("/todos/:id", controllers.UpdateTodo)

จากนั้นเราจะมาแก้ไขไฟล์ models.go ที่อยู่ใน directory models กันครับ โดยจะเพิ่ม struct ชื่อ UpdateTodo โดยมีโค้ดดังนี้ครับ

models/models.go
type UpdateTodo struct {
	Task   *string `json:"task,omitempty" bson:"task"`
	Status *string `json:"status,omitempty" validate:"oneof=todo doing done" bson:"status"`
}

จากนั้นเราจะมาเพิ่ม function UpdateTodo ใน todoController.go กันครับ โดยจะเพิ่มโค้ดส่วนนี้ลงไป

controllers/todoController.go
func UpdateTodo(c *gin.Context) {
	var validate *validator.Validate = validator.New()

	var reqTodo models.UpdateTodo

	if err := c.BindJSON(&reqTodo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if reqTodo.Task != nil && *reqTodo.Task == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Task cannot be empty"})
		return
	}

	updateData := bson.M{}
	if reqTodo.Task != nil {
		updateData["task"] = *reqTodo.Task
	}

  if reqTodo.Status != nil {
		if err := validate.Var(reqTodo.Status, "oneof=todo doing done"); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status value"})
			return
		}
		updateData["status"] = *reqTodo.Status
	}

	if len(updateData) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "No valid fields to update"})
		return
	}

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := todoCollection.UpdateOne(ctx, bson.M{"_id": objId}, bson.M{"$set": updateData})

	if result.ModifiedCount == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task id"})
		return
	}

	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		fmt.Println(err.Error())
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todo":    reqTodo,
	})
}

ด้านบนต้อง import "github.com/go-playground/validator" เพิ่มเข้ามาด้วยเน้อครับ

โค้ดในส่วนอัพเดทคือ ถ้าหากมี task ส่งมา โดยต้องไม่เป็นค่าว่าง หรือมี status ส่งมาโดยต้องเป็น todo, doing หรือ done ก็จะอัพเดทข้อมูลใน MongoDB โดยดูจาก id ที่ส่งเข้ามา

ถ้าหากไม่มี task และ status ส่งมาจะ return No valid fields to update ออกไป

โค้ดทั้งหมดของ main.go จะเป็นดังนี้ครับ

main.go
package main

import (
	"net/http"
	"simple-todo/controllers"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/mongo"
)

var Client *mongo.Client

func main() {
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"message": "Server is running",
		})
	})

	r.GET("/todos", controllers.GetTodos)
	r.GET("/todos/:id", controllers.GetTodoById)
	r.POST("/todos", controllers.CreateTodo)
	r.DELETE("/todos/:id", controllers.DeleteTodo)
	r.PATCH("/todos/:id", controllers.UpdateTodo)
	r.Run(":2000")
}

โค้ดทั้งหมดของ models.go จะเป็นดังนี้ครับ

models/models.go
package models

import "go.mongodb.org/mongo-driver/bson/primitive"

type Todo struct {
	Id     primitive.ObjectID `bson:"_id"`
	Task   string             `json:"task" binding:"required" bson:"task"`
	Status string             `json:"status" bson:"status"`
}

type UpdateTodo struct {
	Task   *string `json:"task,omitempty" bson:"task"`
	Status *string `json:"status,omitempty" validate:"oneof=todo doing done" bson:"status"`
}

โค้ดทั้งหมดของ todoController.go จะเป็นดังนี้ครับ

controllers/todoController.go
package controllers

import (
	"context"
	"fmt"
	"net/http"
	"simple-todo/config"
	"simple-todo/models"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
)

var todoCollection *mongo.Collection = config.OpenCollection(config.Client, "todos")

func GetTodos(c *gin.Context) {
	// สร้าง context สำหรับการดึงข้อมูล
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cursor, err := todoCollection.Find(ctx, bson.M{})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"FindError": err.Error()})
		return
	}
	defer cursor.Close(ctx)

	var todos []models.Todo
	for cursor.Next(ctx) {
		var todo models.Todo

		err := cursor.Decode(&todo)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"Decode Error": err.Error()})
			return
		}

		todos = append(todos, todo)
	}

	if todos == nil {
		todos = []models.Todo{}
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todos":   todos,
	})
}

func GetTodoById(c *gin.Context) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	var todo models.Todo
	err := todoCollection.FindOne(ctx, bson.M{"_id": objId}).Decode(&todo)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error ": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todo":    todo,
	})
}

func CreateTodo(c *gin.Context) {
	var todo models.Todo

	// ผูกข้อมูลจาก request body กับ struct User
	if err := c.ShouldBindJSON(&todo); err != nil {
		// ถ้ามีข้อผิดพลาดในการผูกข้อมูล
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	todo.Id = primitive.NewObjectID()
	todo.Status = "todo"

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	_, err := todoCollection.InsertOne(ctx, todo)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"success": true,
		"todo":    todo,
	})
}

func DeleteTodo(c *gin.Context) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	deleteResult, err := todoCollection.DeleteOne(ctx, bson.M{"_id": objId})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	if deleteResult.DeletedCount == 0 {
		msg := fmt.Sprintf("No todo with id : %v was found, no deletion occurred.", id)
		c.JSON(http.StatusBadRequest, gin.H{"error": msg})
		return
	}

	c.JSON(http.StatusNoContent, gin.H{
		"success": true,
		"id":      objId,
	})
}

func UpdateTodo(c *gin.Context) {
	var validate *validator.Validate = validator.New()

	var reqTodo models.UpdateTodo

	if err := c.BindJSON(&reqTodo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if reqTodo.Task != nil && *reqTodo.Task == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Task cannot be empty"})
		return
	}

	updateData := bson.M{}
	if reqTodo.Task != nil {
		updateData["task"] = *reqTodo.Task
	}
	if reqTodo.Status != nil {
		if err := validate.Var(reqTodo.Status, "oneof=todo doing done"); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status value"})
			return
		}
		updateData["status"] = *reqTodo.Status
	}

	if len(updateData) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "No valid fields to update"})
		return
	}

	id := c.Param("id")
	objId, _ := primitive.ObjectIDFromHex(id)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := todoCollection.UpdateOne(ctx, bson.M{"_id": objId}, bson.M{"$set": updateData})

	if result.ModifiedCount == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task id"})
		return
	}

	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		fmt.Println(err.Error())
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"todo":    reqTodo,
	})
}

เรียบร้อยแล้วครับ หากบทความนี้มีส่วนไหนผิดพลาดประการใด ก็ขออภัยมา ณ ที่นี้ด้วยเน้อครับ

Github: https://github.com/jame3032002/simple-todo-with-go

Reference

golang-todo-mongodb