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) で書かれた Web フレームワークです。martini に似た API を持ち、httprouter のおかげでパフォーマンスが大幅に向上し、速度が 40 倍になっています。パフォーマンスと優れた生産性を求めるなら、Gin を気に入るはずです。Gin は Iris や Beego と比較すると、より軽量なフレームワークで、Web 部分のみを担当し、極限のルーティングパフォーマンスを追求しています。機能はそれほど充実していませんが、軽量で拡張しやすいという利点があります。したがって、すべての Web フレームワークの中で、Gin が最も簡単に習得できます。

特徴

  • 高速:Radix ツリーベースのルーティング、小さなメモリフットプリント。リフレクションなし。予測可能な API パフォーマンス。
  • ミドルウェアサポート:受信 HTTP 要求は一連のミドルウェアと最終操作で処理できます。例:Logger、Authorization、GZIP、最終操作 DB。
  • クラッシュ処理:Gin は HTTP 要求で発生した panic をキャッチして recover できます。これにより、サーバーは常に利用可能になります。
  • JSON 検証:Gin は要求された JSON を解析して検証できます。例えば、必要な値の存在を確認します。
  • ルートグループ:ルートをより適切に整理できます。認証が必要か、異なる API バージョンなど…… さらに、これらのグループは無制限にネストでき、パフォーマンスが低下することはありません。
  • エラー管理:Gin は HTTP 要求中に発生したすべてのエラーを収集する便利な方法を提供します。最終的に、ミドルウェアはそれらをログファイル、データベースに書き込んだり、ネットワーク経由で送信したりできます。
  • 組み込みレンダリング:Gin は JSON、XML、HTML レンダリングのために使いやすい API を提供します。
  • 拡張性:新しいミドルウェアを作成するのは非常に簡単です

インストール

現時点(2022/11/22)で、gin がサポートする Go の最低バージョンは 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"
}

ドキュメント

実際、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 のパラメータ解析は 3 つの方法をサポートしています:ルートパラメータ 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)
}

サンプル 1

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

サンプル 2

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

サンプル 1

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

サンプル 2

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

フォームパラメータ

フォームのコンテンツタイプには通常、application/jsonapplication/x-www-form-urlencodedapplication/xmlmultipart/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"])
}

サンプル 1: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-urlencodedmultipart/form-data タイプのフォームを解析します。

サンプル 2: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 レスポンスを直接行うことです。後者はそうしません。より柔軟なエラー処理を行いたい場合は、後者を選択することをお勧めします。これらの 2 つの関数は、要求の 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 をコンテキストに保存します。これによりパフォーマンスにわずかな影響があります。1 回の呼び出しでバインディングが完了する場合は、このメソッドを使用しないでください。この機能が必要なのは JSONXMLMsgPackProtoBuf などの形式のみです。QueryFormFormPostFormMultipart などの他の形式では、パフォーマンスの損失なしに 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

注意すべき点は、gin では validator の検証タグは binding で、単独で validator を使用する場合は検証タグが 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
// レスポンスヘッダーに書き込み、データレンダリングを行う
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 方式でレンダリング。ユニコードコードを 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, 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)

// データをレスポンスストリームに書き込む
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
// 現在の 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() {
    // 子 goroutine は 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"

両者の出力が異なることがわかります。コピーは複製時、安全のために多くの要素の値を削除しています。

ファイル転送

ファイル転送は Web アプリケーションに欠かせない機能で、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

一般的に、ファイルアップロードの MethodPOST を指定します。一部の企業は PUT を使用する傾向があります。前者は単純な HTTP 要求、後者は複雑な HTTP 要求です。具体的な違いについてはここでは詳述しませんが、後者を使用する場合、特にフロントエンドとバックエンドを分離したプロジェクトでは、適切な CORS 処理を行う必要があります。Gin のデフォルト設定は CORS をサポートしていません [CORS 設定](#CORS 設定)。

複数ファイルアップロード

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 が解析した multipart フォームを取得
  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) {

}

上記の 4 つのインターフェースがあると仮定します。一時的に内部実装は無視します。HelloLogin は 1 グループ、UpdateDelete は 1 グループです。

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

v1v2 の 2 つのグループに分類しました。中括弧 {} は単に規範のためで、中括弧内に登録されたハンドラーが同じルートグループに属することを示すだけで、機能的な役割はありません。同様に、gin はネストされたグループもサポートしており、方法は上記の例と同じなので、ここではデモンストレーションしません。

404 ルート

gin の Engine 構造体は 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"))
}

設定後、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 自体の機能は少ないですが、サードパーティコミュニティによって開発された gin 拡張ミドルウェアは非常に豊富です。

ミドルウェアの本質は実際にはインターフェースハンドラーです

go
// HandlerFunc は gin ミドルウェアによって使用されるハンドラーを戻り値として定義します
type HandlerFunc func(*Context)

