Skip to content

Gin

Resmi Dokümantasyon: Gin Web Framework (gin-gonic.com)

Depo Adresi: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

Resmi Örnekler: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

Giriş

Gin Go (Golang) ile yazılmış bir web framework'tür. martini benzeri bir API'ye sahiptir ve performansı çok daha iyidir. httprouter sayesinde hız 40 kat artırılmıştır. Performans ve iyi bir üretkenlik istiyorsanız Gin'i seveceksiniz. Gin Iris ve Beego'ya göre daha hafif bir framework olma eğilimindedir. Yalnızca web kısmından sorumludur. Maksimum yönlendirme performansı peşindedir. Fonksiyonlar o kadar da tam değildir ancak genişletilebilir olması ve hafif olması avantajıdır. Bu da onun avantajıdır. Bu nedenle tüm web framework'leri arasında Gin en kolay öğrenilen ve kullanılan framework'tür.

Özellikler

  • Hızlı: Radix tree tabanlı yönlendirme. Küçük bellek ayak izi. Yansıma yok. Öngörülebilir API performansı.
  • Middleware Desteği: Gelen HTTP istekleri bir dizi middleware ve son işlem tarafından işlenebilir. Örneğin: Logger, Authorization, GZIP, son işlem DB.
  • Crash İşleme: Gin bir HTTP isteğinde oluşan panic'i yakalayabilir ve recover edebilir. Böylece sunucunuz her zaman kullanılabilir olur.
  • JSON Doğrulama: Gin istek JSON'unu ayrıştırabilir ve doğrulayabilir. Örneğin gerekli değerlerin varlığını kontrol edebilir.
  • Yönlendirme Grupları: Yönlendirmeleri daha iyi organize edin. Yetkilendirme gerekiyor mu, farklı API sürümleri... Ayrıca bu gruplar performans düşüşü olmadan sınırsız iç içe yerleştirilebilir.
  • Hata Yönetimi: Gin HTTP istekleri sırasında oluşan tüm hataları toplamak için uygun bir yöntem sağlar. Son olarak middleware bunları log dosyalarına yazabilir veritabanına kaydedebilir ve ağ üzerinden gönderebilir.
  • Yerleşik Render: Gin JSON, XML ve HTML render için kullanımı kolay API'ler sağlar.
  • Genişletilebilirlik: Yeni bir middleware oluşturmak çok basittir.

Kurulum

Şu anda 2022/11/22 itibarıyla gin'in desteklediği minimum Go sürümü 1.16'dır. Proje bağımlılıklarını yönetmek için go mod kullanılması önerilir.

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

İçe Aktarma

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

Hızlı Başlangıç

go
package main

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

func main() {
   engine := gin.Default() // gin motorunu oluştur
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // sunucuyu başlat. varsayılan olarak localhost:8080'i dinler
}

İstek URL'si

http
GET localhost:8080/ping

Dönüş

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

Dokümantasyon

Aslında Gin resmi dokümantasyonunda çok fazla ders yoktur. Çoğu sadece bazı tanıtımlar temel kullanımlar ve örneklerdir. Ancak gin-gonic/ organizasyonu altında gin-gonic/examples deposu vardır. Bu topluluk tarafından birlikte维护 edilen bir gin örnek deposudur. Hepsi İngilizcedir ve güncelleme sıklığı çok yüksek değildir. Yazar da yavaş yavaş gin framework'ünü buradan öğrenmektedir.

Örnek Depo Adresi: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Başlamadan önce HttpRouter'ı okumanız önerilir: HttpRouter

Parametre Ayrıştırma

gin'de parametre ayrıştırma toplamda üç şekilde desteklenir: yönlendirme parametreleri, URL parametreleri, form parametreleri. Aşağıda her biri açıklanacak ve kod örnekleri ile birlikte verilecektir. Oldukça basit ve anlaşılırdır.

Yönlendirme Parametreleri

Yönlendirme parametreleri aslında HttpRouter'ın parametre ayrıştırma işlevini kapsüller. Kullanım yöntemi temel olarak HttpRouter ile aynıdır.

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

