Skip to content

Gin

الوثائق الرسمية: Gin Web Framework (gin-gonic.com)

عنوان المستودع: 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). يمتلك واجهة برمجة تطبيقات مشابهة لـ martini، ولكن بأداء أفضل بكثير، بفضل httprouter، السرعة أعلى بـ 40 مرة. إذا كنت بحاجة إلى أداء عالي وإنتاجية جيدة، ستحب Gin بالتأكيد. بالمقارنة مع Iris و Beego، تميل Gin إلى أن تكون إطار عمل خفيف الوزن، مسؤولة فقط عن جزء الويب، تسعى لأقصى أداء للتوجيه، قد لا تكون الميزات كاملة، لكنها خفيفة وسهلة التوسع، وهذه ميزتها. لذلك، من بين جميع أطر الويب، تعتبر Gin الأسهل للبدء والتعلم.

الميزات

  • سريع: توجيه قائم على شجرة Radix، استهلاك ذاكرة صغير. لا انعكاس. أداء API قابل للتنبؤ.
  • دعم الوسيطة: يمكن معالجة طلبات HTTP الواردة بواسطة سلسلة من الوسائط والعمليات النهائية. على سبيل المثال: Logger، Authorization، GZIP، العملية النهائية DB.
  • معالجة الأعطال: يمكن لـ Gin التقاط panic الذي يحدث في طلب HTTP واستعادته. بهذه الطريقة، سيعمل الخادم الخاص بك دائماً.
  • التحقق من JSON: يمكن لـ Gin تحليل والتحقق من JSON المطلوب، مثل التحقق من وجود القيم المطلوبة.
  • مجموعات التوجيه: تنظيم التوجيه بشكل أفضل. هل تحتاج إلى تخويل، إصدارات API مختلفة... بالإضافة إلى ذلك، يمكن تداخل هذه المجموعات بدون حد وبنفس الأداء.
  • إدارة الأخطاء: يوفر Gin طريقة مريحة لجمع جميع الأخطاء التي تحدث أثناء طلب HTTP. في النهاية، يمكن للوسيطة كتابتها في ملف السجل، قاعدة البيانات وإرسالها عبر الشبكة.
  • العرض المدمج: يوفر Gin واجهة برمجة تطبيقات سهلة الاستخدام لعرض JSON و XML و HTML.
  • قابلية التوسع: إنشاء وسيطة جديدة أمر سهل جداً.

التثبيت

حتى تاريخ 2022/11/22، أدنى إصدار من Go يدعمه gin هو 1.16، يُنصح باستخدام go mod لإدارة تبعيات المشروع.

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

الاستيراد

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

البدء السريع

go
package main

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

func main() {
   engine := gin.Default() // إنشاء محرك gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // تشغيل الخادم، الاستماع الافتراضي على localhost:8080
}

طلب URL

http
GET localhost:8080/ping

الاستجابة

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

الوثائق

في الواقع، وثائق Gin الرسمية لا تحتوي على العديد من الدروس، معظمها مجرد مقدمة واستخدام أساسي وبعض الأمثلة، لكن تحت منظمة gin-gonic/، هناك مستودع gin-gonic/examples، وهو مستودع أمثلة gin يتم صيانته من قبل المجتمع. كلها باللغة الإنجليزية، ووقت التحديث ليس متكرراً جداً، تعلم المؤلف أيضاً إطار gin من هنا ببطء.

عنوان مستودع الأمثلة: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

قبل البدء، يُنصح بقراءة HttpRouter: HttpRouter

تحليل المعاملات

يدعم تحليل المعاملات في gin ثلاث طرق: معاملات التوجيه، معاملات URL، معاملات النموذج، سيتم شرح كل منها أدناه مع أمثلة برمجية، وهي بسيطة وسهلة الفهم.

معاملات التوجيه

معاملات التوجيه هي في الواقع تغليف لوظيفة تحليل معاملات HttpRouter، وطريقة الاستخدام متطابقة تقريباً مع 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"))
}

