Gin
เอกสารอย่างเป็นทางการ: Gin Web Framework (gin-gonic.com)
ที่อยู่ repository: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)
ตัวอย่างอย่างเป็นทางการ: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
บทนำ
Gin เป็นเฟรมเวิร์กเว็บที่เขียนด้วย Go (Golang) มี API คล้าย martini แต่ประสิทธิภาพดีกว่ามาก ต้องขอบคุณ httprouter ที่ทำให้ความเร็วเพิ่มขึ้น 40 เท่า หากคุณต้องการประสิทธิภาพและผลิตภาพที่ดี คุณจะต้องชอบ Gin Gin เมื่อเทียบกับ Iris และ Beego แล้ว มีแนวโน้มเป็นเฟรมเวิร์กแบบเบา更注重 Web ส่วน มุ่งเน้นประสิทธิภาพเส้นทางสูงสุด ฟังก์ชันอาจไม่ครบถ้วน แต่ชนะที่เบาและขยายง่าย นี่คือข้อดีของมัน ดังนั้น ในเฟรมเวิร์ก Web ทั้งหมด Gin เป็นสิ่งที่ง่ายที่สุดในการเริ่มต้นและเรียนรู้
คุณสมบัติ
- รวดเร็ว: เส้นทาง基于 Radix tree ใช้หน่วยความจำน้อย ไม่มีการ reflection ประสิทธิภาพ API ที่คาดการณ์ได้
- รองรับ middleware: คำขอ HTTP ที่เข้ามาสามารถจัดการโดย series ของ middleware และการดำเนินการสุดท้ายได้ เช่น Logger, Authorization, GZIP, การดำเนินการสุดท้าย DB
- การจัดการ Crash: Gin สามารถ catch panic ที่เกิดขึ้นในคำขอ HTTP และ recover ได้ ดังนั้น เซิร์ฟเวอร์ของคุณจะพร้อมใช้งานเสมอ
- การตรวจสอบ JSON: Gin สามารถแยกวิเคราะห์และตรวจสอบ JSON ของคำขอ เช่น ตรวจสอบการมีอยู่ของค่าที่จำเป็น
- กลุ่มเส้นทาง: จัดระเบียบเส้นทางได้ดีขึ้น ต้องการการอนุญาตหรือไม่, เวอร์ชัน API ที่ต่างกัน... นอกจากนี้ กลุ่มเหล่านี้สามารถซ้อนกันได้ไม่จำกัดโดยไม่ลดประสิทธิภาพ
- การจัดการข้อผิดพลาด: Gin ให้วิธีที่สะดวกในการรวบรวมข้อผิดพลาดทั้งหมดที่เกิดขึ้นระหว่างคำขอ HTTP สุดท้าย middleware สามารถเขียนลงในไฟล์ log, ฐานข้อมูล และส่งผ่านเครือข่ายได้
- การแสดงผลในตัว: Gin ให้ API ที่ใช้งานง่ายสำหรับการแสดงผล JSON, XML และ HTML
- ความสามารถในการขยาย: การสร้าง middleware ใหม่เป็นเรื่องง่ายมาก
การติดตั้ง
จนถึงปัจจุบัน2022/11/22 เวอร์ชันต่ำสุดของ go ที่ gin รองรับคือ1.16 แนะนำให้ใช้go modเพื่อจัดการการพึ่งพาโปรเจกต์
go get -u github.com/gin-gonic/ginนำเข้า
import "github.com/gin-gonic/gin"เริ่มต้นอย่างรวดเร็ว
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default() //สร้าง gin engine
engine.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
engine.Run() //เปิดเซิร์ฟเวอร์ ค่าเริ่มต้นคือ监听 localhost:8080
}ขอ URL
GET localhost:8080/pingส่งคืน
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"
}เอกสาร
จริงๆ แล้วในเอกสารทางการของ Gin ไม่มีบทเรียนมากนัก ส่วนใหญ่เป็นเพียงคำอธิบายและการใช้งานพื้นฐาน以及一些ตัวอย่าง แต่ภายใต้gin-gonic/ organization มี repository gin-gonic/examples ซึ่งเป็น repository ตัวอย่างของ gin ที่ดูแลโดยชุมชน ทั้งหมดเป็นภาษาอังกฤษ อัตราการอัปเดตไม่บ่อยนัก ผู้เขียนก็ค่อยๆ เรียนรู้เฟรมเวิร์ก gin จากที่นี่
ที่อยู่ repository ตัวอย่าง: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
TIP
ก่อนเริ่มต้นแนะนำให้ อ่าน HttpRouter: HttpRouter
การแยกวิเคราะห์พารามิเตอร์
การแยกวิเคราะห์พารามิเตอร์ใน gin รองรับสามวิธี:พารามิเตอร์เส้นทาง, พารามิเตอร์ URL, พารามิเตอร์ฟอร์ม ต่อไปนี้จะอธิบายทีละรายการพร้อมตัวอย่างโค้ด ง่ายและเข้าใจได้
พารามิเตอร์เส้นทาง
พารามิเตอร์เส้นทางจริงๆ แล้วคือการ encapsulate ฟังก์ชันการแยกวิเคราะห์พารามิเตอร์ของ HttpRouter วิธีการใช้งาน基本上เหมือนกับ 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"))
}
// ตัวอย่างพารามิเตอร์ที่ตั้งชื่อ
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)
}
// ตัวอย่างพารามิเตอร์เส้นทาง
func UserPage(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "filepath is %s", filepath)
}ตัวอย่างที่หนึ่ง
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001ตัวอย่างที่สอง
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngพารามิเตอร์ URL
พารามิเตอร์ URL แบบดั้งเดิม รูปแบบคือ/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)
}ตัวอย่างที่หนึ่ง
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001ตัวอย่างที่สอง
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isพารามิเตอร์ฟอร์ม
ประเภทเนื้อหาของฟอร์มโดยทั่วไปมี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"])
}ตัวอย่างที่หนึ่ง: ใช้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]เมธอด PostForm ค่าเริ่มต้นจะแยกวิเคราะห์ประเภทฟอร์มapplication/x-www-form-urlencodedและmultipart/form-data
ตัวอย่างที่สอง: ใช้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]การแยกวิเคราะห์ข้อมูล
ในกรณีส่วนใหญ่ เรามักจะใช้โครงสร้าง体เพื่อ承载ข้อมูล แทนที่จะแยกวิเคราะห์พารามิเตอร์โดยตรง ใน gin เมธอดที่ใช้สำหรับการผูกข้อมูลหลักคือBind()และShouldBind() ความแตกต่างระหว่างทั้งสองคือ前者内部ก็เรียกShouldBind()โดยตรง แน่นอนว่าเมื่อส่งคืนerrจะทำการตอบสนอง 400 โดยตรง后者则不会 หากต้องการทำการจัดการข้อผิดพลาดอย่างยืดหยุ่นมากขึ้น แนะนำให้เลือก后者
ฟังก์ชันทั้งสองนี้จะทำการ推断โดยอัตโนมัติตามcontent-typeของคำขอว่าจะใช้วิธีใดในการแยกวิเคราะห์
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// เรียกใช้ShouldBindWith()
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // ตอบสนอง 400 badrequest โดยตรง
return err
}
return nil
}หากต้องการเลือกเองสามารถใช้BindWith()และShouldBindWith()ได้ เช่น
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlประเภทการผูกที่ gin รองรับมีดังนี้:
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{}
)ตัวอย่าง
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
// ใช้ShouldBindเพื่อให้ gin推断โดยอัตโนมัติ
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)
}การผูกข้อมูล Json
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"root",
"password":"root"
}'login successfully !การผูกข้อมูลฟอร์ม
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !การผูกข้อมูล URL
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'login failed !ที่นี่จะเกิดข้อผิดพลาด เพราะcontent-typeที่ส่งออกเป็นสตริงว่างเปล่า ไม่สามารถ推断ได้ว่าต้องทำการแยกวิเคราะห์ข้อมูลอย่างไร ดังนั้นเมื่อใช้พารามิเตอร์ URL เราควรระบุวิธีการแยกวิเคราะห์ด้วยตนเอง เช่น:
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 !")
}การผูกหลายครั้ง
โดยทั่วไปเมธอดจะเรียก c.Request.Body เพื่อผูกข้อมูล แต่ไม่สามารถเรียกเมธอดนี้หลายครั้งได้ เช่นc.ShouldBind ไม่สามารถนำกลับมาใช้ใหม่ได้ หากต้องการผูกหลายครั้ง สามารถใช้
c.ShouldBindBodyWith
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// อ่าน c.Request.Body และเก็บผลลัพธ์ใน context
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
//这时, นำ body ที่เก็บใน context มาใช้ซ้ำ
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// สามารถรับรูปแบบอื่นได้
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
}
}TIP
c.ShouldBindBodyWith จะเก็บ body ไว้ใน context ก่อนการผูก ซึ่งจะมีผลกระทบต่อประสิทธิภาพเล็กน้อย หากสามารถผูกได้ด้วยการเรียกครั้งเดียว ก็ไม่ควรใช้เมธอดนี้ มีเพียงบางรูปแบบเท่านั้นที่ต้องการฟีเจอร์นี้ เช่น JSON, XML, MsgPack, ProtoBuf สำหรับรูปแบบอื่น เช่น Query, Form, FormPost, FormMultipart สามารถเรียกc.ShouldBind()ได้หลายครั้งโดยไม่สูญเสียประสิทธิภาพใดๆ
การตรวจสอบข้อมูล
เครื่องมือตรวจสอบในตัวginจริงๆ แล้วคือgithub.com/go-playground/validator/v10 วิธีการใช้งานก็几乎ไม่มีความแตกต่าง Validator
ตัวอย่างง่ายๆ
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)
}
}ทดสอบ
curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"jack1"
}'ส่งออก
invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tagTIP
สิ่งที่ควรสังเกตคือ tag การตรวจสอบของ validator ใน gin คือbinding ในขณะที่ tag การตรวจสอบเมื่อใช้validatorแยกต่างหากคือvalidator
การตอบสนองข้อมูล
การตอบสนองข้อมูลเป็นขั้นตอนสุดท้ายที่ต้องทำในการจัดการอินเทอร์เฟซ backend จะประมวลผลข้อมูลทั้งหมดเสร็จแล้ว ส่งกลับไปยังผู้เรียกผ่านโปรโตคอล HTTP gin ให้การรองรับที่หลากหลายสำหรับการตอบสนองข้อมูล ใช้งานง่ายมาก
ตัวอย่างง่ายๆ
func Hello(c *gin.Context) {
// ส่งคืนข้อมูลรูปแบบสตริงธรรมดา http.StatusOK代表着 200 status code ข้อมูลคือ"Hello world !"
c.String(http.StatusOK, "Hello world !")
}การแสดงผล HTML
TIP
เมื่อโหลดไฟล์ ค่าเริ่มต้นของพาธ root คือพาธโปรเจกต์ นั่นคือพาธที่ไฟล์go.modอยู่ ตัวอย่างindex.htmlด้านล่างคือindex.htmlที่อยู่ในพาธ root แต่โดยทั่วไปแล้วไฟล์เทมเพลตเหล่านี้จะไม่放在พาธ root แต่จะ存放在โฟลเดอร์ทรัพยากรสถิต
func main() {
e := gin.Default()
// โหลดไฟล์ HTML สามารถใช้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{})
}ทดสอบ
curl --location --request GET 'http://localhost:8080/'ส่งคืน
<!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>การตอบสนองอย่างรวดเร็ว
ก่อนหน้านี้มักใช้เมธอดcontext.String()เพื่อตอบสนองข้อมูล นี่คือวิธีการตอบสนองที่原始ที่สุด ส่งคืนสตริงโดยตรง ในginยังมีเมธอดตอบสนองอย่างรวดเร็วที่内置ไว้อีกมากมาย เช่น:
// ใช้Renderเขียน响应头 และทำการแสดงผลข้อมูล
func (c *Context) Render(code int, r render.Render)
// ผลลัพธ์เทมเพลต HTML one ตัว nameคือพาธ html objคือเนื้อหา
func (c *Context) HTML(code int, name string, obj any)
// ใช้JSONสตริงที่ beautified และย่อหน้าเพื่อแสดงผลข้อมูล โดยทั่วไปไม่แนะนำให้ใช้เมธอดนี้ เพราะจะทำให้เกิดการสูญเสียการส่งผ่านมากขึ้น
func (c *Context) IndentedJSON(code int, obj any)
// JSON ที่ปลอดภัย สามารถป้องกันJSON hijacking รายละเอียด了解: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)
// ผลลัพธ์แบบJSONP
func (c *Context) JSONP(code int, obj any)
// ผลลัพธ์แบบJSON
func (c *Context) JSON(code int, obj any)
// ผลลัพธ์แบบJSON จะแปลง unicode code เป็น ASCII code
func (c *Context) AsciiJSON(code int, obj any)
// ผลลัพธ์แบบJSON จะไม่ทำการ escape ตัวอักษรพิเศษ HTML
func (c *Context) PureJSON(code int, obj any)
// ผลลัพธ์แบบXML
func (c *Context) XML(code int, obj any)
// ผลลัพธ์แบบYML
func (c *Context) YAML(code int, obj any)
// ผลลัพธ์แบบTOML
func (c *Context) TOML(code int, obj interface{})
// ผลลัพธ์แบบProtoBuf
func (c *Context) ProtoBuf(code int, obj any)
// ผลลัพธ์แบบString
func (c *Context) String(code int, format string, values ...any)
// เปลี่ยนเส้นทางไปยังตำแหน่งที่ระบุ
func (c *Context) Redirect(code int, location string)
// เขียนdataลงในresponse stream
func (c *Context) Data(code int, contentType string, data []byte)
// อ่านstreamผ่านreaderและ写入ลงในresponse stream
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
// เขียนไฟล์ลงในresponse streamอย่างมีประสิทธิภาพ
func (c *Context) File(filepath string)
// เขียนstreamไฟล์ในfsลงในresponse streamอย่างมีประสิทธิภาพ
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
// เขียนstreamไฟล์ในfsลงในresponse streamอย่างมีประสิทธิภาพ และในฝั่งclientจะดาวน์โหลดด้วยชื่อไฟล์ที่ระบุ
func (c *Context) FileAttachment(filepath, filename string)
// เขียนserver push streamลงในresponse stream
func (c *Context) SSEvent(name string, message any)
// ส่งstream response และส่งคืนboolean value เพื่อใช้ตัดสินว่าclientได้断开กลางstreamหรือไม่
func (c *Context) Stream(step func(w io.Writer) bool) boolสำหรับแอปพลิเคชันส่วนใหญ่ ที่ใช้บ่อยที่สุดคือcontext.JSON ส่วนอื่นๆ ค่อนข้างน้อยกว่า ที่นี่จะไม่ยกตัวอย่าง演示 เพราะค่อนข้างง่ายและเข้าใจได้差不多都是直接调用的事情
การประมวลผลแบบ asynchronous
ใน gin การประมวลผลแบบ asynchronous ต้องใช้ร่วมกับ goroutine ใช้งานง่ายมาก
// copyส่งคืนสำเนาของContextปัจจุบันเพื่อใช้งานอย่างปลอดภัยนอกขอบเขตContextปัจจุบัน สามารถใช้ส่งผ่าน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 ย่อยควรใช้สำเนาContext ไม่ควรใช้Contextเดิม
log.Println("异步处理函数:", ctx.HandlerNames())
}()
log.Println("接口处理函数:", c.HandlerNames())
c.String(http.StatusOK, "hello")
}ทดสอบ
curl --location --request GET 'http://localhost:8080/hello'ส่งออก
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"จะเห็นว่าทั้งสองส่งออกแตกต่างกัน สำเนาในการคัดลอก เพื่อความปลอดภัย ได้ลบค่าขององค์ประกอบหลายอย่างออก
การส่งผ่านไฟล์
การส่งผ่านไฟล์เป็นฟีเจอร์ที่ขาดไม่ได้สำหรับแอปพลิเคชัน Web gin สำหรับการรองรับนี้ยัง封装ได้ง่ายมาก แต่จริงๆ แล้วโดยพื้นฐานแล้วกระบวนการก็差不多กับใช้net/httpแบบเดิม กระบวนการคือการอ่านstreamไฟล์จากrequest body แล้วบันทึกไว้ในท้องถิ่น
การอัปโหลดไฟล์เดียว
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}
func uploadFile(ctx *gin.Context) {
// รับไฟล์
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// บันทึกในท้องถิ่น
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// ส่งคืนผลลัพธ์
ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}ทดสอบ
curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'ผลลัพธ์
upload a.jpg size:1424 byte successfully!TIP
โดยทั่วไป การอัปโหลดไฟล์Methodจะระบุใช้POST บางบริษัทอาจ倾向于ใช้PUT前者เป็นคำขอ HTTP แบบง่าย后者เป็นคำขอ HTTP แบบซับซ้อน ความแตกต่างเฉพาะไม่赘述 หากใช้后者โดยเฉพาะโปรเจกต์แบบแยกfront-end และback-end ต้องทำการ处理cross-domain ที่สอดคล้องกัน และการกำหนดค่าเริ่มต้นของ Gin ไม่รองรับcross-domain การกำหนดค่า Cross-domain
การอัปโหลดหลายไฟล์
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}
func uploadFiles(ctx *gin.Context) {
// รับฟอร์มmultipartที่ ginแยกวิเคราะห์好了
form, _ := ctx.MultipartForm()
// รับรายการไฟล์ตามkey
files := form.File["files"]
// 遍历รายการไฟล์ บันทึกในท้องถิ่น
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload failed")
return
}
}
// ส่งคืนผลลัพธ์
ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}ทดสอบ
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"'ส่งออก
upload 3 files successfully!การดาวน์โหลดไฟล์
เกี่ยวกับส่วนการดาวน์โหลดไฟล์ Gin ได้封装API ของstandard library เดิมอีกครั้ง ทำให้การดาวน์โหลดไฟล์ง่าย异常
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) {
// รับชื่อไฟล์
filename := ctx.Param("filename")
// ส่งคืนไฟล์ที่สอดคล้องกัน
ctx.FileAttachment(filename, filename)
}ทดสอบ
curl --location --request GET 'http://localhost:8080/download/a.jpg'ผลลัพธ์
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รู้สึกว่าง่ายเกินไปหรือไม่ ลองไม่ใช้เมธอดของเฟรมเวิร์ก เขียนกระบวนการด้วยตนเอง
func download(ctx *gin.Context) {
// รับพารามิเตอร์
filename := ctx.Param("filename")
// วัตถุ响应และวัตถุคำขอ
response, request := ctx.Writer, ctx.Request
// เขียน响应头
// response.Header().Set("Content-Type", "application/octet-stream") ส่งผ่านไฟล์เป็นbinary stream
response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // ทำการescapeชื่อไฟล์อย่างปลอดภัย
response.Header().Set("Content-Transfer-Encoding", "binary") // การเข้ารหัสการส่งผ่าน
http.ServeFile(response, request, filename)
}จริงๆ แล้วnet/httpก็封装ได้ดีพอแล้ว
TIP
สามารถตั้งEngine.MaxMultipartMemoryเพื่อตั้งค่าหน่วยความจำสูงสุดสำหรับการส่งผ่านไฟล์ ค่าเริ่มต้นคือ32 << 20 // 32 MB
การจัดการเส้นทาง
การจัดการเส้นทางเป็นส่วนที่สำคัญมากของระบบ ต้องรับประกันว่าทุกคำขอ都能被正确映射到对应的函数上
กลุ่มเส้นทาง
การสร้างกลุ่มเส้นทางคือการ分类อินเทอร์เฟซ อินเทอร์เฟซประเภทที่ต่างกัน对应ฟังก์ชันที่ต่างกัน และง่ายต่อการจัดการมากขึ้น
func Hello(c *gin.Context) {
}
func Login(c *gin.Context) {
}
func Update(c *gin.Context) {
}
func Delete(c *gin.Context) {
}สมมติว่าเรามีอินเทอร์เฟซสี่ตัวข้างต้น ชั่วคราวไม่ต้องสนใจการดำเนินการภายในHello, Loginเป็นหนึ่งกลุ่มUpdate, Deleteเป็นหนึ่งกลุ่ม
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupเมื่อสร้างกลุ่ม เราสามารถลงทะเบียนhandlerให้กับroot routeของกลุ่มได้ แต่ส่วนใหญ่แล้วจะไม่ทำเช่นนั้น
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)
}
}เราแบ่งเป็นสองกลุ่มv1, v2วงเล็บปีกกา{}ในนั้นเป็นเพียง为了规范表名ว่าhandlerที่ลงทะเบียนในวงเล็บปีกกาเป็นของกลุ่มเส้นทางเดียวกัน ในด้านฟังก์ชันไม่มีบทบาทใดๆ เช่นเดียวกัน gin还支持การซ้อนกลุ่ม วิธีการเหมือนกับตัวอย่างข้างต้น ที่นี่จะไม่演示อีก
เส้นทาง 404
โครงสร้างEngineใน gin ให้เมธอดNoRoute来ตั้งค่าว่าจะจัดการอย่างไรเมื่อเข้าถึง URL ที่ไม่มีอยู่ ผู้พัฒนาสามารถเขียนlogicลงในเมธอดนี้ เพื่อให้เรียกโดยอัตโนมัติเมื่อไม่พบเส้นทาง ค่าเริ่มต้นจะส่งคืนstatus code 404
func (engine *Engine) NoRoute(handlers ...HandlerFunc)เรายกตัวอย่างจากข้างต้น
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)
}
// ลงทะเบียนhandler
e.NoRoute(func(context *gin.Context) { // ที่นี่เป็นเพียงการ演示不要在生产环境中直接返回HTML代码
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
log.Fatalln(e.Run(":8080"))
}ส่งคำขอใดๆ
curl --location --request GET 'http://localhost:8080/'<h1>404 Page Not Found</h1>เส้นทาง 405
ในHttp status code 405 หมายความว่าวิธีการคำขอปัจจุบันไม่ได้รับอนุญาต gin ให้เมธอดดังนี้
func (engine *Engine) NoMethod(handlers ...HandlerFunc)เพื่อลงทะเบียนhandler เพื่อให้เรียกโดยอัตโนมัติเมื่อเกิดขึ้น เงื่อนไขล่วงหน้าคือตั้งEngine.HandleMethodNotAllowed = true
func main() {
e := gin.Default()
// ต้อง设置为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>")
})
// ลงทะเบียนhandler
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method not allowed")
})
log.Fatalln(e.Run(":8080"))
}หลังกำหนดค่า好了 header เริ่มต้นของ gin ไม่รองรับคำขอOPTIONทดสอบ一下
curl --location --request OPTIONS 'http://localhost:8080/v2/delete'method not allowed至此配置สำเร็จ
การเปลี่ยนเส้นทาง
การเปลี่ยนเส้นทางใน gin ง่ายมาก เรียกเมธอด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")
}ทดสอบ
curl --location --request GET 'http://localhost:8080/'ส่งออก
helloMiddleware
gin เบาและยืดหยุ่นมาก มีความสามารถในการขยายสูงมาก และการรองรับสำหรับmiddleware ก็เป็นมิตรมาก ใน Gin คำขออินเทอร์เฟซทั้งหมดต้องผ่านmiddleware ผ่านmiddleware ผู้พัฒนาสามารถกำหนดและ实现ฟังก์ชันและlogic ได้มากมาย gin แม้ฟังก์ชันในตัวจะน้อยมาก แต่middleware ที่พัฒนาโดยชุมชนบุคคลที่สามนั้นอุดมสมบูรณ์มาก
Middleware โดยพื้นฐานแล้วคือinterface handler
// HandlerFunc กำหนดhandler ที่ใช้โดยgin middleware เป็นค่าส่งคืน
type HandlerFunc func(*Context)ในบางความหมาย handler ที่对应กับแต่ละคำขอก็เป็นmiddleware เช่นกัน แต่เป็นlocal middleware ที่มีขอบเขตการทำงานเล็กมาก
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}ดูsource code ของ gin ในฟังก์ชันDefault Engineที่ส่งคืนค่าเริ่มต้นใช้middleware เริ่มต้นสองตัวLogger(), Recovery() หากไม่ต้องการใช้middleware เริ่มต้น也可以使用gin.New()แทน
Middleware แบบglobal
Middleware แบบglobal คือขอบเขตการทำงานเป็นglobal คำขอทั้งหมดของระบบทั้งหมดจะผ่านmiddleware นี้
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("全局中间件被执行...")
}
}สร้างclosure function เพื่อสร้างmiddleware ก่อน แล้วลงทะเบียนmiddleware แบบglobal ผ่านEngine.Use()
func main() {
e := gin.Default()
// ลงทะเบียนmiddleware แบบglobal
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"))
}ทดสอบ
curl --location --request GET 'http://localhost:8080/v1/hello'ส่งออก
[GIN-debug] Listening and serving HTTP on :8080
全局中间件被执行...
[GIN] 2022/12/21 - 11:57:52 | 200 | 538.9µs | ::1 | GET "/v1/hello"Middleware แบบlocal
Middleware แบบlocal คือขอบเขตการทำงานเป็นlocal คำขอlocal ของระบบจะผ่านmiddleware นี้ Middleware แบบlocal สามารถลงทะเบียนกับroute เดียวได้ แต่ส่วนใหญ่แล้วจะลงทะเบียนกับgroup route
func main() {
e := gin.Default()
// ลงทะเบียนmiddleware แบบglobal
e.Use(GlobalMiddleware())
// ลงทะเบียนmiddleware แบบlocal ของgroup route
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// ลงทะเบียนmiddleware แบบlocal ของroute เดียว
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}ทดสอบ
curl --location --request POST 'http://localhost:8080/v2/update'ส่งออก
全局中间件被执行...
局部中间件被执行
[GIN] 2022/12/21 - 12:05:03 | 200 | 999.9µs | ::1 | POST "/v2/update"หลักการของMiddleware
การใช้งานและการกำหนดเองของMiddleware ในGin นั้นง่ายมาก หลักการภายในก็ค่อนข้างง่าย เพื่อการเรียนรู้ต่อไป ต้องเข้าใจหลักการภายในอย่างง่าย Middleware ในGin จริงๆ แล้วใช้Chain of Responsibility模式 ContextรักษาHandlersChainหนึ่งตัว โดยพื้นฐานแล้วคือ[]HandlerFuncและindexหนึ่งตัว ประเภทข้อมูลคือint8 ในเมธอดEngine.handlerHTTPRequest(c *Context)มีโค้ดส่วนหนึ่งที่แสดงกระบวนการเรียก: gin หลังจากพบroute ที่สอดคล้องกันในroute tree แล้ว ก็เรียกเมธอดNext()
if value.handlers != nil {
// มอบหมายcall chain ให้Context
c.handlers = value.handlers
c.fullPath = value.fullPath
// เรียกmiddleware
c.Next()
c.writermem.WriteHeaderNow()
return
}การเรียกNext()才是关键 Next()จะ遍历HandlerFuncในhandlersของroute และ执行 ในเวลานี้จะเห็นบทบาทของindexคือการ记录ตำแหน่งการเรียกmiddleware其中handler函数ที่ลงทะเบียนกับroute ที่สอดคล้องกัน也在handlers内 นี่คือเหตุผลว่าทำไม前面会说interface ก็เป็นmiddleware หนึ่งตัว
func (c *Context) Next() {
// +1 ทันทีที่เข้ามา是为了避免陷入递归死循环 ค่าเริ่มต้นคือ-1
c.index++
for c.index < int8(len(c.handlers)) {
// 执行HandlerFunc
c.handlers[c.index](c)
// 执行完毕,index+1
c.index++
}
}แก้ไขlogic ของHello()เล็กน้อย เพื่อตรวจสอบว่าเป็นจริงเช่นนั้นหรือไม่
func Hello(c *gin.Context) {
fmt.Println(c.HandlerNames())
}ส่งออกผลลัพธ์เป็น
[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]จะเห็นว่าลำดับcall chain ของmiddleware คือ:Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello องค์ประกอบสุดท้ายของcall chain才是真正要执行的interface函数前面的都是middleware
TIP
เมื่อลงทะเบียนlocal route มีassertionดังนี้
finalSize := len(group.Handlers) + len(handlers) //middleware总数
assert1(finalSize < int(abortIndex), "too many handlers")其中abortIndex int8 = math.MaxInt8 >> 1值为63即使用系统时路由注册数量不要超过63个
Middleware จับเวลา
หลังจากทราบหลักการของmiddleware ข้างต้นแล้ว ก็สามารถเขียนmiddleware สถิติเวลาคำขออย่างง่ายได้
func TimeMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
// บันทึกเวลาเริ่มต้น
start := time.Now()
// 执行后续call chain
context.Next()
// คำนวณช่วงเวลา
duration := time.Since(start)
// ส่งออกnanoseconds เพื่อสังเกตผลลัพธ์
fmt.Println("请求用时:", duration.Nanoseconds())
}
}
func main() {
e := gin.Default()
// ลงทะเบียนmiddleware แบบglobal middleware จับเวลา
e.Use(GlobalMiddleware(), TimeMiddleware())
// ลงทะเบียนmiddleware แบบlocal ของgroup route
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// ลงทะเบียนmiddleware แบบlocal ของroute เดียว
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}ทดสอบ
curl --location --request GET 'http://localhost:8080/v1/hello'ส่งออก
请求用时:517600Middleware จับเวลาอย่างง่ายก็编写เสร็จแล้ว ต่อจากนี้สามารถ凭凭自己的摸索编写一些功能更实用的middleware
การกำหนดค่าบริการ
เพียงใช้การกำหนดค่าเริ่มต้นนั้นไม่เพียงพอ ในกรณีส่วนใหญ่ต้องแก้ไขการกำหนดค่าบริการจำนวนมากจึงจะตรงตามความต้องการ
การกำหนดค่า Http
สามารถสร้างServer ผ่านnet/httpเพื่อกำหนดค่า Gin เองก็รองรับการใช้Gin เหมือนกับnative API
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())
}การกำหนดค่าทรัพยากรสถิต
ทรัพยากรสถิตในอดีต基本上เป็นส่วนที่ขาดไม่ได้ของserver-side แม้ในปัจจุบันอัตราการใช้กำลังลดลงเรื่อยๆ แต่ยังคงมีระบบจำนวนมากที่ยังใช้สถาปัตยกรรมmonolith
Gin ให้สามเมธอดเพื่อโหลดทรัพยากรสถิต
// โหลดโฟลเดอร์สถิต某一ตัว
func (group *RouterGroup) Static(relativePath, root string) IRoutes
// โหลดfs某一ตัว
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
// โหลดไฟล์สถิต某一ตัว
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutesTIP
relativePath คือrelative path ที่映射到网页URL上 root คือactual path ของไฟล์ในโปรเจกต์
สมมติว่า目录ของโปรเจกต์เป็นดังนี้
root
|
|-- static
| |
| |-- a.jpg
| |
| |-- favicon.ico
|
|-- view
|
|-- htmlfunc main() {
router := gin.Default()
// โหลด目录ไฟล์สถิต
router.Static("/static", "./static")
// โหลด目录ไฟล์สถิต
router.StaticFS("/view", http.Dir("view"))
// โหลดไฟล์สถิต
router.StaticFile("/favicon", "./static/favicon.ico")
router.Run(":8080")
}การกำหนดค่า Cross-domain
Gin เองไม่ได้ทำการ处理ใดๆ สำหรับการกำหนดค่าcross-domain ต้องเขียนmiddleware ด้วยตนเองเพื่อ实现ความต้องการที่สอดคล้องกัน จริงๆ แล้วความยากก็ไม่มาก ผู้ที่熟悉HTTP协议一般都能เขียนได้ logic基本上都是那一套
func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// ในserver-side ของproduction environment โดยทั่วไปจะไม่填* ควร填写指定域名
c.Header("Access-Control-Allow-Origin", origin)
// HTTP METHOD ที่อนุญาตให้ใช้
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// Request Header ที่อนุญาตให้ใช้
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// Response Header ที่อนุญาตให้client访问
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// ต้อง携带认证信息หรือไม่ Credentials可以是cookies、authorization headers หรือTLS client certificates
// เมื่อ设置为true时Access-Control-Allow-Originไม่สามารถ为*ได้
c.Header("Access-Control-Allow-Credentials", "true")
}
// 放行OPTION请求但不执行后续方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 放行
c.Next()
}
}ลงทะเบียนmiddleware เป็นglobal middleware即可
การควบคุมSession
ในยุคปัจจุบันWeb session control ที่流行มีสามแบบcookie, session, JWT
Cookie
ข้อมูลในcookieเก็บในรูปแบบkey-value ในbrowser และ可以直接看到数据ในbrowser
ข้อดี:
- โครงสร้างง่าย
- ข้อมูล持久
ข้อเสีย:
ขนาดจำกัด
เก็บแบบplain text
-容易受到CSRF攻击
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// รับcookieที่สอดคล้องกัน
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// ตั้งcookie พารามิเตอร์: key, val, เวลา存在目录域名是否允许他人通过js访问cookie仅http
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}cookie ล้วนๆ ในห้าหกปีก่อนใช้比较多 แต่ผู้เขียน一般很少ใช้cookie ล้วนๆ来做session control这样做确实不太安全
Session
sessionเก็บ在server-side แล้วส่งcookieหนึ่งตัวเก็บ在browser中cookie中เก็บ的是session_id之后每次请求server通过session_id可以获取对应的session信息
ข้อดี:
- เก็บ在server-side เพิ่มความปลอดภัยง่ายต่อการจัดการ
ข้อเสีย:
- เก็บ在server-side เพิ่มserver-side overhead ลดประสิทธิภาพ
- 基于cookie识别ไม่ปลอดภัย -认证信息在分布式情况下不同步
Session กับCookie是分不开的每次要用到Session默认就是要用到Cookie了Gin默认是不支持Session的因为Cookie是Http协议里面的内容但Session不是不过有第三方middleware支持安装依赖即可仓库地址:gin-contrib/sessions: Gin middleware for session management (github.com)
go get github.com/gin-contrib/sessionsรองรับcookie, Redis, MongoDB, GORM, PostgreSQL
func main() {
r := gin.Default()
// สร้างstorage engine基于Cookie
store := cookie.NewStore([]byte("secret"))
// ตั้งSession middleware mysession即session名称也是cookie的名称
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
// เริ่มต้นsession
session := sessions.Default(c)
var count int
// รับค่า
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
// ตั้งค่า
session.Set("count", count)
// บันทึก
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}一般不推荐通过Cookie存储Sesison推荐使用Redis其他例子还请自行去官方仓库了解
JWT
ข้อดี:
- 基于JSON多语言通用
- สามารถเก็บ非敏感信息
- ใช้พื้นที่น้อยง่ายต่อการส่งผ่าน
- Server-side ไม่ต้องเก็บ利于分布式拓展
ข้อเสีย:
- ปัญหาToken刷新 -一旦签发则无法主动控制
自从前端革命以来前端程序员不再只是一个"写页面的"前后端分离的趋势愈演愈烈JWT是最适合前后端分离和分布式系统来做会话控制的具有很大的天然优势考虑到JWT已经完全脱离Gin的内容且没有任何middleware支持因为JWT本身就是不局限于任何框架任何语言在这里就不作细致的讲解可以前往另一篇文档:JWT
การจัดการLog
Gin默认使用的log middleware采用os.Stdoutมีเพียงฟังก์ชันพื้นฐานที่สุดเพราะGin只专注于Web service ในกรณีส่วนใหญ่ควรใช้log framework ที่成熟กว่า不过这并不在本章的讨论范围内而且Gin的拓展性很高可以很轻易的整合其他框架这里只讨论其自带的log service
สีConsole
gin.DisableConsoleColor() // ปิดสีlog console除了在开发的时候大多数时候都不建议开启此项
Log写入ไฟล์
func main() {
e := gin.Default()
// ปิดสีconsole
gin.DisableConsoleColor()
// สร้างสองไฟล์log
log1, _ := os.Create("info1.log")
log2, _ := os.Create("info2.log")
// บันทึก进สองไฟล์logพร้อมกัน
gin.DefaultWriter = io.MultiWriter(log1, log2)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}log ที่内置ของginรองรับการ写入หลายไฟล์แต่เนื้อหา相同กัน使用起来不太方便并且不会将请求log写入文件中
func main() {
router := gin.New()
// LoggerWithFormatter middleware จะ写入log进gin.DefaultWriter
// ค่าเริ่มต้นgin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
//TODO写入对应文件的逻辑
......
// ส่งออกcustom format
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")
}ผ่านcustom middleware สามารถ实现log写入文件中
รูปแบบLog调试Route
这里修改的只是启动时输出路由信息的的日志
func main() {
e := gin.Default()
gin.SetMode(gin.DebugMode)
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
if gin.IsDebugging() {
log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
}
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}ส่งออก
2022/12/21 17:19:13 路由 GET /hello main.Hello 3สรุป: Gin算是Go语言Web框架中最易学习的一种因为Gin真正做到了职责最小化只是单纯的负责Web service其他的认证逻辑数据缓存等等功能都交给开发者自行完成相比于那些大而全的框架轻量简洁的Gin对于初学者而言更适合也更应该去学习因为Gin并没有强制使用某一种规范项目该如何构建采用什么结构都需要自行斟酌对于初学者而言更能锻炼能力