// Adlandırılmış parametre örneği
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)
}

// Yol parametresi örneği
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

Örnek Bir

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

Örnek İki

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

URL Parametreleri

Geleneksel URL parametreleri. Format /url?key=val&key1=val1&key2=val2 şeklindedir.

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

Örnek Bir

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

Örnek İki

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

Form Parametreleri

Form içeriği türleri genellikle application/json, application/x-www-form-urlencoded, application/xml, multipart/form-data şeklindedir.

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

Örnek Bir: form-data kullanımı

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 metodu varsayılan olarak application/x-www-form-urlencoded ve multipart/form-data türündeki formları ayrıştırır.

Örnek İki: json kullanımı

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]

Veri Ayrıştırma

Çoğu durumda parametreleri doğrudan ayrıştırmak yerine veri taşımak için struct'lar kullanırız. gin'de veri bağlama için kullanılan metotlar esas olarak Bind() ve ShouldBind()'dir. Aralarındaki fark ilkinin doğrudan ShouldBind() çağrısı yapmasıdır. Tabii err döndürdüğünde doğrudan 400 yanıtı verir. İkincisi ise vermez. Daha esnek hata işleme istiyorsanız ikincisini kullanmanız önerilir. Bu iki fonksiyon isteğin content-type'ına göre hangi yöntemle ayrıştırılacağını otomatik olarak tahmin eder.

go
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // ShouldBindWith() çağrısı yapar
  if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // doğrudan 400 badrequest yanıtı verir
    return err
  }
  return nil
}

Kendiniz seçmek isterseniz BindWith() ve ShouldBindWith() kullanabilirsiniz. Örneğin:

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

gin'in desteklediği bağlama türleri aşağıdaki gibidir:

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

Örnek

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
    // otomatik tahmin için ShouldBind kullan
  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 Veri Bağlama

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

Form Veri Bağlama

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

URL Veri Bağlama

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

Burada hata oluşur çünkü content-type boş bir string'dir ve veri ayrıştırmanın nasıl yapılacağını tahmin edemez. Bu nedenle URL parametrelerini kullanırken manuel olarak ayrıştırma yöntemini belirtmeliyiz. Örneğin:

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

Çoklu Bağlama

Genel yöntemler c.Request.Body metodunu çağırarak veri bağlar ancak bu metodu birden fazla kez çağırılamaz. Örneğin c.ShouldBind yeniden kullanılamaz. Birden fazla bağlama yapmak istiyorsanız c.ShouldBindBodyWith kullanabilirsiniz.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // c.Request.Body'yi oku ve sonucu bağlama koy.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Bu noktada bağlamda saklanan body'yi yeniden kullan.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // Diğer formatları kabul edebilir
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith bağlamadan önce body'yi bağlama saklar. Bu performansta hafif bir etkiye neden olur. Eğer bir kez çağırarak bağlama yapabiliyorsanız bu metodu kullanmayın. Sadece bazı formatlar bu özelliğe ihtiyaç duyar. Örneğin JSON, XML, MsgPack, ProtoBuf. Diğer formatlar için Query, Form, FormPost, FormMultipart herhangi bir performans kaybı olmadan c.ShouldBind() metodunu birden fazla kez çağırabilir.

Veri Doğrulama

gin'in yerleşik doğrulama aracı aslında github.com/go-playground/validator/v10'dır. Kullanım yöntemi de neredeyse aynıdır. Validator

Basit Örnek

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

Test

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

}'

Çıktı

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

TIP

Dikkat edilmesi gereken bir nokta şudur: gin'de validator'ın doğrulama tag'i binding'dir. Ayrı kullanılan validator'ın doğrulama tag'i ise validator'dır.

Veri Yanıtlama

Veri yanıtı arayüz işleminde yapılacak son adımdır. Backend tüm verileri işledikten sonra HTTP protokolü aracılığıyla çağırıcıya döndürür. gin veri yanıtları için zengin yerleşik destek sağlar. Kullanımı anlaşılması ve öğrenilmesi çok kolaydır.