// مثال على المعامل المسمى
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)
}

المثال الأول

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

المثال الثاني

bash
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.

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)
}

المثال الأول

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

المثال الثاني

bash
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.

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"])
}

المثال الأول: استخدام 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]

طريقة PostForm تحلل افتراضياً نماذج من نوع application/x-www-form-urlencoded و multipart/form-data.

المثال الثاني: استخدام 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]

تحليل البيانات

في معظم الحالات، نستخدم الهياكل لحمل البيانات بدلاً من تحليل المعاملات مباشرة. في gin، الطرق الرئيسية لربط البيانات هي Bind() و ShouldBind()، والفرق بينهما هو أن الأولى تستدعي ShouldBind() داخلياً، وعندما تعود بـ err، ستستجيب مباشرة بـ 400، بينما الثانية لا تفعل ذلك. إذا كنت تريد معالجة الأخطاء بمرونة أكبر، يُنصح باختيار الثانية. هاتان الدالتان ستستنتجان تلقائياً طريقة التحليل بناءً على content-type للطلب.

go
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()، على سبيل المثال

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

أنواع الربط التي يدعمها gin لها عدة تطبيقات كالتالي:

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{}
)

مثال

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
    // استخدام 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

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

ربط بيانات النموذج

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

ربط بيانات URL

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

عند الوصول إلى هنا سيحدث خطأ، لأن content-type المخرج هنا هو سلسلة فارغة، لا يمكن الاستنتاج كيفية تحليل البيانات. لذلك عند استخدام معاملات URL، يجب تحديد طريقة التحليل يدوياً، على سبيل المثال:

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 !")
}

الربط المتعدد

الطرق العامة تربط البيانات من خلال استدعاء طريقة c.Request.Body، لكن لا يمكن استدعاء هذه الطريقة عدة مرات، مثل c.ShouldBind، غير قابل لإعادة الاستخدام، إذا أردت الربط عدة مرات، يمكنك استخدام

c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // قراءة c.Request.Body وتخزين النتيجة في السياق.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // في هذه اللحظة، إعادة استخدام body المخزن في السياق.
  }
  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 في السياق قبل الربط. هذا سيؤثر قليلاً على الأداء، إذا كان يمكن إتمام الربط باستدعاء واحد، فلا تستخدم هذه الطريقة. فقط بعض الصيغ تحتاج هذه الميزة، مثل JSON، XML، MsgPack، ProtoBuf. للصيغ الأخرى، مثل Query، Form، FormPost، FormMultipart يمكن استدعاء c.ShouldBind() عدة مرات دون أي خسارة في الأداء.

التحقق من البيانات

أداة التحقق المدمجة في gin هي في الواقع github.com/go-playground/validator/v10، وطريقة الاستخدام ليس بها فرق يذكر، Validator

مثال بسيط

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)
   }
}

اختبار

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' tag

TIP

شيء واحد يجب ملاحظته هو أن tag التحقق في gin هو binding، بينما عند استخدام validator بمفرده يكون tag التحقق هو validator

استجابة البيانات

استجابة البيانات هي الخطوة الأخيرة في معالجة الواجهة، بعد أن يعالج الخلفية جميع البيانات، يعيدها للمستدعي عبر بروتوكول HTTP، يوفر gin دعماً مدمجاً غنياً لاستجابة البيانات، الاستخدام بسيط وواضح، سهل التعلم جداً.

مثال بسيط

