Skip to content

Gin

Tài liệu chính thức: Gin Web Framework (gin-gonic.com)

Địa chỉ repository: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

Ví dụ chính thức: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

Giới thiệu

Gin là một framework web được viết bằng Go (Golang). Nó có API tương tự martini nhưng hiệu suất tốt hơn nhiều, nhờ httprouter, tốc độ tăng gấp 40 lần. Nếu bạn cần hiệu suất và năng suất tốt, bạn chắc chắn sẽ thích Gin. Gin so với Iris và Beego có xu hướng là framework nhẹ hơn, chỉ phụ trách phần Web, theo đuổi hiệu suất định tuyến tối đa, chức năng có thể không đầy đủ lắm, nhưng thắng ở chỗ nhẹ và dễ mở rộng, đây cũng là ưu điểm của nó. Do đó, trong tất cả các framework Web, Gin là dễ làm quen và học nhất.

Đặc điểm

  • Nhanh: Định tuyến dựa trên cây Radix, chiếm ít bộ nhớ. Không phản xạ. Hiệu suất API có thể dự đoán được.
  • Hỗ trợ middleware: Yêu cầu HTTP đến có thể được xử lý bởi một chuỗi middleware và các thao tác cuối cùng. Ví dụ: Logger, Authorization, GZIP, thao tác cuối cùng DB.
  • Xử lý Crash: Gin có thể bắt một panic xảy ra trong yêu cầu HTTP và recover nó. Như vậy, máy chủ của bạn sẽ luôn khả dụng.
  • Xác thực JSON: Gin có thể phân tích và xác thực JSON của yêu cầu, ví dụ kiểm tra sự tồn tại của các giá trị cần thiết.
  • Nhóm định tuyến: Tổ chức định tuyến tốt hơn. Có cần ủy quyền không, các phiên bản API khác nhau... Hơn nữa, các nhóm này có thể lồng nhau vô hạn mà không làm giảm hiệu suất.
  • Quản lý lỗi: Gin cung cấp một phương pháp thuận tiện để thu thập tất cả lỗi xảy ra trong quá trình yêu cầu HTTP. Cuối cùng, middleware có thể ghi chúng vào file log, cơ sở dữ liệu và gửi qua mạng.
  • Render tích hợp: Gin cung cấp API dễ sử dụng cho render JSON, XML và HTML.
  • Khả năng mở rộng: Việc tạo middleware mới rất đơn giản

Cài đặt

Cho đến thời điểm hiện tại 2022/11/22, phiên bản Go thấp nhất mà gin hỗ trợ là 1.16, khuyến nghị sử dụng go mod để quản lý phụ thuộc dự án.

powershell
go get -u github.com/gin-gonic/gin

Import

go
import "github.com/gin-gonic/gin"

Bắt đầu nhanh

go
package main

import (
   "github.com/gin-gonic/gin"
   "net/http"
)

func main() {
   engine := gin.Default() // tạo engine gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // khởi động máy chủ, mặc định lắng nghe localhost:8080
}

Yêu cầu URL

http
GET localhost:8080/ping

Trả về

http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18

{
  "message": "pong"
}
Response file saved.
> 2022-11-22T164711.200.json

Tài liệu

Thực ra trong tài liệu chính thức của Gin không có nhiều hướng dẫn, hầu hết chỉ là một số giới thiệu và cách sử dụng cơ bản cùng với một số ví dụ, nhưng dưới tổ chức gin-gonic/, có một repository gin-gonic/examples, đây là repository ví dụ gin được cộng đồng cùng nhau bảo trì. Tất cả đều bằng tiếng Anh, thời gian cập nhật không quá thường xuyên, tác giả cũng đang học framework gin từ đây một cách chậm rãi.

Địa chỉ repository ví dụ: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Trước khi bắt đầu, khuyến nghị nên đọc HttpRouter: HttpRouter

Phân tích cú pháp tham số

Phân tích cú pháp tham số trong gin hỗ trợ tổng cộng ba cách: tham số định tuyến, tham số URL, tham số form, dưới đây sẽ giải thích lần lượt và kết hợp với ví dụ mã, khá đơn giản và dễ hiểu.

Tham số định tuyến

Tham số định tuyến thực chất là đóng gói chức năng phân tích cú pháp tham số của HttpRouter, cách sử dụng về cơ bản giống với HttpRouter.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser/:username/:userid", FindUser)
   e.GET("/downloadFile/*filepath", UserPage)

   log.Fatalln(e.Run(":8080"))
}

// Ví dụ tham số có tên
func FindUser(c *gin.Context) {
   username := c.Param("username")
   userid := c.Param("userid")
   c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}

// Ví dụ tham số đường dẫn
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

Ví dụ một

bash
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
username is jack
 userid is 001

Ví dụ hai

bash
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'
filepath is  /img/fruit.png

Tham số URL