Basit Örnek

go
func Hello(c *gin.Context) {
    // Saf string formatında veri döndürür. http.StatusOK 200 durum kodudur. Veri "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

HTML Render

TIP

Dosya yüklenirken varsayılan kök yol proje yoludur. Yani go.mod dosyasının bulunduğu yoldur. Aşağıdaki örneklerdeki index.html kök yol altındaki index.html'dir. Ancak genellikle bu şablon dosyaları kök yola konmaz. Statik kaynak klasörlerinde saklanır.

go
func main() {
   e := gin.Default()
    // HTML dosyasını yükle. Engine.LoadHTMLGlob() da kullanılabilir
   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{})
}

Test

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

Dönüş

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>

Hızlı Yanıt

Önce sık sık context.String() metodu kullanılarak veri yanıtı verilir. Bu en原始 yanıt yöntemidir. Doğrudan bir string döndürür. gin ayrıca birçok hızlı yanıt metodu sağlar. Örneğin:

go
// Yanıt başlığını yazmak ve veri render için Render kullan
func (c *Context) Render(code int, r render.Render)

// Bir HTML şablonu render et. name html yoludur. obj içeriktir
func (c *Context) HTML(code int, name string, obj any)

// Güzel biçimlendirilmiş girintili JSON string ile veri render. Genellikle bu yöntemin kullanılması önerilmez. Çünkü daha fazla aktarım tüketimine neden olur.
func (c *Context) IndentedJSON(code int, obj any)

// Güvenli JSON. JSON hijacking'i önleyebilir. Detaylar için: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// JSONP yöntemi ile render
func (c *Context) JSONP(code int, obj any)

// JSON yöntemi ile render
func (c *Context) JSON(code int, obj any)

// JSON yöntemi ile render. unicode kodlarını ASCII kodlarına dönüştürür
func (c *Context) AsciiJSON(code int, obj any)

// JSON yöntemi ile render. HTML özel karakterlerini escape etmez
func (c *Context) PureJSON(code int, obj any)

// XML yöntemi ile render
func (c *Context) XML(code int, obj any)

// YML yöntemi ile render
func (c *Context) YAML(code int, obj any)

// TOML yöntemi ile render
func (c *Context) TOML(code int, obj interface{})

// ProtoBuf yöntemi ile render
func (c *Context) ProtoBuf(code int, obj any)

// String yöntemi ile render
func (c *Context) String(code int, format string, values ...any)

// Belirli bir konuma yönlendir
func (c *Context) Redirect(code int, location string)

// data'yı yanıt akışına yaz
func (c *Context) Data(code int, contentType string, data []byte)

// reader'dan okuyarak yanıt akışına yaz
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// Dosyayı verimli bir şekilde yanıt akışına yaz
func (c *Context) File(filepath string)

// fs'deki dosya akışını verimli bir şekilde yanıt akışına yaz
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// fs'deki dosya akışını verimli bir şekilde yanıt akışına yaz. İstemcide belirtilen dosya adı ile indirilir
func (c *Context) FileAttachment(filepath, filename string)

// Sunucu push akışını yanıt akışına yaz
func (c *Context) SSEvent(name string, message any)

// Bir akış yanıtı gönder ve istemcinin akış ortasında bağlantıyı kesip kesmediğini判断 etmek için bir boolean döndür
func (c *Context) Stream(step func(w io.Writer) bool) bool

Çoğu uygulama için en çok kullanılan context.JSON'dır. Diğerleri nispeten daha az kullanılır. Burada örnek göstermeyeceğim. Çünkü hepsi oldukça basit ve anlaşılırdır. Neredeyse doğrudan çağırma işidir.

Asenkron İşleme

gin'de asenkron işleme goroutine ile birlikte kullanılmalıdır. Kullanımı çok basittir.