go
func Hello(c *gin.Context) {
    // إرجاع بيانات بتنسيق سلسلة نصية بحتة، http.StatusOK يمثل رمز الحالة 200، البيانات هي "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

عرض HTML

TIP

عند تحميل الملفات، المسار الجذر الافتراضي هو مسار المشروع، أي المسار الذي يوجد فيه ملف go.mod، ملف index.html في المثال أدناه يقع في index.html تحت المسار الجذر، لكن عموماً هذه الملفات القالب لا توضع في المسار الجذر، بل تخزن في مجلد الموارد الثابتة

go
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/'

الاستجابة

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>

الاستجابة السريعة

استخدمنا سابقاً طريقة context.String() كثيراً للاستجابة بالبيانات، هذه هي طريقة الاستجابة الأصلية، تعيد سلسلة نصية مباشرة، في الواقع gin يدمج العديد من طرق الاستجابة السريعة مثل:

go
// استخدام Render لكتابة رأس الاستجابة، وتنفيذ عرض البيانات
func (c *Context) Render(code int, r render.Render)

// عرض قالب HTML، name هو مسار html، obj هو المحتوى
func (c *Context) HTML(code int, name string, obj any)

// عرض البيانات بسلسلة JSON مفصولة بمسافات بادئة، عموماً لا يُنصح باستخدام هذه الطريقة، لأنها ستسبب استهلاك نقل أكبر.
func (c *Context) IndentedJSON(code int, obj any)

// JSON آمن، يمكن منع اختطاف JSON، للتفاصيل: 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 إلى ASCII
func (c *Context) AsciiJSON(code int, obj any)

// العرض بطريقة JSON، لن يهرب من الأحرف الخاصة لـ 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 في تيار الاستجابة
func (c *Context) Data(code int, contentType string, data []byte)

// قراءة تيار من reader وكتابته في تيار الاستجابة
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// كتابة ملف في تيار الاستجابة بكفاءة
func (c *Context) File(filepath string)

// كتابة تيار ملف من fs في تيار الاستجابة بكفاءة
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// كتابة تيار ملف من fs في تيار الاستجابة بكفاءة، وسيتم تنزيله باسم الملف المحدد على العميل
func (c *Context) FileAttachment(filepath, filename string)

// كتابة تيار الدفع من الخادم في تيار الاستجابة
func (c *Context) SSEvent(name string, message any)

// إرسال استجابة تيار وإرجاع قيمة منطقية، لتحديد ما إذا كان العميل قد انقطع في منتصف التيار
func (c *Context) Stream(step func(w io.Writer) bool) bool

بالنسبة لمعظم التطبيقات، الأكثر استخداماً هو context.JSON، والباقي أقل نسبياً، لن نضرب أمثلة هنا لأنها بسيطة وسهلة الفهم، تقريباً كلها مجرد استدعاء مباشر.

المعالجة غير المتزامنة

في gin، المعالجة غير المتزامنة تحتاج لاستخدام goroutine، الاستخدام بسيط جداً.

go
// copy تعيد نسخة من Context الحالي للاستخدام الآمن خارج نطاق Context الحالي، يمكن تمريرها لـ 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() {
    // الكوروتين الفرعي يجب أن يستخدم نسخة من Context، لا يجب استخدام Context الأصلي
    log.Println("دالة المعالجة غير المتزامنة: ", ctx.HandlerNames())
  }()
  log.Println("دالة معالجة الواجهة: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

اختبار

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

المخرجات

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"

يمكن ملاحظة أن المخرجين مختلفين، النسخة عند النسخ، من أجل السلامة، حذفت قيم العديد من العناصر.

نقل الملفات

نقل الملفات وظيفة لا غنى عنها في تطبيقات الويب، دعم gin لها مغلف بشكل بسيط جداً، لكن في الواقع العملية مشابهة لاستخدام net/http الأصلي. العملية هي قراءة تيار الملف من جسم الطلب، ثم حفظه محلياً.

رفع ملف واحد

go
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 معقد، الفرق المحدد لن يُذكر، إذا استخدمت الأخير، خاصة في المشاريع المنفصلة بين الأمام والخلف، تحتاج لمعالجة cross-domain المقابلة، و Gin لا يدعم cross-domain في التكوين الافتراضي تكوين cross-domain.

رفع ملفات متعددة

go
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()
  // الحصول على قائمة الملفات المقابلة حسب المفتاح
  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 للمكتبة القياسية الأصلية، مما يجعل تنزيل الملفات سهلاً بشكل غير عادي.

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) {
    // الحصول على اسم الملف
  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

ألا تظن أنه بسيط للغاية، لنجرب كتابة العملية بأنفسنا بدون استخدام طريقة الإطار

go
func download(ctx *gin.Context) {
   // الحصول على المعاملات
   filename := ctx.Param("filename")

   // كائن الاستجابة والطلب
   response, request := ctx.Writer, ctx.Request
   // كتابة رأس الاستجابة
   // response.Header().Set("Content-Type", "application/octet-stream") نقل الملف كتيار ثنائي
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // تهريب آمن لاسم الملف
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // ترميز النقل
   http.ServeFile(response, request, filename)
}

في الواقع net/http مغلف بشكل جيد كافٍ أيضاً

TIP

يمكن تعيين الحد الأقصى للذاكرة لنقل الملفات عبر Engine.MaxMultipartMemory، الافتراضي هو 32 << 20 // 32 MB

إدارة التوجيه

إدارة التوجيه جزء مهم جداً في النظام، تحتاج للتأكد من أن كل طلب يمكن تعيينه للدالة المقابلة بشكل صحيح.

مجموعات التوجيه

إنشاء مجموعة توجيه هو تصنيف الواجهات، واجهات فئات مختلفة تقابل وظائف مختلفة، وأسهل في الإدارة.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

لنفترض أن لدينا الواجهات الأربع أعلاه، مؤقتاً بغض النظر عن تنفيذها الداخلي، Hello، Login مجموعة، Update، Delete مجموعة.

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

عند إنشاء مجموعة، يمكننا أيضاً تسجيل معالج للمسار الجذر للمجموعة، لكن في معظم الأحيان لا يُفعل ذلك.

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)
  }
}