Tham số URL truyền thống, định dạng là /url?key=val&key1=val1&key2=val2.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser", FindUser)
   log.Fatalln(e.Run(":8080"))
}

func FindUser(c *gin.Context) {
   username := c.DefaultQuery("username", "defaultUser")
   userid := c.Query("userid")
   c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}

Ví dụ một

bash
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'
username is jack
userid is 001

Ví dụ hai

bash
curl --location --request GET '127.0.0.1:8080/findUser'
username is defaultUser
userid is

Tham số form

Loại nội dung của form thường có application/json, application/x-www-form-urlencoded, application/xml, multipart/form-data.

go
package main

import (
  "github.com/gin-gonic/gin"
  "net/http"
)

func main() {
  e := gin.Default()
  e.POST("/register", RegisterUser)
  e.POST("/update", UpdateUser)
  e.Run(":8080")
}

func RegisterUser(c *gin.Context) {
  username := c.PostForm("username")
  password := c.PostForm("password")
  c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
  var form map[string]string
  c.ShouldBind(&form)
  c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}

Ví dụ một: Sử dụng form-data

bash
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'
successfully registered,your username is [jack],password is [123456]

Phương thức PostForm mặc định phân tích cú pháp loại form application/x-www-form-urlencodedmultipart/form-data.

Ví dụ hai: Sử dụng json

bash
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"username",
    "password":"123456"
}'
successfully update,your username is [username],password is [123456]

Phân tích cú pháp dữ liệu

Trong hầu hết các trường hợp, chúng ta sẽ sử dụng struct để chứa dữ liệu thay vì phân tích cú pháp tham số trực tiếp. Trong gin, phương thức chính dùng để ràng buộc dữ liệu là Bind()ShouldBind(), sự khác biệt giữa hai phương thức này là phương thức trước cũng gọi trực tiếp ShouldBind() bên trong, tất nhiên khi trả về err, nó sẽ phản hồi 400 trực tiếp, phương thức sau thì không. Nếu muốn xử lý lỗi linh hoạt hơn, khuyến nghị chọn phương thức sau. Hai hàm này sẽ tự động suy luận cách phân tích cú pháp dựa trên content-type của yêu cầu.

go
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // Gọi ShouldBindWith()
  if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // Phản hồi 400 badrequest trực tiếp
    return err
  }
  return nil
}

Nếu muốn tự lựa chọn, có thể sử dụng BindWith()ShouldBindWith(), ví dụ

go
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xml

Các loại ràng buộc mà gin hỗ trợ như sau:

go
var (
   JSON          = jsonBinding{}
   XML           = xmlBinding{}
   Form          = formBinding{}
   Query         = queryBinding{}
   FormPost      = formPostBinding{}
   FormMultipart = formMultipartBinding{}
   ProtoBuf      = protobufBinding{}
   MsgPack       = msgpackBinding{}
   YAML          = yamlBinding{}
   Uri           = uriBinding{}
   Header        = headerBinding{}
   TOML          = tomlBinding{}
)

Ví dụ

go
package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "net/http"
)

type LoginUser struct {
  Username string `binding:"required" json:"username" form:"username" uri:"username"`
  Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
  e := gin.Default()
  e.POST("/loginWithJSON", Login)
  e.POST("/loginWithForm", Login)
  e.GET("/loginWithQuery/:username/:password", Login)
  e.Run(":8080")
}

func Login(c *gin.Context) {
  var login LoginUser
    // Sử dụng ShouldBind để gin tự động suy luận
  if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
    c.String(http.StatusOK, "login successfully !")
  } else {
    c.String(http.StatusBadRequest, "login failed !")
  }
  fmt.Println(login)
}

Ràng buộc dữ liệu Json

bash
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"root",
    "password":"root"
}'
login successfully !

Ràng buộc dữ liệu form

go
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
login successfully !

Ràng buộc dữ liệu URL

go
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
login failed !

Đến đây sẽ xảy ra lỗi, vì content-type xuất ra ở đây là chuỗi rỗng, không thể suy luận rốt cuộc là muốn phân tích cú pháp dữ liệu như thế nào. Vì vậy khi sử dụng tham số URL, chúng ta nên chỉ định cách phân tích cú pháp thủ công, ví dụ:

go
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
   c.String(http.StatusOK, "login successfully !")
} else {
   fmt.Println(err)
   c.String(http.StatusBadRequest, "login failed !")
}

Ràng buộc nhiều lần

