gin-gonic/ginが圧倒的な人気を誇っているので、README.mdにある一通りの機能でWebAPI(JSON format)の想定で関わりそうな機能を試していきます。
記載してあるコードはREADME.mdにあるコードを抜粋したり、改変したりして記載しています。
QuickStart
とりあえずQuickStartのコードを試してみます。
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
}
$ curl localhost:8080/ping
{"message":"pong"}
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2020/08/16 - 16:43:49 | 200 | 132.118µs | 127.0.0.1 | GET "/ping"
かっこい・・・、この時点でときめいてしまいますね。
早速気になった部分を見てみます。
- gin.Default()
最初にHandlerChainを登録するんですね。engine.Use()にLogger()とRecover()があるので必要に応じてこの要領でWrapしたり追加すれば良さそうですね。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
- r.GET()
EndpointのPathとHandlerを登録してくれるようです。HandlerFuncはgin.Contextを引数とするもので抽象化しているようです。
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
type HandlerFunc func(*Context)
- c.JSON()
ginのctxを受けて、status codeとJSONにMarshalするDataを渡せば良さそうですね。
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
API Examples
Using GET, POST, PUT, PATCH, DELETE and OPTIONS
router.{HTTP METHOD}で登録して行けばいいんですね。第2引数はfunc(c *gin.Context)
を満たす関数ですね。簡単にはクロージャーでしょうが、別のパッケージに分けていけば良さそうですね。
func main() {
router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
router.Run()
}
Parameters in path
pathのヒットする条件をhiddenに書いた。
func main() {
router := gin.Default()
// 200 -> /user/john, 301 -> /user/john/, 404 -> /user/john/get
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 200 -> /user/john/get, /user/john/get/ok
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.POST("/user/:name/*action", func(c *gin.Context) {
// /user/john/get/ok も /user/:name/*actionと等価になる
fmt.Println(c.FullPath() == "/user/:name/*action")
})
router.Run(":8080")
}
Querystring parameters
Query: firstnameがなければGuestを定義もしてくれるようですね。
func main() {
router := gin.Default()
// welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run(":8080")
}
掘ってた結果、このメソッドのwrapのようです。 つまりc.RequestでRequestにアクセスできそうですね。
func (c *Context) getQueryCache() {
if c.queryCache == nil {
c.queryCache = c.Request.URL.Query()
}
}
Multipart/Urlencoded Form
これはapplication/x-www-form-urlencoded
, multipart/form-data
の時にformを参照する方法のようです。
func main() {
router := gin.Default()
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}
ちゃんと中身見てJSONで返してくれていますね。
$ curl -X POST localhost:8080/form_post -d 'message=urlencoded'
{"message":"urlencoded","nick":"anonymous","status":"posted"}
$ curl -X POST localhost:8080/form_post -F 'message=multipart'
{"message":"multipart","nick":"anonymous","status":"posted"}
Another example: query + post form
組み合わせればqueryもformも取れるよということだけなので割愛
Map as querystring or postform parameters
QueryをMap化できる、keyの存在確認かな?Validatorとか、Bind使うほうが良さそう?
Grouping routes
Groupingは簡単ですね。
func main() {
router := gin.Default()
v1 := router.Group("/v1")
v1.GET("/get", func(c *gin.Context) {
c.String(200, "/v1/GET")
})
v2 := router.Group("/v2")
{
v2.GET("/get", func(c *gin.Context) {
c.String(200, "/v2/GET")
})
}
router.Run(":8080")
}
Blank Gin without middleware by default
各種Middlewareが存在しない初期状態でのスタート方法です。
func main() {
router := gin.New()
v1 := router.Group("/v1")
v1.GET("/get", func(c *gin.Context) {
c.String(200, "/v1/GET")
})
router.Run(":8080")
}
Using middleware
Groupの場合であっても.Use()で適用させていけば良さそうですね。
ただAuthRequired()などの自作Middlewareはgin.HandlerFunc
を返す必要がありますが、いまいちわからないですね。。既存のPackageでの作り方を調べたほうが良さそう。
func main() {
// Creates a router without any middleware by default
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
// authorized := r.Group("/", AuthRequired())
authorized := r.Group("/")
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// nested group
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}
r.Run(":8080")
}
Custom Recovery behavior
RecoveryのMiddlewareの変更する例なので省略します。
How to write log file
MultiWriterを利用してos.Stdinだけではなくfile書き込みする方法ですね。これは大事ですね。
package main
import (
"io"
"os"
"github.com/gin-gonic/gin"
)
func main() {
// gin.DisableConsoleColor()
f, _ := os.Create("gin.log")
// gin.DefaultWriter = io.MultiWriter(f)
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
[GIN] 2020/08/16 - 18:48:10 | 200 | 83.465µs | 127.0.0.1 | GET "/ping"
[GIN] 2020/08/16 - 18:48:12 | 404 | 860ns | 127.0.0.1 | GET "/ping/pong"
Custom Log Format
Defaultも結構好きですが、時刻とかはいじったほうがカスタムできるのはいいですね
func main() {
router := gin.New()
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
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")
}
127.0.0.1 - [Sun, 16 Aug 2020 18:50:16 JST] "GET /ping HTTP/1.1 200 160.899µs "curl/7.54.0" "
127.0.0.1 - [Sun, 16 Aug 2020 18:50:19 JST] "GET /ping/pong HTTP/1.1 404 587ns "curl/7.54.0" "
Controlling Log output coloring
ご自由にですね
Model binding and validation
go-playground/validatorを利用したvalidation機能の提供ですね。それ自体はココの解説がわかりやすいです。
MustBindWithだとレスポンスが定まってしまうので、次のようにshouldBindWithのほうがいいですね。
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
router.POST("/loginForm", func(c *gin.Context) {
var form Login
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
router.Run(":8080")
}
Custom Validators
これはgo-playground/validatorでcustom validatorを使うのと同じ流れかと思います。
Only Bind Query String
URL queryのなかに該当のkeyをbindして取得してくれる
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
$ curl "localhost:8085/testing?name=test&address=tokyo"
2020/08/16 21:04:59 ====== Only Bind By Query String ======
2020/08/16 21:04:59 test
2020/08/16 21:04:59 tokyo
[GIN] 2020/08/16 - 21:04:59 | 200 | 73.068µs | 127.0.0.1 | GET "/testing?name=test&address=tokyo"
Bind Query String or Post Data
これはpost dataを受けてbindする方法ですね。
Bind Uri
Uriをbindする方法ですね。
Bind Header
こっちはHeaderをbindする方法ですね。
XML, JSON, YAML and ProtoBuf rendering
gin.H()で返すか、構造体を作成してつけるか。
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
r.GET("/moreJSON", func(c *gin.Context) {
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
SecureJSON
JSON hijacking対策のための方法のようです。
なぜGoogleはJSONの先頭に while(1); をつけるのか
JSONP
ドメイン間通信をできるようにしますが、セキュリティリスクを招くため使い所は注意のようですね。
AsciiJSON
JSONでASCII以外はすべてescapeする。
PureJSON
次のようにHTML tagをピュアにする方法だがセキュリティの問題があるため使わないこと。
$ curl localhost:8080/json
{"html":"\u003cb\u003eHello, world!\u003c/b\u003e"}
$ curl localhost:8080/purejson
{"html":"<b>Hello, world!</b>"}
Using BasicAuth() middleware
Custom HTTP configuration
これは標準パッケージの時と同じですね。
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
Support Let’s Encrypt
gin-gonic/autotls
を利用してLet’s Encryptを利用できるみたいですね
Run multiple service using Gin
goroutineによって複数のhttp serverを同時に立ち上げるようですが、、使うケースってなんだろう。。
Graceful shutdown or restart
API WebサーバーをGracefulにshutdownする方法として以前はendless
などの3rd partyを使うテクニックがあったようですが、標準パッケージにShutdown()が追加されたので、次のようなテクニックが生まれたようです。
kill signalを受け取るchannelとserverを動かすchannelを別にして、kill signalを受け取ったらShutdown()を動作させて、Graceful Shutdownを実現するみたいですね。
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}
まとめ
一通り読み終わりました。ベーシックな仕組みを作るなら次の内容あたりを抑えれば作れそうですね。
- Router
- Validator
- Bind Uri, Header, Query, Post Data
- JSON rendring
- Using BasicAuth() middleware
- Graceful shutdown