Gin
Dokumentasi Resmi: Gin Web Framework (gin-gonic.com)
Alamat Repositori: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)
Contoh Resmi: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
Pengantar
Gin adalah framework web yang ditulis dalam Go (Golang). Ia memiliki API mirip martini dengan performa jauh lebih baik berkat httprouter kecepatan meningkat 40 kali lipat. Jika Anda membutuhkan performa dan produktivitas yang baik Anda pasti akan menyukai Gin. Gin dibandingkan Iris dan Beego lebih cenderung ke framework ringan hanya bertanggung jawab untuk bagian web mengejar performa routing maksimal fungsi mungkin tidak selengkap lainnya namun unggul dalam ringan dan mudah diperluas ini juga merupakan kelebihannya. Oleh karena itu di antara semua framework web Gin adalah yang paling mudah dipelajari dan dikuasai.
Fitur
- Cepat: Routing berbasis pohon Radix penggunaan memori kecil. Tanpa refleksi. Performa API yang dapat diprediksi.
- Dukungan Middleware: Request HTTP yang masuk dapat ditangani oleh serangkaian middleware dan operasi final. Misalnya: Logger, Authorization, GZIP, operasi final DB.
- Crash Handling: Gin dapat catch panic yang terjadi dalam request HTTP dan recover-nya. Dengan demikian server Anda akan selalu tersedia.
- JSON Validation: Gin dapat mengurai dan memvalidasi JSON request misalnya memeriksa keberadaan nilai yang diperlukan.
- Route Groups: Mengorganisir route dengan lebih baik. Apakah perlu authorization versi API yang berbeda... Selain itu grup ini dapat di-nested tanpa batas tanpa menurunkan performa.
- Error Management: Gin menyediakan cara yang mudah untuk mengumpulkan semua error yang terjadi selama request HTTP. Pada akhirnya middleware dapat menulisnya ke file log database dan mengirim melalui network.
- Built-in Rendering: Gin menyediakan API yang mudah digunakan untuk rendering JSON XML dan HTML.
- Extensible: Membuat middleware baru sangat sederhana
Instalasi
Hingga saat ini 2022/11/22 versi minimum Go yang didukung gin adalah 1.16 disarankan menggunakan go mod untuk mengelola dependensi proyek.
go get -u github.com/gin-gonic/ginImport
import "github.com/gin-gonic/gin"Quick Start
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default() //membuat engine gin
engine.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
engine.Run() //membuka server default mendengarkan localhost:8080
}Request URL
GET localhost:8080/pingResponse
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"
}Dokumentasi
Sebenarnya dokumentasi resmi Gin tidak banyak tutorial kebanyakan hanya pengenalan dan penggunaan dasar serta beberapa contoh tetapi di bawah organisasi gin-gonic/ ada repositori gin-gonic/examples yang merupakan repositori contoh gin yang dikelola bersama oleh komunitas. Semua dalam bahasa Inggris waktu update tidak terlalu sering penulis juga belajar framework gin dari sini perlahan-lahan.
Alamat Repositori Contoh: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
TIP
Sebelum memulai disarankan membaca HttpRouter: HttpRouter
Parsing Parameter
Parsing parameter di gin mendukung tiga cara: Parameter Route Parameter URL Parameter Form di bawah akan dijelaskan satu per satu dengan contoh kode yang cukup sederhana.
Parameter Route
Parameter route sebenarnya mengenkapsulasi fungsi parsing parameter HttpRouter cara penggunaan pada dasarnya sama dengan 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"))
}
// Contoh parameter named
func FindUser(c *gin.Context) {
username := c.Param("username")
userid := c.Param("userid")
c.String(http.StatusOK, "username adalah %s\n userid adalah %s", username, userid)
}
// Contoh parameter path
func UserPage(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "filepath adalah %s", filepath)
}Contoh 1
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username adalah jack
userid adalah 001Contoh 2
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath adalah /img/fruit.pngParameter URL
Parameter URL tradisional formatnya adalah /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 adalah %s\nuserid adalah %s", username, userid)
}Contoh 1
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username adalah jack
userid adalah 001Contoh 2
curl --location --request GET '127.0.0.1:8080/findUser'username adalah defaultUser
userid adalahParameter Form
Tipe konten form umumnya ada 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, "berhasil terdaftar username Anda adalah [%s] password adalah [%s]", username, password)
}
func UpdateUser(c *gin.Context) {
var form map[string]string
c.ShouldBind(&form)
c.String(http.StatusOK, "berhasil update username Anda adalah [%s] password adalah [%s]", form["username"], form["password"])
}Contoh 1: Menggunakan form-data
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'berhasil terdaftar username Anda adalah [jack] password adalah [123456]Method PostForm secara default memparse form tipe application/x-www-form-urlencoded dan multipart/form-data.
Contoh 2: Menggunakan json
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"username",
"password":"123456"
}'berhasil update username Anda adalah [username] password adalah [123456]Parsing Data
Dalam kebanyakan kasus kita akan menggunakan struct untuk menampung data bukan memparse parameter secara langsung. Di gin method yang digunakan untuk binding data terutama Bind() dan ShouldBind() perbedaan keduanya adalah yang pertama internalnya langsung memanggil ShouldBind() tentu saat mengembalikan err akan langsung response 400 yang kedua tidak. Jika ingin lebih fleksibel dalam penanganan error disarankan memilih yang kedua. Kedua fungsi ini akan otomatis inferensi berdasarkan content-type request untuk menentukan cara parse.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// memanggil ShouldBindWith()
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // langsung response 400 badrequest
return err
}
return nil
}Jika ingin memilih sendiri dapat menggunakan BindWith() dan ShouldBindWith() misalnya
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlTipe binding yang didukung gin adalah sebagai berikut:
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{}
)Contoh
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
// menggunakan ShouldBind agar gin inferensi otomatis
if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login berhasil !")
} else {
c.String(http.StatusBadRequest, "login gagal !")
}
fmt.Println(login)
}Binding Data Json
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"root",
"password":"root"
}'login berhasil !Binding Data Form
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login berhasil !Binding Data URL
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'login gagal !Di sini akan terjadi error karena content-type yang output adalah string kosong tidak dapat inferensi bagaimana cara parse data. Jadi saat menggunakan parameter URL kita harus manual menentukan cara parse misalnya:
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login berhasil !")
} else {
fmt.Println(err)
c.String(http.StatusBadRequest, "login gagal !")
}Multiple Binding
Method umum adalah dengan memanggil c.Request.Body untuk binding data tetapi tidak dapat memanggil method ini beberapa kali misalnya c.ShouldBind tidak dapat digunakan kembali jika ingin binding beberapa kali dapat menggunakan c.ShouldBindBodyWith.
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// membaca c.Request.Body dan menyimpan hasil ke context
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `body seharusnya formA`)
// saat ini reuse body yang disimpan di context
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `body seharusnya formB JSON`)
// dapat menerima format lain
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `body seharusnya formB XML`)
}
}TIP
c.ShouldBindBodyWith akan menyimpan body ke context sebelum binding. Ini akan berdampak ringan pada performa jika dapat diselesaikan dengan satu panggilan maka jangan gunakan method ini. Hanya beberapa format yang membutuhkan fitur ini seperti JSON XML MsgPack ProtoBuf. Untuk format lain seperti Query Form FormPost FormMultipart dapat memanggil c.ShouldBind() beberapa kali tanpa dampak performa apa pun.
Validasi Data
Tool validasi built-in gin sebenarnya adalah github.com/go-playground/validator/v10 cara penggunaan juga hampir tidak ada perbedaan Validator
Contoh Sederhana
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, "user tidak valid %v", err)
}
}Test
curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"jack1"
}'Output
user tidak valid Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tagTIP
Perlu diperhatikan bahwa tag validasi validator di gin adalah binding sedangkan saat menggunakan validator secara terpisah tag validasinya adalah validator
Response Data
Response data adalah langkah terakhir yang harus dilakukan dalam penanganan interface backend mengembalikan semua data yang telah diproses ke caller melalui protokol HTTP gin menyediakan dukungan built-in yang kaya untuk response data cara penggunaan sederhana dan mudah dipahami.
Contoh Sederhana
func Hello(c *gin.Context) {
// mengembalikan data format string murni http.StatusOK mewakili kode status 200 data adalah "Hello world !"
c.String(http.StatusOK, "Hello world !")
}Rendering HTML
TIP
Saat memuat file path root default adalah path proyek yaitu path tempat file go.mod berada index.html dalam contoh di bawah terletak di index.html di bawah path root namun umumnya file template ini tidak diletakkan di path root tetapi disimpan di folder static resource
func main() {
e := gin.Default()
// memuat file HTML dapat juga menggunakan 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{})
}Test
curl --location --request GET 'http://localhost:8080/'Response
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GinLearn</title>
</head>
<body>
<h1>Hello World!</h1>
<h1>Ini adalah Contoh Render Template HTML</h1>
</body>
</html>Response Cepat
Sering menggunakan method context.String() untuk response data ini adalah method response paling primitif langsung mengembalikan string gin juga内置 banyak method response cepat例如:
// Menulis response header menggunakan Render dan melakukan rendering data
func (c *Context) Render(code int, r render.Render)
// Merender template HTML name adalah path html obj adalah konten
func (c *Context) HTML(code int, name string, obj any)
// Merender data dengan string JSON yang diindentsasi indah umumnya tidak disarankan menggunakan method ini karena akan menyebabkan lebih banyak overhead transmisi.
func (c *Context) IndentedJSON(code int, obj any)
// JSON aman dapat mencegah JSON hijacking detail: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)
// Merender dengan cara JSONP
func (c *Context) JSONP(code int, obj any)
// Merender dengan cara JSON
func (c *Context) JSON(code int, obj any)
// Merender dengan cara JSON akan mengkonversi kode unicode ke ASCII
func (c *Context) AsciiJSON(code int, obj any)
// Merender dengan cara JSON tidak akan mengescape karakter khusus HTML
func (c *Context) PureJSON(code int, obj any)
// Merender dengan cara XML
func (c *Context) XML(code int, obj any)
// Merender dengan cara YML
func (c *Context) YAML(code int, obj any)
// Merender dengan cara TOML
func (c *Context) TOML(code int, obj interface{})
// Merender dengan cara ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)
// Merender dengan cara String
func (c *Context) String(code int, format string, values ...any)
// Redirect ke lokasi tertentu
func (c *Context) Redirect(code int, location string)
// Menulis data ke response stream
func (c *Context) Data(code int, contentType string, data []byte)
// Membaca stream melalui reader dan menulis ke response stream
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
// Menulis file ke response stream dengan efisien
func (c *Context) File(filepath string)
// Menulis stream file dari fs ke response stream dengan cara yang efisien
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
// Menulis stream file dari fs ke response stream dengan cara yang efisien dan di client akan didownload dengan nama file yang ditentukan
func (c *Context) FileAttachment(filepath, filename string)
// Menulis server push stream ke response stream
func (c *Context) SSEvent(name string, message any)
// Mengirim response stream dan mengembalikan boolean untuk menentukan apakah client disconnect di tengah stream
func (c *Context) Stream(step func(w io.Writer) bool) boolUntuk sebagian besar aplikasi yang paling sering digunakan adalah context.JSON yang lain relatif lebih sedikit di sini tidak akan memberikan contoh演示 karena cukup sederhana hampir semuanya langsung调用.
Pemrosesan Asinkron
Di gin pemrosesan asinkron perlu dikombinasikan dengan goroutine penggunaannya sangat sederhana.
// Copy mengembalikan salinan Context saat ini untuk digunakan dengan aman di luar scope Context saat ini dapat digunakan untuk diteruskan ke 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() {
// Sub goroutine seharusnya menggunakan salinan Context tidak boleh menggunakan Context asli
log.Println("Fungsi pemrosesan asinkron: ", ctx.HandlerNames())
}()
log.Println("Fungsi pemrosesan interface: ", c.HandlerNames())
c.String(http.StatusOK, "hello")
}Test
curl --location --request GET 'http://localhost:8080/hello'Output
2022/12/21 13:33:47 Fungsi pemrosesan asinkron: []
2022/12/21 13:33:47 Fungsi pemrosesan interface: [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"Dapat dilihat output keduanya berbeda salinan saat replikasi untuk alasan keamanan menghapus banyak nilai elemen.
Transfer File
Transfer file adalah fungsi yang tidak terpisahkan dari aplikasi web dukungan gin untuk ini juga dikemas sangat sederhana tetapi pada dasarnya sama dengan proses menggunakan net/http native. Prosesnya adalah membaca file stream dari request body kemudian menyimpan ke lokal.
Upload Single File
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}
func uploadFile(ctx *gin.Context) {
// mendapatkan file
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// menyimpan ke lokal
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// mengembalikan hasil
ctx.String(http.StatusOK, "upload %s size:%d byte berhasil!", file.Filename, file.Size)
}Test
curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'Hasil
upload a.jpg size:1424 byte berhasil!TIP
Umumnya method upload file akan指定 menggunakan POST beberapa perusahaan mungkin cenderung menggunakan PUT yang pertama adalah request HTTP sederhana yang kedua adalah request HTTP kompleks perbedaan spesifik tidak akan dijelaskan di sini jika menggunakan yang kedua terutama dalam proyek front-end back-end terpisah perlu melakukan penanganan CORS yang sesuai sedangkan konfigurasi default Gin tidak mendukung CORS Konfigurasi CORS.
Upload Multi File
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}
func uploadFiles(ctx *gin.Context) {
// mendapatkan form multipart yang sudah diparse gin
form, _ := ctx.MultipartForm()
// mendapatkan daftar file sesuai key
files := form.File["files"]
//遍历 daftar file menyimpan ke lokal
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload gagal")
return
}
}
// mengembalikan hasil
ctx.String(http.StatusOK, "upload %d file berhasil!", 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"'Output
upload 3 file berhasil!Download File
Untuk bagian download file Gin sekali lagi mengenkapsulasi API standard library原有 membuat download file sangat sederhana.
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) {
// mendapatkan nama file
filename := ctx.Param("filename")
// mengembalikan file yang sesuai
ctx.FileAttachment(filename, filename)
}Test
curl --location --request GET 'http://localhost:8080/download/a.jpg'Hasil
Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMTApakah terasa terlalu sederhana不妨 tanpa menggunakan method framework tulis sendiri prosesnya
func download(ctx *gin.Context) {
// mendapatkan parameter
filename := ctx.Param("filename")
// objek response dan request
response, request := ctx.Writer, ctx.Request
// menulis response header
// response.Header().Set("Content-Type", "application/octet-stream") mentransmisikan file dengan binary stream
response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // melakukan safe escape pada nama file
response.Header().Set("Content-Transfer-Encoding", "binary") // encoding transmisi
http.ServeFile(response, request, filename)
}Sebenarnya net/http juga sudah dikemas dengan cukup baik
TIP
Dapat mengatur memori maksimum transfer file melalui Engine.MaxMultipartMemory default adalah 32 << 20 // 32 MB
Manajemen Route
Manajemen route adalah bagian yang sangat penting dalam sistem perlu memastikan setiap request dapat dipetakan dengan benar ke fungsi yang sesuai.
Group Route
Membuat group route adalah mengklasifikasikan interface interface dengan kategori berbeda sesuai dengan fungsi yang berbeda juga lebih mudah dikelola.
func Hello(c *gin.Context) {
}
func Login(c *gin.Context) {
}
func Update(c *gin.Context) {
}
func Delete(c *gin.Context) {
}Misalkan kita memiliki empat interface di atas sementara tidak peduli implementasi internalnya Hello Login adalah satu grup Update Delete adalah satu grup.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupSaat membuat grup kita juga dapat mendaftarkan handler untuk root route grup namun kebanyakan时候 tidak akan melakukan ini.
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)
}
}Kita membaginya menjadi dua grup v1 v2 kurung kurawal {} di sini hanya untuk规范 menunjukkan bahwa handler yang terdaftar dalam kurung kurawal milik group route yang sama tidak ada fungsi fungsional. Demikian pula gin juga mendukung nested group method sama dengan contoh di atas di sini tidak akan演示 lagi.
Route 404
Struktur Engine di gin menyediakan method NoRoute untuk mengatur bagaimana menangani saat URL yang diakses tidak ada developer dapat menulis logika ke method ini agar otomatis dipanggil saat route tidak ditemukan default akan mengembalikan kode status 404
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Kita ambil contoh sebelumnya
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)
}
// mendaftarkan handler
e.NoRoute(func(context *gin.Context) { // ini hanya演示 jangan langsung mengembalikan HTML code di production environment
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
log.Fatalln(e.Run(":8080"))
}Kirim request sembarang
curl --location --request GET 'http://localhost:8080/'<h1>404 Page Not Found</h1>Route 405
Dalam kode status Http 405 mewakili bahwa method request saat ini tidak diizinkan gin menyediakan method berikut
func (engine *Engine) NoMethod(handlers ...HandlerFunc)Untuk mendaftarkan handler agar otomatis dipanggil saat terjadi prasyarat adalah mengatur Engine.HandleMethodNotAllowed = true.
func main() {
e := gin.Default()
// perlu mengatur ini menjadi 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>")
})
// mendaftarkan handler
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method tidak diizinkan")
})
log.Fatalln(e.Run(":8080"))
}Setelah konfigurasi header default gin tidak mendukung request OPTION test一下
curl --location --request OPTIONS 'http://localhost:8080/v2/delete'method tidak diizinkanHingga sini konfigurasi berhasil
Redirect
Redirect di gin sangat sederhana cukup panggil method 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")
}Test
curl --location --request GET 'http://localhost:8080/'Output
helloMiddleware
Gin sangat ringan dan fleksibel extensibility sangat tinggi dukungan untuk middleware juga sangat ramah. Di Gin semua request interface harus melalui middleware melalui middleware developer dapat mengimplementasikan banyak fungsi dan logika kustom gin meskipun fungsi built-in sendiri sangat sedikit tetapi middleware extensi yang dikembangkan oleh komunitas pihak ketiga sangat kaya.
Middleware pada dasarnya masih merupakan handler interface
// HandlerFunc mendefinisikan handler yang digunakan oleh middleware gin sebagai nilai return.
type HandlerFunc func(*Context)Dalam arti tertentu handler yang sesuai dengan setiap request juga merupakan middleware tetapi scope actionnya sangat kecil middleware lokal.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}Melihat source code gin fungsi Default Engine default yang dikembalikan menggunakan dua middleware default Logger() Recovery() jika tidak ingin menggunakan middleware default dapat juga menggunakan gin.New() sebagai gantinya.
Middleware Global
Middleware global yaitu scope actionnya global semua request dalam seluruh sistem akan melalui middleware ini.
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("Middleware global dieksekusi...")
}
}Pertama membuat closure function untuk membuat middleware kemudian mendaftarkan middleware global melalui Engine.Use().
func main() {
e := gin.Default()
// mendaftarkan 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"))
}Test
curl --location --request GET 'http://localhost:8080/v1/hello'Output
[GIN-debug] Listening and serving HTTP on :8080
Middleware global dieksekusi...
[GIN] 2022/12/21 - 11:57:52 | 200 | 538.9µs | ::1 | GET "/v1/hello"Middleware Lokal
Middleware lokal yaitu scope actionnya lokal request lokal dalam sistem akan melalui middleware ini. Middleware lokal dapat didaftarkan ke route tunggal namun lebih sering didaftarkan ke group route.
func main() {
e := gin.Default()
// mendaftarkan middleware global
e.Use(GlobalMiddleware())
// mendaftarkan middleware lokal group route
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// mendaftarkan middleware lokal route tunggal
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request POST 'http://localhost:8080/v2/update'Output
Middleware global dieksekusi...
Middleware lokal dieksekusi
[GIN] 2022/12/21 - 12:05:03 | 200 | 999.9µs | ::1 | POST "/v2/update"Prinsip Middleware
Penggunaan dan kustomisasi middleware Gin sangat mudah prinsip internalnya juga cukup sederhana untuk pembelajaran selanjutnya perlu memahami prinsip internalnya dengan sederhana. Middleware di Gin sebenarnya menggunakan pola Chain of Responsibility Context memelihara HandlersChain pada dasarnya adalah []HandlerFunc dan index tipe datanya adalah int8. Di method Engine.handlerHTTPRequest(c *Context) ada kode yang menunjukkan proses调用: gin setelah menemukan route yang sesuai di route tree kemudian memanggil method Next().
if value.handlers != nil {
// menugaskan call chain ke Context
c.handlers = value.handlers
c.fullPath = value.fullPath
// memanggil middleware
c.Next()
c.writermem.WriteHeaderNow()
return
}Panggilan Next() adalah kunci Next() akan遍历 HandlerFunc di handlers route dan mengeksekusi saat ini dapat dilihat fungsi index adalah merekam posisi调用 middleware. Di antaranya fungsi interface yang terdaftar untuk route yang sesuai juga ada di handlers inilah mengapa sebelumnya dikatakan interface juga merupakan middleware.
func (c *Context) Next() {
// begitu masuk langsung +1 adalah untuk menghindari recursive infinite loop default value adalah -1
c.index++
for c.index < int8(len(c.handlers)) {
// mengeksekusi HandlerFunc
c.handlers[c.index](c)
// eksekusi selesai index+1
c.index++
}
}Mengubah logika Hello() untuk memverifikasi apakah benar-benar demikian
func Hello(c *gin.Context) {
fmt.Println(c.HandlerNames())
}Hasil output adalah
[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]Dapat dilihat urutan call chain middleware adalah: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello elemen terakhir call chain adalah fungsi interface yang sebenarnya akan dieksekusi sebelumnya semua adalah middleware.
TIP
Saat mendaftarkan route lokal ada asersi berikut
finalSize := len(group.Handlers) + len(handlers) //total middleware
assert1(finalSize < int(abortIndex), "terlalu banyak handlers")Di mana abortIndex int8 = math.MaxInt8 >> 1 nilai 63 yaitu saat menggunakan sistem jumlah registrasi route jangan melebihi 63.
Middleware Timer
Setelah mengetahui prinsip middleware di atas dapat编写 middleware statistik waktu request sederhana.
func TimeMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
// merekam waktu mulai
start := time.Now()
// mengeksekusi call chain selanjutnya
context.Next()
// menghitung durasi
duration := time.Since(start)
// output nanodetik untuk observasi hasil
fmt.Println("Waktu request: ", duration.Nanoseconds())
}
}
func main() {
e := gin.Default()
// mendaftarkan middleware global middleware timer
e.Use(GlobalMiddleware(), TimeMiddleware())
// mendaftarkan middleware lokal group route
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// mendaftarkan middleware lokal route tunggal
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request GET 'http://localhost:8080/v1/hello'Output
Waktu request: 517600Middleware timer sederhana sudah selesai编写 selanjutnya dapat凭 eksplorasi sendiri编写 beberapa middleware yang lebih fungsional dan praktis.
Konfigurasi Service
Hanya menggunakan konfigurasi default masih jauh dari cukup dalam kebanyakan kasus perlu mengubah banyak konfigurasi service untuk mencapai kebutuhan.
Konfigurasi Http
Dapat mengkonfigurasi melalui membuat Server net/http Gin sendiri juga mendukung penggunaan Gin seperti API native.
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())
}Konfigurasi Static Resource
Static resource di masa lalu pada dasarnya adalah bagian yang tidak terpisahkan dari server meskipun saat ini proporsi penggunaan secara bertahap berkurang tetapi masih banyak sistem yang menggunakan arsitektur monolitik.
Gin menyediakan tiga method untuk memuat static resource
// Memuat folder static tertentu
func (group *RouterGroup) Static(relativePath, root string) IRoutes
// Memuat fs tertentu
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
// Memuat file static tertentu
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutesTIP
relativePath adalah path relatif yang dipetakan ke URL web root adalah path aktual file dalam proyek
Misalkan direktori proyek adalah sebagai berikut
root
|
|-- static
| |
| |-- a.jpg
| |
| |-- favicon.ico
|
|-- view
|
|-- htmlfunc main() {
router := gin.Default()
// memuat direktori file static
router.Static("/static", "./static")
// memuat direktori file static
router.StaticFS("/view", http.Dir("view"))
// memuat file static
router.StaticFile("/favicon", "./static/favicon.ico")
router.Run(":8080")
}Konfigurasi CORS
Gin sendiri tidak melakukan penanganan apa pun untuk konfigurasi CORS perlu编写 middleware sendiri untuk mengimplementasikan kebutuhan yang sesuai sebenarnya kesulitannya tidak besar orang yang sedikit熟悉 protokol HTTP umumnya dapat menulisnya logika pada dasarnya sama.
func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// Server di production environment umumnya tidak mengisi * sebaiknya mengisi domain name yang ditunjuk
c.Header("Access-Control-Allow-Origin", origin)
// HTTP METHOD yang diizinkan
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// Request header yang diizinkan
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// Response header yang diizinkan client untuk mengakses
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// Apakah perlu membawa informasi kredensial Credentials bisa cookies authorization headers atau TLS client certificates
// Saat mengatur true Access-Control-Allow-Origin tidak boleh *
c.Header("Access-Control-Allow-Credentials", "true")
}
// Melepaskan request OPTION tetapi tidak mengeksekusi method selanjutnya
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// Melepaskan
c.Next()
}
}Daftarkan middleware sebagai middleware global即可
Session Control
Di era saat ini tiga jenis session control Web yang populer total ada tiga cookie session JWT.
Cookie
Informasi dalam cookie disimpan dalam bentuk key-value di browser dan data dapat dilihat langsung di browser
Kelebihan:
- Struktur sederhana
- Data persisten
Kekurangan:
Ukuran terbatas
Penyimpanan plain text
Mudah terkena serangan CSRF
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// mendapatkan cookie yang sesuai
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// mengatur cookie parameter: key val waktu eksistensi path domain apakah mengizinkan orang lain mengakses cookie melalui js hanya http
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Nilai Cookie: %s \n", cookie)
})
router.Run()
}Cookie murni lebih banyak digunakan lima enam tahun yang lalu namun penulis umumnya jarang menggunakan cookie murni untuk session control memang不太 aman.
Session
Session disimpan di server kemudian mengirim cookie disimpan di browser cookie menyimpan session_id setelah itu setiap request server melalui session_id dapat获取 informasi session
Kelebihan:
- Disimpan di服务端 meningkatkan keamanan mudah dikelola
Kekurangan:
- Disimpan di服务端 meningkatkan overhead server menurunkan performa
- Berdasarkan identifikasi cookie tidak aman
- Informasi autentikasi tidak sinkron dalam situasi distributed
Session dan Cookie tidak terpisahkan setiap kali menggunakan Session default adalah menggunakan Cookie. Gin default tidak mendukung Session karena Cookie adalah konten dalam protokol Http tetapi Session bukan namun ada middleware pihak ketiga yang mendukung instal dependensi即可 alamat repositori: gin-contrib/sessions: Gin middleware for session management (github.com)
go get github.com/gin-contrib/sessionsMendukung cookie Redis MongoDB GORM PostgreSQL
func main() {
r := gin.Default()
// membuat storage engine berbasis Cookie
store := cookie.NewStore([]byte("secret"))
// mengatur middleware Session mysession即 nama session juga nama cookie
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
// inisialisasi session
session := sessions.Default(c)
var count int
// mendapatkan nilai
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
// mengatur
session.Set("count", count)
// menyimpan
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}Umumnya tidak disarankan menyimpan Session melalui Cookie disarankan menggunakan Redis contoh lain silakan pergi ke repositori resmi untuk了解.
JWT
Kelebihan:
- Berdasarkan JSON通用 multi-bahasa
- Dapat menyimpan informasi non-sensitif
- Ukuran kecil mudah ditransmisikan
- Server tidak perlu menyimpan利于 distributed extensi
Kekurangan:
- Masalah refresh Token
- Setelah diterbitkan tidak dapat dikontrol secara aktif
Sejak revolusi front-end programmer front-end tidak lagi hanya "menulis halaman" tren front-end back-end terpisah semakin menjadi-jadi JWT adalah yang paling cocok untuk front-end back-end terpisah dan sistem distributed untuk session control memiliki keunggulan alami yang besar. Mempertimbangkan JWT sudah sepenuhnya keluar dari konten Gin dan tidak ada dukungan middleware karena JWT sendiri tidak terbatas pada framework atau bahasa apa pun di sini tidak akan讲解 secara detail dapat pergi ke dokumentasi lain: JWT
Log Management
Middleware log default yang digunakan Gin menggunakan os.Stdout hanya memiliki fungsi paling dasar karena Gin hanya fokus pada layanan web dalam kebanyakan kasus sebaiknya menggunakan framework log yang lebih成熟 namun ini tidak dalam pembahasan bab ini dan extensibility Gin sangat tinggi dapat dengan mudah mengintegrasikan framework lain di sini hanya讨论 layanan log built-in-nya.
Warna Console
gin.DisableConsoleColor() // menonaktifkan warna log consoleKecuali saat development kebanyakan时候 tidak disarankan mengaktifkan item ini
Log Tulis ke File
func main() {
e := gin.Default()
// mematikan warna console
gin.DisableConsoleColor()
// membuat dua file log
log1, _ := os.Create("info1.log")
log2, _ := os.Create("info2.log")
// merekam ke dua file log secara bersamaan
gin.DefaultWriter = io.MultiWriter(log1, log2)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}Log built-in gin mendukung menulis ke beberapa file tetapi kontennya sama penggunaan不太 nyaman dan tidak akan menulis request log ke file.
func main() {
router := gin.New()
// Middleware LoggerWithFormatter akan menulis log ke gin.DefaultWriter
// default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
//TODO menulis logika ke file yang sesuai
......
// output format kustom
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")
}Melalui middleware kustom dapat实现 log tulis ke file
Format Log Debug Route
Di sini hanya memodifikasi log output informasi route saat startup
func main() {
e := gin.Default()
gin.SetMode(gin.DebugMode)
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
if gin.IsDebugging() {
log.Printf("Route %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
}
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}Output
2022/12/21 17:19:13 Route GET /hello main.Hello 3Penutup: Gin bisa dibilang framework web bahasa Go yang paling mudah dipelajari karena Gin benar-benar mencapai minimalisasi tanggung jawab hanya bertanggung jawab pada layanan web fungsi autentikasi lainnya caching data dan sebagainya diserahkan kepada developer untuk menyelesaikan sendiri dibandingkan dengan framework besar dan lengkap Gin yang ringan dan ringkas lebih cocok dan seharusnya dipelajari oleh pemula karena Gin tidak memaksa penggunaan规范 tertentu bagaimana membangun proyek struktur apa yang diadopsi perlu dipertimbangkan sendiri bagi pemula lebih dapat melatih kemampuan.