Thông thường phương thức đều gọi hàm c.Request.Body để ràng buộc dữ liệu, nhưng không thể gọi phương thức này nhiều lần, ví dụ c.ShouldBind, không thể tái sử dụng, nếu muốn ràng buộc nhiều lần, có thể sử dụng c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // Đọc c.Request.Body và lưu kết quả vào ngữ cảnh.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Lúc này, tái sử dụng body được lưu trữ trong ngữ cảnh.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // Có thể chấp nhận các định dạng khác
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith sẽ lưu body vào ngữ cảnh trước khi ràng buộc. Điều này sẽ ảnh hưởng nhẹ đến hiệu suất, nếu có thể hoàn thành ràng buộc chỉ bằng một lần gọi thì không nên sử dụng phương thức này. Chỉ một số định dạng cần chức năng này, như JSON, XML, MsgPack, ProtoBuf. Đối với các định dạng khác, như Query, Form, FormPost, FormMultipart có thể gọi c.ShouldBind() nhiều lần mà không gây ra bất kỳ tổn thất hiệu suất nào.

Xác thực dữ liệu

Công cụ xác thực tích hợp của gin thực chất là github.com/go-playground/validator/v10, cách sử dụng cũng gần như không có khác biệt, Validator

Ví dụ đơn giản

go
type LoginUser struct {
   Username string `binding:"required"  json:"username" form:"username" uri:"username"`
   Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
   e := gin.Default()
   e.POST("/register", Register)
   log.Fatalln(e.Run(":8080"))
}

func Register(ctx *gin.Context) {
   newUser := &LoginUser{}
   if err := ctx.ShouldBind(newUser); err == nil {
      ctx.String(http.StatusOK, "user%+v", *newUser)
   } else {
      ctx.String(http.StatusBadRequest, "invalid user,%v", err)
   }
}

Kiểm tra

curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"jack1"

}'

Đầu ra

invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

TIP

Điều cần lưu ý là tag xác thực của validator trong gin là binding, còn tag xác thực của validator khi sử dụng riêng là validator

Phản hồi dữ liệu

Phản hồi dữ liệu là bước cuối cùng cần làm trong xử lý interface, backend sau khi xử lý xong tất cả dữ liệu sẽ trả về cho người gọi thông qua giao thức HTTP, gin cung cấp hỗ trợ phong phú cho phản hồi dữ liệu, cách sử dụng đơn giản và dễ hiểu, rất dễ làm quen.

Ví dụ đơn giản

