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.
go get -u github.com/gin-gonic/ginImport
import "github.com/gin-gonic/gin"Bắt đầu nhanh
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
GET localhost:8080/pingTrả về
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.jsonTà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.
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
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001Ví dụ hai
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngTham số URL
Tham số URL truyền thống, định dạng là /url?key=val&key1=val1&key2=val2.
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
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001Ví dụ hai
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isTham 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.
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
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-urlencoded và multipart/form-data.
Ví dụ hai: Sử dụng json
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() và 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.
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() và ShouldBindWith(), ví dụ
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlCác loại ràng buộc mà gin hỗ trợ như sau:
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ụ
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
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
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
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ụ:
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.
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
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' tagTIP
Đ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
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
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ề
<!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ụ:
// 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.
// 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() *Contextfunc 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
curl --location --request GET 'http://localhost:8080/hello'Đầu ra
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
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
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.
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 GMTCó 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
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.
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.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupKhi 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.
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
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Lấy ví dụ trên
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
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.
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().
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
helloMiddleware
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
// 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ỏ.
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.
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().
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.
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().
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.
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
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
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.
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: 517600Mộ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.
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
// 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) IRoutesTIP
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
|
|-- htmlfunc 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.
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.
Cookie
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
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/sessionsHỗ trợ cookie, Redis, MongoDB, GORM, PostgreSQL
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
gin.DisableConsoleColor() // Tắt màu log consoleNgoà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
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.
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
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 3Kế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.