قسمناها إلى مجموعتين v1، v2، الأقواس المعقوفة {} هنا فقط للتنسيق، لتوضيح أن المعالجات المسجلة داخل الأقواس تنتمي لنفس مجموعة التوجيه، وظيفياً ليس لها أي تأثير. وبالمثل، gin يدعم أيضاً المجموعات المتداخلة، الطريقة مشابهة للمثال أعلاه، لن نعرضها هنا.

توجيه 404

يوفر الهيكل Engine في gin طريقة NoRoute، لتعيين كيفية المعالجة عند عدم وجود URL المطلوب، يمكن للمطورين كتابة المنطق في هذه الطريقة، ليُستدعى تلقائياً عند عدم العثور على التوجيه، افتراضياً سيعيد رمز الحالة 404

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

لنأخذ المثال السابق

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)
   }
   // تسجيل المعالج
   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، 405 تعني أن نوع طريقة الطلب الحالي غير مسموح، يوفر gin الطريقة التالية

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

لتسجيل معالج، ليُستدعى تلقائياً عند الحدوث، بشرط تعيين Engine.HandleMethodNotAllowed = true.

go
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>")
   })
   // تسجيل المعالج
   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().

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")
}

اختبار

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

المخرجات

hello

الوسيطة

gin خفيف ومرن للغاية، قابلية التوسع عالية جداً، ودعم الوسائط ودود جداً. في Gin، جميع طلبات الواجهات تمر عبر الوسائط، من خلال الوسائط، يمكن للمطورين تنفيذ العديد من الوظائف والمنطق بأنفسهم، gin رغم أن وظائفه المدمجة قليلة، لكن الوسائط التوسعية من طرف ثالث غنية جداً.

الوسيطة في جوهرها هي في الواقع معالج واجهة

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

بمعنى ما، كل معالج يقابل طلب هو أيضاً وسيطة، لكنه وسيطة محلية بنطاق صغير جداً.

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

بالنظر إلى كود مصدر gin، في دالة Default، Engine الافتراضي المُعاد يستخدم وسيطين افتراضيين Logger()، Recovery()، إذا لم ترغب في استخدام الوسائط الافتراضية يمكنك استخدام gin.New() بدلاً من ذلك.