go
// mevcut Context'in bir kopyasını döndür. Böylece mevcut Context kapsamı dışında güvenli bir şekilde kullanılabilir. Bir goroutine'e geçmek için kullanılabilir
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() {
    // Alt goroutine orijinal Context'i değil Context'in kopyasını kullanmalıdır
    log.Println("Asenkron işleme fonksiyonu: ", ctx.HandlerNames())
  }()
  log.Println("Arayüz işleme fonksiyonu: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Test

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

Çıktı

go
2022/12/21 13:33:47 Asenkron işleme fonksiyonu:  []
2022/12/21 13:33:47 Arayüz işleme fonksiyonu:  [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"

İkisinin çıktısının farklı olduğunu görebilirsiniz. Kopya kopyalanırken güvenlik nedeniyle birçok elemanın değeri silinmiştir.

Dosya Aktarımı

Dosya aktarımı web uygulamalarının vazgeçilmez bir işlevidir. gin bunun için desteği çok basit bir şekilde kapsüller. Ancak aslında özünde orijinal net/http ile işlem süreci neredeyse aynıdır. Süreç istek gövdesinden dosya akışını okuyup ardından yerel depolamaya kaydetmektir.

Tek Dosya Yükleme

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

func uploadFile(ctx *gin.Context) {
  // Dosyayı al
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Yerel depolamaya kaydet
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Sonucu döndür
  ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

Test

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

Sonuç

upload a.jpg size:1424 byte successfully!

TIP

Genellikle dosya yükleme Method'u POST olarak belirtilir. Bazı şirketler PUT kullanmayı tercih edebilir. İlki basit HTTP isteğidir. İkincisi karmaşık HTTP isteğidir. Aradaki fark burada detaylandırılmayacaktır. İkincisini kullanırsanız özellikle ön ve arka uç ayrımı olan projelerde ilgili CORS işleme yapılması gerekir. Gin'in varsayılan yapılandırması CORS'u desteklemez. CORS Yapılandırması.

Çoklu Dosya Yükleme

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

func uploadFiles(ctx *gin.Context) {
  // gin'in ayrıştırdığı multipart form'u al
  form, _ := ctx.MultipartForm()
  // Anahtara göre dosya listesini al
  files := form.File["files"]
  // Dosya listesini yinele ve yerel depolamaya kaydet
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // Sonucu döndür
  ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

Test

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

Çıktı

upload 3 files successfully!

Dosya İndirme

Dosya indirme kısmı Gin orijinal standart kütüphane API'sini tekrar kapsülleyerek dosya indirmeyi son derece basit hale getirir.

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) {
    // Dosya adını al
  filename := ctx.Param("filename")
    // İlgili dosyayı döndür
  ctx.FileAttachment(filename, filename)
}

Test

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

Sonuç

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

Çok basit geldiğini düşünebilirsiniz. Framework yöntemini kullanmadan süreci kendiniz yazın.

go
func download(ctx *gin.Context) {
   // Parametreyi al
   filename := ctx.Param("filename")

   // Yanıt ve istek nesneleri
   response, request := ctx.Writer, ctx.Request
   // Yanıt başlığını yaz
   // response.Header().Set("Content-Type", "application/octet-stream")  Dosyayı ikili akış olarak ilet
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Dosya adını güvenli bir şekilde escape et
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // Aktarım kodlaması
   http.ServeFile(response, request, filename)
}

Aslında net/http da yeterince iyi kapsüllenmiştir.

TIP

Engine.MaxMultipartMemory aracılığıyla dosya aktarımı için maksimum bellek ayarlanabilir. Varsayılan 32 << 20 // 32 MB'dır.

Yönlendirme Yönetimi

Yönlendirme yönetimi bir sistemin çok önemli bir parçasıdır. Her isteğin doğru fonksiyona eşlenmesini sağlamak gerekir.

Yönlendirme Grupları

Bir yönlendirme grubu oluşturmak arayüzleri sınıflandırmaktır. Farklı kategorideki arayüzler farklı işlevlere sahiptir ve yönetilmesi daha kolaydır.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Diyelim ki yukarıdaki dört arayüzümüz var. Şimdilik iç implementasyonlarını dikkate almayalım. Hello ve Login bir grup. Update ve Delete bir grup.

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

Grup oluştururken grubun kök yönlendirmesine de işleyici kaydedebiliriz. Ancak çoğu zaman bunu yapmayız.

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

Onları v1 ve v2 olmak üzere iki gruba ayırdık. Buradaki süslü parantezler {} sadece规范 içindir. Süslü parantezler içinde kaydedilen işleyicilerin aynı yönlendirme grubuna ait olduğunu gösterir. İşlevsel olarak hiçbir etkisi yoktur. Aynı şekilde gin iç içe grupları da destekler. Yöntem yukarıdaki örnekle aynıdır. Burada tekrar gösterilmeyecektir.

404 Yönlendirmesi

gin'deki Engine struct'ı erişilen URL mevcut olmadığında nasıl işleneceğini ayarlamak için bir NoRoute metodu sağlar. Geliştiriciler bu metoda mantığı yazabilir. Böylece yönlendirme bulunamadığında otomatik olarak çağrılır. Varsayılan olarak 404 durum kodu döndürür.

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

Bir önceki örneği alalım:

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)
   }
   // İşleyici kaydet
   e.NoRoute(func(context *gin.Context) { // Bu sadece gösterimdir. Üretim ortamında doğrudan HTML kodu döndürmeyin
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Rastgele bir istek gönderin:

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

405 Yönlendirmesi

Http durum kodunda 405 mevcut istek yönteminin izin verilmediğini gösterir. gin aşağıdaki metodu sağlar:

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

Bir işleyici kaydetmek için kullanılır. Böylece meydana geldiğinde otomatik olarak çağrılır. Önkoşul Engine.HandleMethodNotAllowed = true ayarlanmasıdır.

go
func main() {
   e := gin.Default()
   // true olarak ayarlanmalı
   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>")
   })
   // İşleyici kaydet
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "method not allowed")
   })
   log.Fatalln(e.Run(":8080"))
}