go
func Hello(c *gin.Context) {
    // Trả về dữ liệu định dạng chuỗi thuần túy, http.StatusOK đại diện cho mã trạng thái 200, dữ liệu là "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

Render HTML

TIP

Khi tải file, đường dẫn gốc mặc định là đường dẫn dự án, tức là đường dẫn nơi file go.mod tọa lạc, index.html trong ví dụ dưới đây tức là nằm ở index.html dưới đường dẫn gốc, nhưng nói chung các file template này sẽ không được đặt ở đường dẫn gốc mà sẽ được lưu trữ trong thư mục tài nguyên tĩnh

go
func main() {
   e := gin.Default()
    // Tải file HTML, cũng có thể sử dụng Engine.LoadHTMLGlob()
   e.LoadHTMLFiles("index.html")
   e.GET("/", Index)
   log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
   c.HTML(http.StatusOK, "index.html", gin.H{})
}

Kiểm tra

curl --location --request GET 'http://localhost:8080/'

Trả về

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>GinLearn</title>
  </head>

  <body>
    <h1>Hello World!</h1>
    <h1>This is a HTML Template Render Example</h1>
  </body>
</html>

Phản hồi nhanh

Thường hay sử dụng phương thức context.String() ở trên để phản hồi dữ liệu, đây là phương thức phản hồi nguyên thủy nhất, trực tiếp trả về một chuỗi, trong gin còn tích hợp nhiều phương thức phản hồi nhanh khác ví dụ:

go
// Sử dụng Render để ghi header phản hồi và render dữ liệu
func (c *Context) Render(code int, r render.Render)

// Render một template HTML, name là đường dẫn html, obj là nội dung
func (c *Context) HTML(code int, name string, obj any)

// Render dữ liệu bằng chuỗi JSON được thụt lề đẹp, thường không khuyến nghị sử dụng phương thức này vì sẽ gây ra nhiều chi phí truyền tải hơn.
func (c *Context) IndentedJSON(code int, obj any)

// JSON an toàn, có thể ngăn chặn JSON hijacking, chi tiết xem tại: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// Render theo cách JSONP
func (c *Context) JSONP(code int, obj any)

// Render theo cách JSON
func (c *Context) JSON(code int, obj any)

// Render theo cách JSON, sẽ chuyển đổi mã unicode sang mã ASCII
func (c *Context) AsciiJSON(code int, obj any)

// Render theo cách JSON, sẽ không escape các ký tự đặc biệt HTML
func (c *Context) PureJSON(code int, obj any)

// Render theo cách XML
func (c *Context) XML(code int, obj any)

// Render theo cách YML
func (c *Context) YAML(code int, obj any)

// Render theo cách TOML
func (c *Context) TOML(code int, interface{})

// Render theo cách ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)

// Render theo cách String
func (c *Context) String(code int, format string, values ...any)

// Chuyển hướng đến vị trí cụ thể
func (c *Context) Redirect(code int, location string)

// Ghi data vào stream phản hồi
func (c *Context) Data(code int, contentType string, data []byte)

// Đọc stream thông qua reader và ghi vào stream phản hồi
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// Ghi file vào stream phản hồi một cách hiệu quả
func (c *Context) File(filepath string)

// Ghi stream file từ fs vào stream phản hồi một cách hiệu quả
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// Ghi stream file từ fs vào stream phản hồi một cách hiệu quả, và ở phía client sẽ tải xuống với tên file được chỉ định
func (c *Context) FileAttachment(filepath, filename string)

// Ghi stream đẩy từ máy chủ vào stream phản hồi
func (c *Context) SSEvent(name string, message any)

// Gửi một phản hồi stream và trả về một giá trị boolean, để xác định xem client có ngắt kết nối giữa chừng hay không
func (c *Context) Stream(step func(w io.Writer) bool) bool

Đối với hầu hết các ứng dụng, sử dụng nhiều nhất vẫn là context.JSON, các phương thức khác tương đối ít hơn, ở đây không đưa ra ví dụ minh họa, vì đều khá đơn giản dễ hiểu, gần như đều là gọi trực tiếp.

Xử lý bất đồng bộ

Trong gin, xử lý bất đồng bộ cần kết hợp với goroutine, sử dụng rất đơn giản.

go
// copy trả về một bản sao của Context hiện tại để sử dụng an toàn bên ngoài phạm vi Context hiện tại, có thể dùng để truyền cho một goroutine
func (c *Context) Copy() *Context
go
func main() {
  e := gin.Default()
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Hello(c *gin.Context) {
  ctx := c.Copy()
  go func() {
    // Goroutine con nên sử dụng bản sao của Context, không nên sử dụng Context gốc
    log.Println("Hàm xử lý bất đồng bộ: ", ctx.HandlerNames())
  }()
  log.Println("Hàm xử lý interface: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Kiểm tra

go
curl --location --request GET 'http://localhost:8080/hello'

Đầu ra

go
2022/12/21 13:33:47 异步处理函数:[]
2022/12/21 13:33:47 接口处理函数:[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 |     11.1927ms |             ::1 | GET      "/hello"

Có thể thấy đầu ra của hai bên khác nhau, bản sao khi sao chép, vì lý do an toàn, đã xóa đi giá trị của nhiều phần tử.

Truyền tải file

Truyền tải file là một chức năng không thể thiếu trong ứng dụng Web, gin hỗ trợ việc này cũng được đóng gói rất đơn giản, nhưng thực chất về cơ bản cũng giống như quy trình sử dụng net/http nguyên thủy. Quy trình đều là đọc stream file từ body yêu cầu, sau đó lưu vào local.

Tải lên một file

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
  // Lấy file
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Lưu vào local
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Trả về kết quả
  ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

Kiểm tra

curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'

Kết quả

upload a.jpg size:1424 byte successfully!

TIP

Nói chung, Method tải lên file sẽ chỉ định sử dụng POST, một số công ty có thể có xu hướng sử dụng PUT, cái trước là yêu cầu HTTP đơn giản, cái sau là yêu cầu HTTP phức tạp, sự khác biệt cụ thể không nói lại ở đây, nếu sử dụng cái sau, đặc biệt là trong dự án tách biệt frontend và backend, cần xử lý cross-domain tương ứng, mà cấu hình mặc định của Gin không hỗ trợ cross-domain Cấu hình cross-domain.

Tải lên nhiều file

go
func main() {
   e := gin.Default()
   e.POST("/upload", uploadFile)
   e.POST("/uploadFiles", uploadFiles)
   log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
  // Lấy form multipart đã được gin phân tích
  form, _ := ctx.MultipartForm()
  // Lấy danh sách file tương ứng theo key
  files := form.File["files"]
  // Duyệt danh sách file, lưu vào local
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // Trả về kết quả
  ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

Kiểm tra

curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'

Đầu ra

upload 3 files successfully!

Tải xuống file

Về phần tải xuống file, Gin một lần nữa đóng gói API của thư viện chuẩn gốc, khiến việc tải xuống file trở nên cực kỳ đơn giản.

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  e.POST("/uploadFiles", uploadFiles)
  e.GET("/download/:filename", download)
  log.Fatalln(e.Run(":8080"))
}

func download(ctx *gin.Context) {
    // Lấy tên file
  filename := ctx.Param("filename")
    // Trả về file tương ứng
  ctx.FileAttachment(filename, filename)
}

Kiểm tra

curl --location --request GET 'http://localhost:8080/download/a.jpg'

Kết quả

Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMT

Có cảm thấy đơn giản quá không, hãy thử tự viết lại quy trình mà không dùng phương thức của framework

go
func download(ctx *gin.Context) {
   // Lấy tham số
   filename := ctx.Param("filename")

   // Đối tượng phản hồi yêu cầu và đối tượng yêu cầu
   response, request := ctx.Writer, ctx.Request
   // Ghi header phản hồi
   // response.Header().Set("Content-Type", "application/octet-stream")  truyền file dưới dạng stream nhị phân
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Escape an toàn cho tên file
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // Mã hóa truyền tải
   http.ServeFile(response, request, filename)
}

Thực ra net/http cũng đã được đóng gói đủ tốt

TIP

Có thể thiết lập bộ nhớ tối đa cho việc truyền tải file thông qua Engine.MaxMultipartMemory, mặc định là 32 << 20 // 32 MB

Quản lý định tuyến

Quản lý định tuyến là một phần rất quan trọng trong hệ thống, cần đảm bảo mỗi yêu cầu đều có thể được ánh xạ chính xác đến hàm tương ứng.

Nhóm định tuyến

Tạo một nhóm định tuyến là để phân loại các interface, các interface thuộc loại khác nhau tương ứng với các chức năng khác nhau, cũng dễ quản lý hơn.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Giả sử chúng ta có bốn interface trên, tạm thời không quan tâm đến việc triển khai bên trong, Hello, Login là một nhóm, Update, Delete là một nhóm.

go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

Khi tạo nhóm, chúng ta cũng có thể đăng ký bộ xử lý cho route gốc của nhóm, nhưng hầu hết thời gian sẽ không làm như vậy.

go
func main() {
  e := gin.Default()
  v1 := e.Group("v1")
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("v2")
  {
    v2.POST("/update", Update)
    v2.DELETE("/delete", Delete)
  }
}

Chúng ta chia thành hai nhóm v1, v2, dấu ngoặc nhọn {} trong đó chỉ là để quy chuẩn, biểu thị các bộ xử lý được đăng ký trong ngoặc nhọn thuộc về cùng một nhóm định tuyến, không có tác dụng gì về chức năng. Tương tự, gin cũng hỗ trợ nhóm lồng nhau, phương thức giống như ví dụ trên, ở đây không trình bày lại.

Định tuyến 404

Cấu trúc Engine trong gin cung cấp một phương thức NoRoute, để thiết lập cách xử lý khi URL truy cập không tồn tại, nhà phát triển có thể ghi logic vào phương thức này, để tự động gọi khi không tìm thấy route, mặc định sẽ trả về mã trạng thái 404

go
func (engine *Engine) NoRoute(handlers ...HandlerFunc)

Lấy ví dụ trên

go
func main() {
   e := gin.Default()
   v1 := e.Group("v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   // Đăng ký bộ xử lý
   e.NoRoute(func(context *gin.Context) { // Đây chỉ là demo, không nên trả về mã HTML trực tiếp trong môi trường sản xuất
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Gửi một yêu cầu bất kỳ

curl --location --request GET 'http://localhost:8080/'
<h1>404 Page Not Found</h1>

Định tuyến 405

Trong mã trạng thái Http, 405 đại diện cho loại phương thức yêu cầu hiện tại không được phép, gin cung cấp phương thức sau

go
func (engine *Engine) NoMethod(handlers ...HandlerFunc)

Để đăng ký một bộ xử lý, để tự động gọi khi xảy ra, với điều kiện là thiết lập Engine.HandleMethodNotAllowed = true.

go
func main() {
   e := gin.Default()
   // Cần thiết lập thành true
   e.HandleMethodNotAllowed = true
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   e.NoRoute(func(context *gin.Context) {
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   // Đăng ký bộ xử lý
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "method not allowed")
   })
   log.Fatalln(e.Run(":8080"))
}

Sau khi cấu hình xong, header mặc định của gin không hỗ trợ yêu cầu OPTION, thử kiểm tra

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
method not allowed

Đến đây cấu hình thành công

Chuyển hướng

Việc chuyển hướng trong gin rất đơn giản, chỉ cần gọi phương thức gin.Context.Redirect().

go
func main() {
  e := gin.Default()
  e.GET("/", Index)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
  c.Redirect(http.StatusMovedPermanently, "/hello")
}

func Hello(c *gin.Context) {
  c.String(http.StatusOK, "hello")
}

Kiểm tra

curl --location --request GET 'http://localhost:8080/'

Đầu ra

hello

Middleware

Gin rất nhẹ và linh hoạt, khả năng mở rộng rất cao, hỗ trợ middleware cũng rất tốt. Trong Gin, tất cả các yêu cầu interface đều đi qua middleware, thông qua middleware, nhà phát triển có thể tùy chỉnh triển khai nhiều chức năng và logic, Gin tuy bản thân mang theo rất ít chức năng, nhưng các middleware mở rộng do cộng đồng bên thứ ba phát triển rất phong phú.

Middleware về cơ bản vẫn là một bộ xử lý interface

go
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

Theo một nghĩa nào đó, bộ xử lý tương ứng với mỗi yêu cầu cũng là middleware, chỉ là middleware cục bộ có phạm vi tác dụng rất nhỏ.

go
func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

Xem mã nguồn của gin, trong hàm Default, Engine mặc định trả về đã sử dụng hai middleware mặc định Logger(), Recovery(), nếu không muốn sử dụng middleware mặc định cũng có thể dùng gin.New() để thay thế.

Middleware toàn cục

Middleware toàn cục tức là phạm vi tác dụng là toàn cục, tất cả các yêu cầu của toàn bộ hệ thống đều sẽ đi qua middleware này.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("Middleware toàn cục được thực thi...")
   }
}

Trước tiên tạo một hàm closure để tạo middleware, sau đó đăng ký middleware toàn cục thông qua Engine.Use().

go
func main() {
   e := gin.Default()
   // Đăng ký middleware toàn cục
   e.Use(GlobalMiddleware())
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Kiểm tra

curl --location --request GET 'http://localhost:8080/v1/hello'

Đầu ra

[GIN-debug] Listening and serving HTTP on :8080
Middleware toàn cục được thực thi...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

Middleware cục bộ

Middleware cục bộ tức là phạm vi tác dụng là cục bộ, các yêu cầu cục bộ trong hệ thống sẽ đi qua middleware này. Middleware cục bộ có thể được đăng ký vào một route đơn lẻ, nhưng phần lớn thời gian là đăng ký vào nhóm định tuyến.

go
func main() {
   e := gin.Default()
   // Đăng ký middleware toàn cục
   e.Use(GlobalMiddleware())
   // Đăng ký middleware cục bộ cho nhóm định tuyến
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // Đăng ký middleware cục bộ cho một route đơn lẻ
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Kiểm tra

curl --location --request POST 'http://localhost:8080/v2/update'

Đầu ra

Middleware toàn cục được thực thi...
Middleware cục bộ được thực thi
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

Nguyên lý middleware

Việc sử dụng và tùy chỉnh middleware trong Gin rất dễ dàng, nguyên lý bên trong cũng khá đơn giản, để học tập tiếp theo, cần hiểu đơn giản về nguyên lý bên trong. Middleware trong Gin thực chất sử dụng mô hình Chain of Responsibility, trong Context duy trì một HandlersChain, về cơ bản là một []HandlerFunc, và một index, kiểu dữ liệu của nó là int8. Trong phương thức Engine.handlerHTTPRequest(c *Context), có một đoạn mã biểu thị quá trình gọi: sau khi gin tìm thấy route tương ứng trong cây route, sẽ gọi phương thức Next().

go
if value.handlers != nil {
   // Gán chuỗi gọi cho Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // Gọi middleware
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Việc gọi Next() mới là then chốt, Next() sẽ duyệt qua HandlerFunc trong handlers của route và thực thi, lúc này có thể thấy tác dụng của index là ghi lại vị trí gọi middleware. Trong đó, hàm interface được đăng ký cho route tương ứng cũng nằm trong handlers, đây cũng là lý do tại sao trước đó nói interface cũng là một middleware.

go
func (c *Context) Next() {
   // +1 ngay khi vào để tránh陷入递归死循环, giá trị mặc định là -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // Thực thi HandlerFunc
      c.handlers[c.index](c)
      // Thực thi xong, index+1
      c.index++
   }
}

Sửa logic của Hello() để xác minh xem có thực sự như vậy không

go
func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

Kết quả đầu ra là

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

Có thể thấy thứ tự của chuỗi gọi middleware là: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello, phần tử cuối cùng của chuỗi gọi mới thực sự là hàm interface cần thực thi, các phần trước đều là middleware.

TIP

Khi đăng ký route cục bộ, có một assertion như sau

go
finalSize := len(group.Handlers) + len(handlers) // Tổng số middleware
assert1(finalSize < int(abortIndex), "too many handlers")

Trong đó abortIndex int8 = math.MaxInt8 >> 1 giá trị là 63, tức là khi sử dụng hệ thống, số lượng đăng ký route không được vượt quá 63.

Middleware đồng hồ bấm thời gian

Sau khi biết nguyên lý middleware ở trên, có thể viết một middleware thống kê thời gian yêu cầu đơn giản.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // Ghi lại thời gian bắt đầu
      start := time.Now()
      // Thực thi chuỗi gọi tiếp theo
      context.Next()
      // Tính khoảng cách thời gian
      duration := time.Since(start)
      // Đầu ra nano giây để quan sát kết quả
      fmt.Println("Thời gian yêu cầu: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // Đăng ký middleware toàn cục, middleware đồng hồ bấm thời gian
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // Đăng ký middleware cục bộ cho nhóm định tuyến
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // Đăng ký middleware cục bộ cho một route đơn lẻ
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

Kiểm tra

curl --location --request GET 'http://localhost:8080/v1/hello'

Đầu ra

Thời gian yêu cầu:  517600

Một middleware đồng hồ bấm thời gian đơn giản đã được viết xong, sau này có thể dựa vào sự tìm tòi của bản thân để viết một số middleware tiện ích hơn.

Cấu hình dịch vụ

Chỉ sử dụng cấu hình mặc định là chưa đủ, trong hầu hết các trường hợp đều cần sửa đổi nhiều cấu hình dịch vụ để đạt được yêu cầu.

Cấu hình Http

Có thể cấu hình thông qua việc tạo Server từ net/http, bản thân Gin cũng hỗ trợ sử dụng Gin giống như API nguyên thủy.

go
func main() {
   router := gin.Default()
   server := &http.Server{
      Addr:           ":8080",
      Handler:        router,
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
   }
   log.Fatal(server.ListenAndServe())
}

Cấu hình tài nguyên tĩnh

Tài nguyên tĩnh trong quá khứ về cơ bản là một phần không thể thiếu của máy chủ, mặc dù tỷ lệ sử dụng đang dần giảm trong hiện tại, nhưng vẫn còn rất nhiều hệ thống vẫn sử dụng kiến trúc monolithic.

Gin cung cấp ba phương thức để tải tài nguyên tĩnh

go
// Tải một thư mục tĩnh cụ thể
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// Tải một fs cụ thể
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// Tải một file tĩnh cụ thể
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath là đường dẫn tương đối được ánh xạ lên URL web, root là đường dẫn thực tế của file trong dự án

Giả sử cấu trúc thư mục của dự án như sau

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // Tải thư mục file tĩnh
   router.Static("/static", "./static")
   // Tải thư mục file tĩnh
   router.StaticFS("/view", http.Dir("view"))
   // Tải file tĩnh
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

Cấu hình CORS

Bản thân Gin không xử lý bất kỳ cấu hình cross-domain nào, cần tự viết middleware để triển khai các yêu cầu tương ứng, thực ra độ khó cũng không lớn, những người quen với giao thức HTTP nói chung đều có thể viết được, logic về cơ bản đều giống nhau.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // Máy chủ trong môi trường sản xuất thường sẽ không điền *, nên điền tên miền cụ thể
         c.Header("Access-Control-Allow-Origin", origin)
         // HTTP METHOD được phép sử dụng
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // Header yêu cầu được phép sử dụng
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // Header phản hồi được phép client truy cập
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Có cần mang theo thông tin xác thực hay không, Credentials có thể là cookies, authorization headers hoặc TLS client certificates
         // Khi thiết lập thành true, Access-Control-Allow-Origin không được là *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // Cho phép yêu cầu OPTION, nhưng không thực thi các phương thức tiếp theo
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // Cho phép
      c.Next()
   }
}

Đăng ký middleware làm middleware toàn cục là được

Kiểm soát phiên

Trong thời đại hiện nay, ba loại kiểm soát phiên Web phổ biến tổng cộng có ba loại, cookie, session, JWT.

Thông tin trong cookie được lưu trữ dưới dạng cặp key-value trong trình duyệt, và có thể nhìn thấy dữ liệu trực tiếp trong trình duyệt

Ưu điểm:

  • Cấu trúc đơn giản
  • Dữ liệu持久化

Nhược điểm:

  • Kích thước bị hạn chế
  • Lưu trữ dưới dạng văn bản thuần túy
  • Dễ bị tấn công CSRF
go
import (
    "fmt"

    "github.com/gin-gonic/gin"
)

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

         // Lấy cookie tương ứng
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // Thiết lập cookie 参数:key, val, thời gian tồn tại, thư mục, tên miền, có cho phép người khác truy cập cookie qua js hay không, chỉ http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

Cookie thuần túy được sử dụng nhiều hơn vào năm sáu năm trước, nhưng tác giả nói chung rất ít khi sử dụng cookie thuần túy để kiểm soát phiên, làm như vậy thực sự không an toàn lắm.

Session

Session được lưu trữ trên máy chủ, sau đó gửi một cookie lưu trữ trong trình duyệt, cookie lưu trữ session_id, sau đó mỗi lần yêu cầu máy chủ có thể lấy thông tin session tương ứng thông qua session_id

Ưu điểm:

  • Lưu trữ ở phía máy chủ, tăng tính bảo mật, dễ quản lý

Nhược điểm:

  • Lưu trữ ở phía máy chủ, tăng chi phí máy chủ, giảm hiệu suất
  • Dựa trên nhận diện cookie, không an toàn
  • Thông tin xác thực không đồng bộ trong trường hợp phân tán

Session và Cookie không tách rời nhau, mỗi khi dùng đến Session, mặc định là phải dùng đến Cookie. Gin mặc định không hỗ trợ Session, vì Cookie là nội dung trong giao thức Http, còn Session thì không, nhưng có middleware của bên thứ ba hỗ trợ, chỉ cần cài đặt dependency là được, địa chỉ repository: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

Hỗ trợ cookie, Redis, MongoDB, GORM, PostgreSQL

go
func main() {
   r := gin.Default()
   // Tạo storage engine dựa trên Cookie
   store := cookie.NewStore([]byte("secret"))
   // Thiết lập middleware Session, mysession即 là tên session, cũng là tên cookie
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // Khởi tạo session
      session := sessions.Default(c)
      var count int
      // Lấy giá trị
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // Thiết lập
      session.Set("count", count)
      // Lưu
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Nói chung không khuyến nghị lưu Sesison thông qua Cookie, khuyến nghị sử dụng Redis, các ví dụ khác vui lòng tự tìm hiểu tại repository chính thức.

JWT

Ưu điểm:

  • Dựa trên JSON, đa ngôn ngữ thông dụng
  • Có thể lưu trữ thông tin không nhạy cảm
  • Kích thước nhỏ, dễ truyền tải
  • Máy chủ không cần lưu trữ, có lợi cho việc mở rộng phân tán

Nhược điểm:

  • Vấn đề làm mới Token
  • Một khi đã ký phát thì không thể chủ động kiểm soát

Kể từ cuộc cách mạng frontend, lập trình viên frontend không còn chỉ là "người viết giao diện" nữa, xu hướng tách biệt frontend và backend ngày càng mạnh mẽ, JWT là phù hợp nhất cho việc kiểm soát phiên trong hệ thống tách biệt frontend-backend và hệ thống phân tán, có ưu thế tự nhiên rất lớn. Xét việc JWT đã hoàn toàn tách rời khỏi nội dung của Gin, và không có hỗ trợ middleware nào, vì JWT bản thân nó đã không bị giới hạn trong bất kỳ framework hay ngôn ngữ nào, ở đây sẽ không giải thích chi tiết, có thể đến tài liệu khác: JWT

Quản lý log

Middleware log mặc định mà Gin sử dụng là os.Stdout, chỉ có chức năng cơ bản nhất, dù sao Gin chỉ tập trung vào dịch vụ Web, trong hầu hết các trường hợp nên sử dụng framework log trưởng thành hơn, tuy nhiên điều này không nằm trong phạm vi thảo luận của chương này, và khả năng mở rộng của Gin rất cao, có thể dễ dàng tích hợp với các framework khác, ở đây chỉ thảo luận về dịch vụ log tích hợp sẵn của nó.

Màu console

go
gin.DisableConsoleColor() // Tắt màu log console

Ngoài thời gian phát triển, hầu hết thời gian đều không khuyến nghị bật mục này

Ghi log vào file

go
func main() {
  e := gin.Default()
    // Tắt màu console
  gin.DisableConsoleColor()
    // Tạo hai file log
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // Ghi lại cùng lúc vào hai file log
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

Log tích hợp của gin hỗ trợ ghi vào nhiều file, nhưng nội dung là giống nhau, sử dụng không quá tiện lợi, và sẽ không ghi log yêu cầu vào file.

go
func main() {
  router := gin.New()
  // Middleware LoggerWithFormatter sẽ ghi log vào gin.DefaultWriter
  // Mặc định gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO logic ghi vào file tương ứng
        ......
    // Đầu ra định dạng tùy chỉnh
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
        param.ClientIP,
        param.TimeStamp.Format(time.RFC1123),
        param.Method,
        param.Path,
        param.Request.Proto,
        param.StatusCode,
        param.Latency,
        param.Request.UserAgent(),
        param.ErrorMessage,
    )
  }))
  router.Use(gin.Recovery())
  router.GET("/ping", func(c *gin.Context) {
    c.String(200, "pong")
  })
  router.Run(":8080")
}

Thông qua middleware tùy chỉnh, có thể thực hiện ghi log vào file

Định dạng log debug định tuyến

Ở đây chỉ sửa đổi log đầu ra thông tin route khi khởi động

go
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("Route %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

Đầu ra

2022/12/21 17:19:13 Route GET /hello main.Hello 3

Kết luận: Gin có thể coi là một trong những framework Web dễ học nhất trong ngôn ngữ Go, vì Gin thực sự đạt được việc tối thiểu hóa trách nhiệm, chỉ đơn thuần phụ trách dịch vụ Web, các logic xác thực khác, bộ nhớ đệm dữ liệu, v.v. đều giao cho nhà phát triển tự hoàn thành, so với những framework lớn và đầy đủ, Gin nhẹ và gọn gàng hơn phù hợp và nên học hơn cho người mới bắt đầu, vì Gin không bắt buộc sử dụng bất kỳ quy chuẩn nào, dự án nên xây dựng như thế nào, áp dụng cấu trúc gì đều cần tự suy nghĩ, đối với người mới bắt đầu càng có thể rèn luyện năng lực.

Golang by www.golangdev.cn edit