الوسيطة العامة

الوسيطة العامة أي نطاقها عام، جميع طلبات النظام بأكمله ستمر عبر هذه الوسيطة.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("تم تنفيذ الوسيطة العامة...")
   }
}

أولاً أنشئ دالة closure لإنشاء الوسيطة، ثم سجل الوسيطة العامة عبر Engine.Use().

go
func main() {
   e := gin.Default()
   // تسجيل الوسيطة العامة
   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"

الوسيطة المحلية

الوسيطة المحلية أي نطاقها محلي، طلبات محلية في النظام ستمر عبر هذه الوسيطة. يمكن تسجيل الوسيطة المحلية على مسار واحد، لكن في كثير من الأحيان تُسجل على مجموعة مسارات.

go
func main() {
   e := gin.Default()
   // تسجيل الوسيطة العامة
   e.Use(GlobalMiddleware())
   // تسجيل وسيطة محلية لمجموعة المسارات
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // تسجيل وسيطة محلية لمسار واحد
      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"

مبدأ الوسيطة

استخدام وتخصيص الوسائط في Gin سهل للغاية، مبدأها الداخلي بسيط نسبياً أيضاً، للتعلم اللاحق، تحتاج لفهم مبدأها الداخلي باختصار. الوسائط في Gin تستخدم في الواقع نمط سلسلة المسؤولية، Context يحتفظ بـ HandlersChain، في جوهرها []HandlerFunc، و index، نوع بياناته int8. في طريقة Engine.handlerHTTPRequest(c *Context)، هناك قطعة كود توضح عملية الاستدعاء: بعد أن يجد gin المسار المقابل في شجرة التوجيه، يستدعي طريقة Next().

go
if value.handlers != nil {
   // تعيين سلسلة الاستدعاء لـ Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // استدعاء الوسيطة
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

استدعاء Next() هو المفتاح، Next() سيعبر HandlerFunc في handlers الخاص بالمسار وينفذها، هنا يمكن رؤية أن وظيفة index هي تسجيل موقع استدعاء الوسيطة. من بينها، دالة الواجهة المسجلة للمسار المقابل موجودة أيضاً في handlers، وهذا لماذا قلنا سابقاً أن الواجهة هي أيضاً وسيطة.

go
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++
   }
}

لنعدّل منطق Hello() للتحقق مما إذا كان هذا صحيحاً

go
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]

يمكن ملاحظة أن ترتيب سلسلة استدعاء الوسائط هو: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello، آخر عنصر في سلسلة الاستدعاء هو دالة الواجهة الفعلية المراد تنفيذها، ما قبلها كلها وسائط.

TIP

عند تسجيل المسار المحلي، هناك تأكيد كالتالي

go
finalSize := len(group.Handlers) + len(handlers) // إجمالي الوسائط
assert1(finalSize < int(abortIndex), "too many handlers")

حيث abortIndex int8 = math.MaxInt8 >> 1 قيمته 63، أي عند استخدام النظام لا يجب أن يتجاوز عدد تسجيلات المسار 63.

وسيطة المؤقت

بعد معرفة مبدأ الوسائط أعلاه، يمكن كتابة وسيطة بسيطة لإحصاء وقت الطلب.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // تسجيل وقت البدء
      start := time.Now()
      // تنفيذ سلسلة الاستدعاء اللاحقة
      context.Next()
      // حساب الفاصل الزمني
      duration := time.Since(start)
      // إخراج بالنانوثانية، لتسهيل ملاحظة النتيجة
      fmt.Println("وقت الطلب: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // تسجيل الوسيطة العامة، وسيطة المؤقت
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // تسجيل وسيطة محلية لمجموعة المسارات
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // تسجيل وسيطة محلية لمسار واحد
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

اختبار

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

المخرجات

وقت الطلب:  517600

تمت كتابة وسيطة مؤقت بسيطة، لاحقاً يمكن استكشاف كتابة وسائط بوظائف أكثر عملية.

تهيئة الخدمة

استخدام التهيئة الافتراضية وحدها ليس كافياً، في معظم الحالات تحتاج لتعديل العديد من تهيئات الخدمة لتلبية المتطلبات.

تهيئة Http

يمكن إنشاء Server عبر net/http، Gin نفسه يدعم استخدام Gin مثل API الأصلي.

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())
}

