- Published on
เรียนรู้ Go - หัดทำ API ด้วย gin
อันยองฮาเซโยวววว วันนี้เจมส์จะมาเขียนบทความเกี่ยวกับการเรียนรู้ Go คือ หัดทำ API ด้วย gin web framework
ก่อนหน้านี้ก็เคยลองศึกษา Go มาแล้ว แต่ไม่เคยลองเขียนแบบจริง ๆ จัง ๆ สักที คิดว่าตอนนี้ถึงเวลาแล้วแหละ
สิ่งที่เจมส์จะลองทำวันนี้คือเขียน Todo นั้นเอง ทำแบบไม่ต้องใช้ database อะไร ให้เก็บไว้ในตัวแปรแล้วก็ดึงเอาครับ ถือว่าเป็นการเริ่มต้น
มาเริ่มกันเลย
สร้างโปรเจค simple-todo
และเข้าไปในโปรเจค
mkdir simple-todo && cd simple-todo
เริ่มต้นโปรเจค Go ด้วยคำสั่ง
go mod init simple-todo
Install gin web framework
go get -u github.com/gin-gonic/gin
สร้างไฟล์ main.go
ขั้นตอนต่อไปให้เราสร้างไฟล์ main.go
จากนั้นเราจะลองใส่โค้ดตามตัวอย่างของ gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run()
}
จากนั้นใช้คำสั่ง
go run main.go
แล้วลองเข้า http://localhost:8080/ping
จะพบว่าจะได้ response เป็น
{
"message": "pong"
}
ถ้าหากต้องการเปลี่ยน PORT ที่ Run
เราสามารถเปลี่ยน port ที่ต้องการ run ได้ จากเดิม default คือ 8080 ถ้าหากเราต้องการ run บน port 2000 เราสามารถแก้ไขตรง r.Run()
ให้เป็น r.Run(":2000")
ขั้นต่อไปคือเดี๋ยวเราจะมาสร้าง route ให้มีดังนี้ครับ
- GET /todos ให้ดึงข้อมูล todos ทั้งหมด
- GET /todos/:id ให้ดึงข้อมูล todo จาก id
- POST /todos จะสร้าง todo โดยรับค่า task
- PATCH /todos/:id จะแก้ไขข้อมูล โดยรับค่า task หรือ status
- DELETE /todos/:id จะลบข้อมูล todo จาก id
GET /todos
เดี๋ยวเรามาเริ่มทำในส่วนของดึงข้อมูล todos ทั้งหมดมาแสดงก่อนครับ โดยขั้นแรกเจมส์จะสร้าง Todo
เป็น struct ขึ้นมาก่อนครับ
type Todo struct {
Id int `json:"id"`
Task string `json:"task" binding:"required"`
Status string `json:"status"`
}
และกำหนด todos
ให้มีค่าเป็น slice ของ struct Todo
var todos []Todo = []Todo{}
จากนั้นจะเพิ่ม route GET /todos
ให้ return ค่า todos ออกไป ซึ่งโค้ดในตอนนี้จะเป็นดังนี้ครับ
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Todo struct {
Id int `json:"id"`
Task string `json:"task" binding:"required"`
Status string `json:"status"`
}
var todos []Todo = []Todo{}
var id = 1
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", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"todos": todos,
})
})
r.Run(":2000")
}
เมื่อเราลองเข้า http://localhost:2000/todos จะพบว่าจะแสดงข้อมูล
{
"success": true,
"todos": []
}
POST /todos
route ต่อมาที่เจมส์จะทำต่อไปคือการสร้าง todos โดยเจมส์จะเพิ่ม route เป็น POST /todos
โดยใส่โค้ดลงไปดังนี้
r.POST("/todos", func(c *gin.Context) {
var todo Todo
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo.Id = id
todo.Status = "todo"
todos = append(todos, todo)
id++
c.JSON(http.StatusCreated, gin.H{
"success": true,
"todo": todo,
})
})
คือกำหนด todo ให้มีค่าเป็น Struct Todo
จากนั้นจะ bind ค่าที่ส่งเข้ามาใส่ใน todo โดยจะเอาค่า json ไป map กับค่าที่เราเขียนไว้ใน Todo
Struct
จากนั้นเจมส์กำหนดค่า Id
ให้ todo ให้มีค่า id ที่เรากำหนดไว้ และให้มี Status
เริ่มต้นคือ "todo" และเอาค่าไป append ใส่ใน todos และให้เพิ่มค่า id เตรียมไว้ สำหรับ todo ที่เพิ่มถัดไป
จากนั้นเจมส์จะ return ค่า todo ที่เพิ่ม กลับไปครับ
เราลองมายิง POST ไปที่ http://localhost:2000/todos โดยส่ง task
ไป จะพบว่าเมื่อเรายิง post request เรียบร้อย จะได้ response success และค่า todo ตามที่เราได้สร้าง ดังรูปด้านล่างครับ
ซึ่งเมื่อเราลองไปเรียก GET /todos ใหม่อีกรอบจะพบว่าข้อมูล todos ทั้งหมดแสดงขึ้นมาแล้ว
GET /todos/:id
เดี๋ยวเราจะมาทำในส่วนของ GET /todos/:id
กันครับ ใน path นี้เราจะแทน :id
ด้วย id ของ task ที่เราสร้าง ซึ่งถ้าหากใส่ id มาไม่ถูกต้องเราจะให้ Return status เป็น 404 และ Error message กลับไปครับ
ให้เราสร้าง route GET /todos/:id
ขึ้นมาครับ โดยโค้ดที่ใส่จะเป็นดังนี้ครับ
r.GET("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var todo Todo
for _, t := range todos {
if t.Id == id {
todo = t
break
}
}
if todo.Id == 0 {
c.JSON(http.StatusNotFound, gin.H{
"error": fmt.Sprintf("Not found ID: %d ", id),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"todo": todo,
})
})
คือเราจะรับ id ที่ส่งเข้ามาโดยใช้คำสั่ง c.Param("id")
แต่ในที่นี้เจมส์จะแปลงค่า string เป็น int ด้วยคำสั่ง strconv.Atoi
เนื่องจาก Id ของ todo ที่เราสร้างเป็น int และเราจะเอาไปเช็คทำให้เราต้องแปลงค่าเป็น int ก่อนครับ
ถ้าหากมี Error เจมส์จะให้ Return status 400 และ message Invalid ID
จากนั้นเราจะหา id ของ todos ที่ตรงกับ id ที่ส่งเข้ามา โดยจะเอาไปเก็บไว้ในตัวแปร todo ที่เราสร้างไว้
จากนั้นเราจะเช็คว่า todo ที่เราสร้างมีค่า id เป็น 0 ไหม คือถ้าหากมีค่าเป็น 0 แสดงว่า id ที่ส่งเข้ามานั้นไม่ตรงกับ todos ที่มี เราจึงให้ return เป็น 404 ออกไป
แต่ถ้า id ใน todo ไม่ได้เป็นค่า 0 จะ return 200 และค่า todo ออกไป
DELETE /todos/:id
มาทำ Delete กันต่อเลยดีกว่าครับ path จะเหมือนกับการดึง todo ด้วย id ก่อนหน้านี้เลยครับ เพียงแต่เราจะใช้ Method เป็น DELETE ครับ โค้ดจะเป็นประมาณนี้ครับ
r.DELETE("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var newTodos []Todo
for _, todo := range todos {
if todo.Id != id {
newTodos = append(newTodos, todo)
}
}
if len(todos) == len(newTodos) {
c.JSON(http.StatusNotFound, gin.H{
"error": fmt.Sprintf("Not found ID: %d to delete", id),
})
return
}
todos = newTodos
c.JSON(http.StatusNoContent, gin.H{
"success": true,
"id": id,
})
})
คือเจมส์จะสร้าง route DELETE /todos/:id
ขึ้นมา จากนั้นก็จะทำเหมือนก่อนหน้านี้เลยครับคือแปลง id ที่รับเข้ามาเป็น int
ถ้าหากมี Error ในตอนแปลงก็จะให้ return เป็น 400 Bad Request
จากนั้นเราจะวนลูป todos ที่มี โดยจะเอาค่าที่ไม่ตรงกับ id ที่ส่งเข้ามาใส่ไปใน newTodos ที่เราสร้างขึ้นมาใหม่
จากนั้นเจมส์จะเช็คว่าจำนวน todos ที่มีกับจำนวน newTodos ใหม่ที่เราสร้างมีจำนวนเท่ากันไหม ถ้ามีเท่ากันแสดงว่า id ที่ส่งมาไม่ถูกต้องเพราะ ถ้าหาก id ที่ส่งเข้ามาถูกต้อง จำนวน newTodos ควรจะน้อยกว่า ดังนั้นเจมส์เลยให้มัน error ออกไปเป็น 404 เหมือนเดิม
จากนั้นเจมส์ก็กำหนด todos ให้มีค่าเป็น newTodos (ไม่มีข้อมูล todo ของ id ที่เราส่งมาแล้ว)
จากนั้นก็ให้ return เป็น status 204 NoContent ออกไป
PATCH /todos/:id
เราจะมาทำ route สุดท้ายกันครับ คือการอัพเดท todo เดี๋ยวในส่วนนี้เราจะให้ค่า status สามารถส่งได้แค่ todo
, doing
, done
ดังนั้นเดี๋ยวเราจะมาติดตั้ง package go-playground/validator
เพิ่มครับ
ขั้นแรกให้ใช้คำสั่ง
go get go-playground/validator
จากนั้นเดี๋ยวเราจะเพิ่ม struct อีกตัวนึงสำหรับอัพเดท todo ซึ่งโค้ดจะเป็นดังนี้ครับ
type UpdateTodo struct {
Task *string `json:"task,omitempty"`
Status *string `json:"status,omitempty" validate:"oneof=todo doing done"`
}
ถ้าสังเกตจะพบว่า ตรง type ของ Task จะเป็น *string
เพื่อให้เราเช็คได้ว่ามีการส่งเข้ามา หรือไม่ส่งเข้ามา
และมีส่วน validate:"oneof=todo doing done"
คือค่าที่ส่งเข้ามาของ status จะต้องเป็น todo
, doing
หรือ done
จากนั้นเพิ่มโค้ดด้านล่างนี้ที่ด้านนอก main()
var validate *validator.Validate
และเพิ่มโค้ดด้านล่างนี้ด้านใน main()
validate = validator.New()
โค้ดในส่วน route PATCH /todos/:id
จะเป็นดังนี้ครับ
r.PATCH("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var todo Todo
var reqTodo UpdateTodo
if err := c.BindJSON(&reqTodo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validate.Struct(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
}
found := false
for index, t := range todos {
if t.Id == id {
if reqTodo.Task != nil {
todos[index].Task = *reqTodo.Task
}
if reqTodo.Status != nil {
todos[index].Status = *reqTodo.Status
}
todo = todos[index]
found = true
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"todo": todo,
})
})
ขั้นแรกก็คือแปลง id ที่รับเข้ามาเป็น int
ถ้าหากมี Error ในตอนแปลงก็จะให้ Return เป็น 400 Bad Request
จากนั้นก็ Bind ค่าใส่ใน reqTodo
ถ้าหากมี Error ก็จะให้ Return เป็น 400 Bad Request ครับ
แล้วเราก็ validate ค่า reqTodo กับ struct ว่าถูกไหม คือในที่นี้ Task ต้องเป็นค่า todo หรือ doing หรือ done เท่านั้น
ถ้าหาก Error ตรงส่วนนี้ก็จะ Return 400 Bad Request เหมือนเดิม
และเช็คว่าค่า Task หากส่งมาเป็นค่าว่างให้ Error 400 Bad Request เหมือนกันครับ
ต่อมา เราจะกำหนดค่า found ให้เป็น false เพื่อไว้สำหรับเช็คว่า id ที่ส่งเข้ามามีค่าตรงกับ id ใน todos ไหม
จากนั้นเราก็วนลูปเพื่อดูว่า id ที่ส่งเข้ามาตรงกับ id ใน todos อันไหน จากนั้นก็ให้แก้ค่า Task หรือค่า Status ถ้าหากมีการส่งค่านั้น ๆ เข้ามา
และกำหนดค่า found ให้เป็น true
ขั้นต่อมาให้เช็คว่า ถ้าหาก found เป็น false คือ id ที่ส่งเข้ามาไม่ตรงกับ id ใน todos เลย จะ return status 404 ออกไป พร้อมกับ error "Todo not found"
โค้ดทั้งหมดจะเป็นประมาณนี้ครับ
package main
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type Todo struct {
Id int `json:"id"`
Task string `json:"task" binding:"required"`
Status string `json:"status"`
}
type UpdateTodo struct {
Task *string `json:"task,omitempty"`
Status *string `json:"status,omitempty" validate:"oneof=todo doing done"`
}
var validate *validator.Validate
var todos []Todo = []Todo{}
var id = 1
func main() {
validate = validator.New()
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Server is running",
})
})
r.GET("/todos", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"todos": todos,
})
})
r.GET("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var todo Todo
for _, t := range todos {
if t.Id == id {
todo = t
break
}
}
if todo.Id == 0 {
c.JSON(http.StatusNotFound, gin.H{
"error": fmt.Sprintf("Not found ID: %d ", id),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"todo": todo,
})
})
r.POST("/todos", func(c *gin.Context) {
var todo Todo
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo.Id = id
todo.Status = "todo"
todos = append(todos, todo)
id++
c.JSON(http.StatusCreated, gin.H{
"success": true,
"todo": todo,
})
})
r.DELETE("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var newTodos []Todo
for _, todo := range todos {
if todo.Id != id {
newTodos = append(newTodos, todo)
}
}
if len(todos) == len(newTodos) {
c.JSON(http.StatusNotFound, gin.H{
"error": fmt.Sprintf("Not found ID: %d to delete", id),
})
return
}
todos = newTodos
c.JSON(http.StatusNoContent, gin.H{
"success": true,
"id": id,
})
})
r.PATCH("/todos/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID",
})
return
}
var todo Todo
var reqTodo UpdateTodo
if err := c.BindJSON(&reqTodo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validate.Struct(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
}
found := false
for index, t := range todos {
if t.Id == id {
if reqTodo.Task != nil {
todos[index].Task = *reqTodo.Task
}
if reqTodo.Status != nil {
todos[index].Status = *reqTodo.Status
}
todo = todos[index]
found = true
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"todo": todo,
})
})
r.Run(":2000")
}
เรียบร้อยแล้วครับ เขียน Todo ด้วย Go
หากบทความนี้มีส่วนไหนผิดพลาดประการใด ก็ขออภัยมา ณ ที่นี้ด้วยเน้อครับ
เดี๋ยวคิดว่าน่าจะพยายามลงบทความเกี่ยวกับ Go มากขึ้น พยายามลองเล่นให้มากขึ้นครับ
หากคุณผู้อ่านมีส่วนไหนแนะนำเกี่ยวกับ Go หรือบทความ สามารถพิมพ์ทิ้งไว้ใน comment ได้เลยเน้อครับ