Yapılandırıldıktan sonra gin'in varsayılan header'ı OPTION isteğini desteklemez. Test edelim:

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

Bu şekilde yapılandırma başarılı olur.

Yönlendirme

gin'de yönlendirme çok basittir. gin.Context.Redirect() metodunu çağırmak yeterlidir.

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

Test

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

Çıktı

hello

Middleware

gin oldukça hafif ve esnektir. Genişletilebilirliği çok yüksektir. Middleware desteği de çok iyidir. Gin'de tüm arayüz istekleri middleware'den geçer. Geliştiriciler middleware aracılığıyla birçok işlev ve mantığı kendi başlarına uygulayabilirler. gin'in kendi işlevleri az olmasına rağmen üçüncü taraf topluluk tarafından geliştirilen gin genişletme middleware'leri çok zengindir.

Middleware özünde bir arayüz işleyicidir:

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

Bir anlamda her istek için karşılık gelen işleyici de middleware'dir. Ancak etki alanı çok küçük olan yerel bir middleware'dir.

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

gin'in kaynak koduna bakıldığında Default fonksiyonunda döndürülen varsayılan Engine iki varsayılan middleware kullanır: Logger() ve Recovery(). Varsayılan middleware'leri kullanmak istemiyorsanız gin.New() kullanabilirsiniz.

Global Middleware

Global middleware etki alanı globaldir. Sistemin tüm istekleri bu middleware'den geçer.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("Global middleware çağrıldı...")
   }
}

Middleware oluşturmak için bir closure fonksiyonu oluşturun. Ardından Engine.Use() ile global middleware'i kaydedin.

go
func main() {
   e := gin.Default()
   // Global middleware kaydet
   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"))
}

Test

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

Çıktı

[GIN-debug] Listening and serving HTTP on :8080
Global middleware çağrıldı...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

Yerel Middleware

Yerel middleware etki alanı yereldir. Sistemin yerel istekleri bu middleware'den geçer. Yerel middleware tek bir yönlendirmeye kaydedilebilir. Ancak çoğu zaman yönlendirme grubuna kaydedilir.