تهيئة الموارد الثابتة

الموارد الثابتة في الماضي كانت جزءاً لا غنى عنه من الخادم، رغم أن نسبة استخدامها آخذة في التناقص حالياً، لكن لا يزال هناك العديد من الأنظمة التي تستخدم بنية أحادية.

يوفر Gin ثلاث طرق لتحميل الموارد الثابتة

go
// تحميل مجلد ثابت معين
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) IRoutes

TIP

relativePath هو المسار النسبي المعين إلى URL للصفحة، root هو المسار الفعلي للملف في المشروع

لنفترض أن هيكل المشروع كالتالي

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func 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، تحتاج لكتابة وسيطة بنفسك لتنفيذ المتطلبات المقابلة، في الواقع الصعوبة ليست كبيرة، أي شخص على دراية ببروتوكول HTTP يمكنه كتابتها، المنطق عموماً نفس الشيء.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // في بيئة الإنتاج عادة لا يملأ *، يجب ملء اسم النطاق المحدد
         c.Header("Access-Control-Allow-Origin", origin)
         // طرق HTTP المسموح بها
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // رؤوس الطلبات المسموح بها
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // رؤوس الاستجابة المسموح للعميل بالوصول إليها
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // هل يحتاج لحمل معلومات المصادقة Credentials يمكن أن تكون cookies، رؤوس المصادقة أو شهادات عميل TLS
         // عند تعيينه true، Access-Control-Allow-Origin لا يمكن أن يكون *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // السماح بطلب OPTION، لكن لا تنفذ الطرق اللاحقة
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // السماح بالمرور
      c.Next()
   }
}

سجل الوسيطة كوسيطة عامة

التحكم في الجلسة

في العصر الحالي، هناك ثلاث طرق شائعة للتحكم في جلسات الويب، cookie، session، JWT.

المعلومات في cookie تُخزن في المتصفح على شكل أزواج مفتاح-قيمة، ويمكن رؤية البيانات مباشرة في المتصفح

المميزات:

  • هيكل بسيط
  • بيانات دائمة

العيوب:

  • حجم محدود
  • تخزين بنص عادي
  • عرضة لهجمات CSRF
go
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، وقت الوجود، المسار، النطاق، هل يسمح للآخرين بالوصول لـ cookie عبر js، http فقط
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

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

    router.Run()
}

cookie وحده كان يُستخدم كثيراً قبل خمس أو ست سنوات، لكن المؤلف نادراً ما يستخدم cookie وحده للتحكم في الجلسة، هذا غير آمن بالفعل.

Session

تُخزن session في الخادم، ثم يُرسل cookie يُخزن في المتصفح، ما يُخزن في cookie هو session_id، بعد ذلك كل طلب للخادم يمكنه الحصول على معلومات session المقابلة عبر session_id

المميزات:

  • التخزين في الخادم، يزيد الأمان، يسهل الإدارة

العيوب:

  • التخزين في الخادم، يزيد عبء الخادم، يقلل الأداء
  • يعتمد على cookie للتعرف، غير آمن
  • معلومات المصادقة لا تتزامن في البيئة الموزعة

Session و Cookie لا ينفصلان، كلما استخدمت Session، افتراضياً ستستخدم Cookie. Gin لا يدعم Session افتراضياً، لأن Cookie جزء من بروتوكول Http، لكن Session ليست كذلك، لكن هناك وسيطة طرف ثالث تدعمها، فقط ثبت التبعية، عنوان المستودع: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

