Go Gin packageの基本を触る

Posted on
go gin

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

ドメイン間通信をできるようにしますが、セキュリティリスクを招くため使い所は注意のようですね。

これでできる! クロスブラウザJavaScript入門

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