ある意味では、すべての要求に対応するハンドラーもミドルウェアですが、作用範囲が非常に小さいローカルミドルウェアです。

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

gin のソースコードを確認すると、Default 関数で返されるデフォルトの Engine は 2 つのデフォルトミドルウェア Logger()Recovery() を使用しています。デフォルトのミドルウェアを使用したくない場合は、gin.New() を代わりに使用できます。

グローバルミドルウェア

グローバルミドルウェアは作用範囲がグローバルで、システム全体のすべての要求がこのミドルウェアを経由します。

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("グローバルミドルウェアが実行されました...")
   }
}

クロージャー関数を作成してミドルウェアを作成し、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 のミドルウェアは実際には責任連鎖パターンを使用しています。ContextHandlersChain を維持しており、本質的には []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() はルートの handlers 内の HandlerFunc をループして実行します。ここで 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 設定

net/http を通じて Server を作成して設定できます。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 は静的リソースを読み込むために 3 つのメソッドを提供します

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

CORS 設定

Gin 自体は CORS 設定に対して何の処理も行っておらず、ミドルウェアを自分で作成して対応する要件を実装する必要があります。実際、難易度は高くなく、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 METHOD
         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、authorization headers、TLS client certificates にできます
         // true に設定すると、Access-Control-Allow-Origin は * にできません
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // OPTION 要求を通過させるが、後続のメソッドは実行しない
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // 通過させる
      c.Next()
   }
}

ミドルウェアをグローバルミドルウェアとして登録するだけです

セッション制御

現在の時代では、一般的な 3 つの Web セッション制御は cookiesessionJWT です。

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、存在時間、ディレクトリ、ドメイン、他者が js を通じて cookie にアクセスできるかどうか、http のみ
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

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

    router.Run()
}

単純な cookie は 5〜6 年前によく使用されていましたが、著者は通常単純な cookie をセッション制御に使用することはあまりありません。これは確かに安全ではありません。

Session

session はサーバーに保存され、ブラウザに cookie を保存します。cookie には session_id が保存され、その後要求ごとにサーバーは session_id を通じて対応する session 情報を取得できます

利点:

  • サーバー側に保存され、安全性が向上し、管理が容易

欠点:

  • サーバー側に保存され、サーバー負荷が増大し、パフォーマンスが低下
  • 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 はセッション名で、cookie の名前でもあります
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // session を初期化
      session := sessions.Default(c)
      var count int
      // 値を取得
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // 設定
      session.Set("count", count)
      // 保存
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

通常、Cookie を通じて Session を保存することはお勧めしません。Redis を使用することをお勧めします。他の例については、公式リポジトリを参照してください。

JWT

利点:

  • JSON ベース、多言語共通
  • 機密でない情報を保存可能
  • 占有が小さく、転送が容易
  • サーバー側は保存不要、分散拡張に有利

欠点:

  • トークン更新問題
  • 一度発行すると積極的に制御できない

フロントエンド革命以来、フロントエンドプログラマーは単なる「ページを書く人」ではなくなりました。フロントエンドとバックエンドの分離の傾向がますます強まり、JWT はフロントエンドとバックエンドの分離および分散システムに最も適したセッション制御で、大きな自然な利点があります。JWT は完全に Gin の内容を離れ、ミドルウェアのサポートもないため、JWT 自体がどのフレームワークや言語にも限定されないことを考慮し、ここでは詳細な説明を行いません。別のドキュメントを参照してください:JWT

ログ管理

Gin がデフォルトで使用するログミドルウェアは os.Stdout を使用し、最も基本的な機能のみがあります。Gin は Web サービスのみに焦点を当てているため、ほとんどの場合、より成熟したログフレームワークを使用する必要があります。ただし、これは本章の議論範囲内ではなく、Gin の拡張性は非常に高く、他のフレームワークを簡単に統合できます。ここでは組み込みのログサービスのみを議論します。

コンソールカラー

go
gin.DisableConsoleColor() // コンソールログカラーを無効化

開発時を除き、ほとんどの場合、この項目を有効にすることはお勧めしません。

ログをファイルに書き込む

go
func main() {
  e := gin.Default()
    // コンソールカラーを無効化
  gin.DisableConsoleColor()
    // 2 つのログファイルを作成
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // 同時に 2 つのログファイルに記録
  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 言語の Web フレームワークの中で最も学習しやすいものです。Gin は実際に責任の最小化を実現し、単に Web サービスのみを担当し、他の認証ロジック、データキャッシュなどの機能は開発者に任せているためです。大而全のフレームワークと比較して、軽量で簡潔な Gin は初学者にとってより適しており、学習すべきです。Gin は特定の規範を強制せず、プロジェクトをどのように構築するか、どのような構造を採用するかは自分で考慮する必要があるため、初学者にとって能力を鍛えるのに最適です。

Golang学习网由www.golangdev.cn整理维护