يدعم cookie، Redis، MongoDB، GORM، PostgreSQL

go
func main() {
   r := gin.Default()
   // إنشاء محرك تخزين قائم على Cookie
   store := cookie.NewStore([]byte("secret"))
   // تعيين وسيطة Session، 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")
}

عموماً لا يُنصح بتخزين Session عبر Cookie، يُنصح باستخدام Redis، للأمثلة الأخرى يرجى مراجعة المستودع الرسمي.

JWT

المميزات:

  • مبني على JSON، مشترك بين لغات متعددة
  • يمكن تخزين معلومات غير حساسة
  • حجم صغير جداً، سهل النقل
  • الخادم لا يحتاج للتخزين، مناسب للتوسع الموزع

العيوب:

  • مشكلة تحديث Token
  • بمجرد الإصدار لا يمكن التحكم فيه

منذ ثورة الواجهات الأمامية، مبرمجو الواجهات لم يعودوا مجرد "كتاب صفحات"، اتجاه الفصل بين الأمام والخلف يزداد قوة، JWT هو الأنسب للتحكم في جلسات الأنظمة المنفصلة بين الأمام والخلف والأنظمة الموزعة، له مزايا طبيعية كبيرة. بالنظر إلى أن JWT انفصل تماماً عن محتوى Gin، ولا توجد وسيطة تدعمه، لأن JWT نفسه غير محوط بأي إطار أو لغة، لن نشرحه بالتفصيل هنا، يمكن الذهاب لوثيقة أخرى: JWT

إدارة السجلات

وسيطة السجلات الافتراضية في Gin تستخدم os.Stdout، لها فقط الوظائف الأساسية،毕竟 Gin يركز فقط على خدمة الويب، في معظم الحالات يجب استخدام إطار سجلات أكثر نضجاً، لكن هذا ليس ضمن نطاق هذه الفصل، و قابلية توسع Gin عالية، يمكن دمج أطر أخرى بسهولة، هنا نناقش فقط خدمة السجلات المدمجة.

ألوان وحدة التحكم

go
gin.DisableConsoleColor() // تعطيل ألوان سجلات وحدة التحكم

إلا في وقت التطوير، في معظم الأوقات لا يُنصح بتفعيل هذا

كتابة السجلات لملف

go
func main() {
  e := gin.Default()
    // تعطيل ألوان وحدة التحكم
  gin.DisableConsoleColor()
    // إنشاء ملفي سجلات
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // التسجيل في ملفي السجلات معاً
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

السجلات المدمجة في gin تدعم الكتابة لملفات متعددة، لكن المحتوى هو نفسه، استخدامها غير مريح، ولن تكتب سجلات الطلب في الملف.

go
func main() {
  router := gin.New()
  // وسيطة LoggerWithFormatter ستكتب السجلات إلى gin.DefaultWriter
  // افتراضياً gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO منطق الكتابة للملف المقابل
        ......
    // إخراج صيغة مخصصة
    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")
}

من خلال تخصيص الوسيطة، يمكن تنفيذ كتابة السجلات لملف

صيغة سجل توجيه التصحيح

ما يتم تعديله هنا هو فقط سجل معلومات التوجيه المُخرج عند البدء

go
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، لأن Gin يحقق فعلاً تقليل المسؤوليات لأقصى حد، مسؤول فقط عن خدمة الويب، منطق المصادقة، تخزين البيانات وغيرها من الوظائف يتركها للمطور لإنجازها، بالمقارنة مع تلك الأطر الشاملة، Gin الخفيف والبسيط أنسب للمبتدئين للتعلم، لأن Gin لا يفرض استخدام معيار معين، كيفية بناء المشروع، أي هيكل يجب اعتماده، كلها تحتاج للتفكير بنفسك، للمبتدئين هذا يطور القدرات بشكل أفضل.

Golang تم تحريره بواسطة www.golangdev.cn