go
func main() {
   e := gin.Default()
   // Global middleware kaydet
   e.Use(GlobalMiddleware())
   // Yönlendirme grubu yerel middleware kaydet
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // Tek yönlendirme yerel middleware kaydet
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Test

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

Çıktı

Global middleware çağrıldı...
Yerel middleware çağrıldı
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

Middleware Prensibi

Gin'de middleware kullanımı ve özelleştirmesi çok kolaydır. İç prensibi de oldukça basittir. Sonraki öğrenme için iç prensibi basitçe anlamak gerekir. Gin'deki middleware aslında Responsibility Chain pattern'i kullanır. Context bir HandlersChain tutar. Özünde bir []HandlerFunc'tür. Ve bir index tutar. Veri türü int8'dir. Engine.handlerHTTPRequest(c *Context) metodunda çağrı sürecini gösteren bir kod vardır: gin yönlendirme ağacında karşılık gelen yönlendirmeyi bulduktan sonra Next() metodunu çağırır.

go
if value.handlers != nil {
   // Çağrı zincirini Context'e ata
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // Middleware'i çağır
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Next() çağrısı anahtardır. Next() yönlendirmenin handlersındaki HandlerFunc'ları yineleyip yürütür. Bu noktada index'in middleware çağrı konumunu kaydettiği görülebilir. Karşılık gelen yönlendirmeye kaydedilen arayüz fonksiyonu da handlers içindedir. Bu da neden arayüzün de bir middleware olduğunu söylediğimizdir.

go
func (c *Context) Next() {
   // İçeri girer girmez +1 recursive deadlock'tan kaçınmak içindir. Varsayılan değer -1'dir
   c.index++
   for c.index < int8(len(c.handlers)) {
      // HandlerFunc'u yürüt
      c.handlers[c.index](c)
      // Yürütme tamamlandı. index+1
      c.index++
   }
}

Hello() mantığını değiştirerek gerçekten böyle olup olmadığını doğrulayalım:

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

Çıktı sonucu:

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

Middleware çağrı zinciri sırası şöyledir: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello. Çağrı zincirinin son elemanı gerçekten yürütülecek arayüz fonksiyonudur. Öncekilerin hepsi middleware'dir.

TIP

Yerel yönlendirme kaydedilirken şu assertion vardır:

go
finalSize := len(group.Handlers) + len(handlers) // middleware toplam sayısı
assert1(finalSize < int(abortIndex), "too many handlers")

Burada abortIndex int8 = math.MaxInt8 >> 1 değeri 63'tür. Yani sistem kullanılırken yönlendirme kayıt sayısı 63'ü geçmemelidir.

Zamanlayıcı Middleware

Yukarıdaki middleware prensibini öğrendikten sonra basit bir istek zaman istatistik middleware'i yazabiliriz.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // Başlangıç zamanını kaydet
      start := time.Now()
      // Sonraki çağrı zincirini yürüt
      context.Next()
      // Zaman aralığını hesapla
      duration := time.Since(start)
      // Sonucu gözlemlemek için nanosaniye cinsinden çıktı
      fmt.Println("İstek süresi: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // Global middleware kaydet. Zamanlayıcı middleware
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // Yönlendirme grubu yerel middleware kaydet
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // Tek yönlendirme yerel middleware kaydet
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

Test

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

Çıktı

İstek süresi:  517600

Basit bir zamanlayıcı middleware başarıyla yazıldı. Sonrasında kendi keşiflerinizle daha kullanışlı middleware'ler yazabilirsiniz.

Servis Yapılandırması

Sadece varsayılan yapılandırmayı kullanmak yeterli değildir. Çoğu durumda gereksinimleri karşılamak için birçok servis yapılandırmasını değiştirmek gerekir.

Http Yapılandırması

net/http ile Server oluşturarak yapılandırabilirsiniz. Gin aynı zamanda orijinal API gibi kullanılmasını da destekler.

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

Statik Kaynak Yapılandırması

Statik kaynaklar geçmişte temelde sunucunun vazgeçilmez bir parçasıydı. Günümüzde kullanım oranı yavaş yavaş azalıyor olmasına rağmen hala çok sayıda sistem monolitik mimari kullanıyor.

Gin statik kaynakları yüklemek için üç metod sağlar:

go
// Belirli bir statik klasörü yükle
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// Belirli bir fs yükle
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// Belirli bir statik dosya yükle
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath web URL'sine eşlenen göreli yoldur. root dosyanın projedeki gerçek yoludur.

Projenin dizini şu şekilde olsun:

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // Statik dosya dizinini yükle
   router.Static("/static", "./static")
   // Statik dosya dizinini yükle
   router.StaticFS("/view", http.Dir("view"))
   // Statik dosyayı yükle
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

CORS Yapılandırması

Gin'in CORS yapılandırması için herhangi bir işlemi yoktur. İlgili gereksinimleri uygulamak için middleware'i kendiniz yazmanız gerekir. Aslında zorluğu da fazla değildir. HTTP protokolünü biraz bilenler genellikle yazabilir. Mantık temelde aynıdır.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // Üretim ortamındaki sunucular genellikle * işaretlemez. Belirli alan adı doldurulmalıdır
         c.Header("Access-Control-Allow-Origin", origin)
         // İzin verilen HTTP METHOD'lar
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // İzin verilen istek başlıkları
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // İstemcinin erişebileceği yanıt başlıkları
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Kimlik doğrulama bilgisi taşıması gerekiyor mu. Credentials cookies, authorization headers veya TLS client certificates olabilir
         // true olarak ayarlandığında Access-Control-Allow-Origin * olamaz
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // OPTION isteğine izin ver. Ancak sonraki metodları yürütme
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // İzin ver
      c.Next()
   }
}

Middleware'i global middleware olarak kaydetmek yeterlidir.

Oturum Kontrolü

Günümüzde popüler üç web oturum kontrolü vardır: cookie, session, JWT.

Cookie'deki bilgiler anahtar-değer çifti şeklinde tarayıcıda saklanır. Ve tarayıcıda veriler doğrudan görülebilir.

Avantajları:

  • Basit yapı
  • Veri kalıcı

Dezavantajları:

  • Boyut sınırlı
  • Düz metin depolama
  • CSRF saldırılarına karşı hassas
go
import (
    "fmt"

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

func main() {

    router := gin.Default()

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

         // İlgili cookie'yi al
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // Cookie ayarla. Parametreler: key, val, varlık süresi, dizin, alan adı, başkalarının cookie'ye js ile erişmesine izin verilip verilmeyeceği, sadece http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

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

    router.Run()
}

Saf cookie beş altı yıl önce daha çok kullanılıyordu. Ancak yazar genellikle oturum kontrolü için saf cookie kullanmaz. Bu gerçekten çok güvenli değil.

Session

session sunucuda saklanır. Ardından tarayıcıda bir cookie saklanır. cookie'de session_id saklanır. Sonraki her istekte sunucu session_id aracılığıyla ilgili session bilgisini alabilir.

Avantajları:

  • Sunucu tarafında saklanır. Güvenliği artırır. Yönetimi kolaylaştırır.

Dezavantajları:

  • Sunucu tarafında saklanır. Sunucu yükünü artırır. Performansı düşürür.
  • cookie tabanlı tanıma. Güvenli değil.
  • Dağıtık durumda kimlik doğrulama bilgileri senkronize değil.

Session ve Cookie ayrılmazdır. Session kullanıldığında varsayılan olarak Cookie kullanılır. Gin varsayılan olarak Session'ı desteklemez. Çünkü Cookie Http protokolündeki içeriktir. Ancak Session değildir. Ancak üçüncü taraf middleware desteği vardır. Bağımlılığı yüklemek yeterlidir. Depo adresi: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

cookie, Redis, MongoDB, GORM, PostgreSQL destekler.

go
func main() {
   r := gin.Default()
   // Cookie tabanlı depolama motoru oluştur
   store := cookie.NewStore([]byte("secret"))
   // Session middleware ayarla. mysession session adıdır. cookie adıdır.
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // session'ı başlat
      session := sessions.Default(c)
      var count int
      // Değeri al
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // Ayarla
      session.Set("count", count)
      // Kaydet
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Genellikle Cookie ile Session depolamak önerilmez. Redis kullanılması önerilir. Diğer örnekler için lütfen resmi depoyu inceleyin.

JWT

Avantajları:

  • JSON tabanlı. Çoklu dil genelinde kullanılabilir.
  • Hassas olmayan bilgileri depolayabilir.
  • Çok küçük. Aktarımı kolay.
  • Sunucu tarafında depolama gerekmez. Dağıtık genişletmeyi kolaylaştırır.

Dezavantajları:

  • Token yenileme sorunu.
  • Bir kez imzalandığında aktif olarak kontrol edilemez.

Ön uç devriminden bu yana ön uç programcıları artık sadece "sayfa yazan" değil. Ön ve arka uç ayrımı trendi giderek artıyor. JWT ön ve arka uç ayrımı ve dağıtık sistemler için oturum kontrolü yapmaya en uygun. Çok büyük doğal avantajlara sahip. JWT'nin tamamen Gin içeriğinden ayrıldığı ve herhangi bir middleware desteği olmadığı göz önüne alındığında. Çünkü JWT zaten herhangi bir framework veya dille sınırlı değil. Burada detaylı açıklama yapılmayacak. Başka bir dokümana gidebilirsiniz: JWT

Log Yönetimi

Gin varsayılan olarak log middleware için os.Stdout kullanır. Sadece en temel işlevlere sahiptir. Çünkü Gin sadece web servisine odaklanır. Çoğu durumda daha olgun log framework'leri kullanılmalıdır. Ancak bu bu bölümün tartışma alanı dışındadır. Gin'in genişletilebilirliği çok yüksektir. Diğer framework'leri kolayca entegre edebilir. Burada sadece kendi log servisini tartışacağız.

Konsol Rengi

go
gin.DisableConsoleColor() // Konsol log rengini kapat

Geliştirme zamanı dışında çoğu zaman bu öğenin açılması önerilmez.

Log Dosyaya Yazma

go
func main() {
  e := gin.Default()
    // Konsol rengini kapat
  gin.DisableConsoleColor()
    // İki log dosyası oluştur
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // Aynı anda iki log dosyasına kaydet
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

gin'in yerleşik log desteği birden fazla dosyaya yazmayı destekler. Ancak içerik aynıdır. Kullanımı çok uygun değildir. Ve istek loglarını dosyaya yazmaz.

go
func main() {
  router := gin.New()
  // LoggerWithFormatter middleware logları gin.DefaultWriter'a yazar
  // Varsayılan gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO ilgili dosyaya yazma mantığı
        ......
    // Özel format çıktısı
    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")
}

Özel middleware aracılığıyla loglar dosyaya yazılabilir.

Yönlendirme Debug Log Formatı

Burada değiştirilen sadece başlangıçta yönlendirme bilgilerinin log çıktısıdır.

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

Çıktı

2022/12/21 17:19:13 Yönlendirme GET /hello main.Hello 3

Sonuç: Gin Go dilindeki web framework'leri arasında öğrenilmesi en kolay olanıdır. Çünkü Gin gerçekten sorumluluğu en aza indirmiştir. Sadece web servisten sorumludur. Diğer kimlik doğrulama mantığı veri önbellekleme vb. işlevler geliştiricilere bırakılır. Büyük ve tam framework'lere kıyasla hafif ve temiz Gin yeni başlayanlar için daha uygundur ve öğrenilmesi daha gerekir. Çünkü Gin belirli bir规范 kullanmayı zorunlu kılmaz. Projenin nasıl oluşturulacağı hangi yapının benimseneceği kendi takdirinize bırakılır. Yeni başlayanlar için yeteneklerini geliştirmeye daha uygundur.

Golang by www.golangdev.cn edit