初めに
ginとは何でしょうか
ginはGo(Golang)で書かれたWebフレームワークです。 httprouterのおかげで、最大40倍高速なパフォーマンスを備えたmartiniのようなAPIを備えています。パフォーマンスと優れた生産性が必要な場合は、Ginを好きになるでしょう。--公式ドキュメント
パフォーマンスが良いのが売りらしいですが、他のGo言語のwebフレームワークと比較してみます。
人気ランキング
githubのスターの多い順
Name | Stars | Forks | Issues Open | Issues Closed | Birth Year | Latest Update | Author |
---|---|---|---|---|---|---|---|
Gin | 34,231 | 3,900 | 183 | 1,115 | 2014 | 2019-12-27 | @manucorporat |
Beego | 22,890 | 4,600 | 737 | 1,900 | 2012 | 2019-12-27 | @astaxie |
Iris | 17,133 | 1,906 | 4 | 467 | 2016 | 2019-12-27 | @kataras |
Echo | 16,058 | 1,541 | 26 | 902 | 2015 | 2019-12-27 | @vishr |
また、国内に案件が存在するのは、現在GinとEchoだけです
準備
作業PCにGo言語が入ってることをまず確認してください。
go version
go version go1.13
ginをgetしましょう、新規プロジェクトを作る際にgomodを使用することをお勧めします。
gomodについてよくわからない方はこちらの記事を参考にしてください。
GOMODULE--Goのパッケージ管理
新規フォルダを作ってください、フォルダ名は任意で結構です。
mkdir gin_test && cd gin_test
go modを初期化します。
go mod init gin_test
初期化完了したら、ginのパッケージを取得します
go get -u github.com/gin-gonic/gin
ginの基礎
ginはlaravelやDjangoのようなフルスタックフレームワークと違って非常にライトなフレームワークになります、
最初の実例を書いてみよう。
package main
import "github.com/gin-gonic/gin"
import "net/http"
func main() {
engine:= gin.Default()
engine.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "hello world",
})
})
engine.Run(":3000")
}
go run main.go
でサーバー立ち上げて localhost:3000
にアクセスしてみると
Jsonメッセージの {"message":"hello world"}
が確認できます。
もしginを使わずに素のGoで同じサーバーを作る場合、以下のようになります。
package main
import (
"encoding/json"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, q *http.Request) {
message := map[string]string{
"message": "hello world",
}
jsonMessage, err := json.Marshal(message)
if err != nil {
panic(err.Error())
}
w.Write(jsonMessage)
})
http.ListenAndServe("127.0.0.1:3000", mux)
}
コードの量はほぼ倍になりました。
では、ginは実際何をしたのでしょうか、ginのソースコード見てみます。
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
}
素のGoで HandleFunc
を書く際に、パラメタとして存在する writermem responseWriter
や Request *http.Request
、ginの Context
の struct
の構成要素になってます。
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
engin.Run(":3000")
も実際 http.ListenAndServe(address, engine)
を中身で呼んでます。
Goのhttpサーバーのコードを活かして、便利な処理を追加するのがginです。
素のGoでhttpサーバー書いたことがあれば、ginのソースコードは非常にわかやすいはずです
ミドルウェアを使ってみよう
ginはリクエストに対する処理は基本下記のようになります
Request -> Route Parser -> Middleware -> Route Handler -> Middleware -> Response
実際処理する関数に到達する前に、必ずミドルウェアを通る必要があります、
簡単な例として、アクセスするユーザーのUser-Agent
を取得するミドルウェアを作ってみましょう。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
engine:= gin.Default()
ua := ""
// ミドルウェアを使用
engine.Use(func(c *gin.Context) {
ua = c.GetHeader("User-Agent")
c.Next()
})
engine.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "hello world",
"User-Agent": ua,
})
})
engine.Run(":3000")
}
http://localhost:3000/
にアクセスすれば、以下のメッセージが確認できるはずです。
{
User-Agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
message: "hello world"
}
HTMLや静的ファイルを扱う
HTMLの扱い
mkdir templates && cd templates && touch index.html
index.htmlの中身は以下のように
<h1 style="color: rebeccapurple;">{{.message}}</h1>
main.goを修正
package main
import "github.com/gin-gonic/gin"
import "net/http"
func main() {
engine:= gin.Default()
// htmlのディレクトリを指定
engine.LoadHTMLGlob("templates/*")
engine.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
// htmlに渡す変数を定義
"message": "hello gin",
})
})
engine.Run(":3000")
}
サーバー立ち上げてlocalhost:3000
にアクセルすると、以下の内容が確認できるはずです
静的ファイルを扱う
mkdir static
任意の画像をstaticフォルダに入れます。
main.goを以下のように修正します。
package main
import "github.com/gin-gonic/gin"
func main() {
engine:= gin.Default()
engine.Static("/static", "./static")
engine.Run(":3000")
}
サーバーを立ち上げて、http://localhost:3000/static/gin.png
をアクセスすると、画像が表示されるはずです。
ファイルのアップロード
まず画像保存用のフォルダを作っておきます。
mkdir images
package main
import (
"github.com/gin-gonic/gin"
"io"
"log"
"net/http"
"os"
)
func main(){
engine := gin.Default()
engine.POST("/upload", func(c *gin.Context) {
file,header, err := c.Request.FormFile("image")
if err != nil {
c.String(http.StatusBadRequest, "Bad request")
return
}
fileName := header.Filename
dir, _ := os.Getwd()
out, err := os.Create(dir+"\\images\\"+fileName)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
engine.Run(":3000")
}
フロントエンド、vue.jsを使ってます。 urlが /api/upload
になってるのはproxy
の設定に/api
で始まるurlのtarget
をginサーバーにリダイレクトしています。設定の仕方に疑問のある方はコメントください
<template>
<div id="app">
<input type="file" id="people-export" ref="input">
<button type="submit" @click="fileUpload">提出</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'app',
components: {
HelloWorld
},
methods: {
fileUpload(){
let file = this.$refs.input;
let formData = new FormData();
formData.append('image', file.files[0]);
axios({
method: 'post',
url :'/api/upload',
data : formData,
header :{
'Content-Type': 'multipart/form-data',
}
}).then(( res )=>{
console.log(res.data)
})
}
}
}
</script>
<style>
</style>
ginで簡単なREST風のAPIサーバーを作ってみよう
実装機能、書籍についての
- 新規追加
- データ一覧
- データ修正
- 削除
ディレクトリ構成
qiita
|- controller
|- |- book.go
|- middleware
|- |- bookMiddleware.go
|- model
|- |- book.go
|- service
|- |- book.go
|- |- init.go
|- go.mod
|- main.go
必要なパッケージをgetします。
go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql
go get github.com/go-xorm/xorm
go get go.uber.org/zap
main.go
役割はREST風のAPIを実装とmysql用のパッケージの初期化します。
package main
import (
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"qiita/controller"
"qiita/middleware"
)
func main(){
engine := gin.Default()
// ミドルウェア
engine.Use(middleware.RecordUaAndTime)
// CRUD 書籍
bookEngine := engine.Group("/book")
{
v1 := bookEngine.Group("/v1")
{
v1.POST("/add", controller.BookAdd)
v1.GET("/list", controller.BookList)
v1.PUT("/update", controller.BookUpdate)
v1.DELETE("/delete", controller.BookDelete)
}
}
engine.Run(":3000")
}
controller/book.go
機能としてはmain.goから振られたリクエストをserviceにハンドルし、レスポンスを返します。
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"qiita/model"
"qiita/service"
"strconv"
)
func BookAdd(c *gin.Context) {
book := model.Book{}
err := c.Bind(&book)
if err != nil{
c.String(http.StatusBadRequest, "Bad request")
return
}
bookService :=service.BookService{}
err = bookService.SetBook(&book)
if err != nil{
c.String(http.StatusInternalServerError, "Server Error")
return
}
c.JSON(http.StatusCreated, gin.H{
"status": "ok",
})
}
func BookList(c *gin.Context){
bookService :=service.BookService{}
BookLists := bookService.GetBookList()
c.JSONP(http.StatusOK, gin.H{
"message": "ok",
"data": BookLists,
})
}
func BookUpdate(c *gin.Context){
book := model.Book{}
err := c.Bind(&book)
if err != nil{
c.String(http.StatusBadRequest, "Bad request")
return
}
bookService :=service.BookService{}
err = bookService.UpdateBook(&book)
if err != nil{
c.String(http.StatusInternalServerError, "Server Error")
return
}
c.JSON(http.StatusCreated, gin.H{
"status": "ok",
})
}
func BookDelete(c *gin.Context){
id := c.PostForm("id")
intId, err := strconv.ParseInt(id, 10, 0)
if err != nil{
c.String(http.StatusBadRequest, "Bad request")
return
}
bookService :=service.BookService{}
err = bookService.DeleteBook(int(intId))
if err != nil{
c.String(http.StatusInternalServerError, "Server Error")
return
}
c.JSON(http.StatusCreated, gin.H{
"status": "ok",
})
}
middleware/bookMiddleware.go
リクエストのlogを記録します。
go.uber.org/zap
パッケージを使用しています、出前で有名なUberさんのオープンソースらしいです。
package middleware
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"log"
"time"
)
func RecordUaAndTime(c *gin.Context){
logger, err := zap.NewProduction()
if err != nil{
log.Fatal(err.Error())
}
oldTime := time.Now()
ua := c.GetHeader("User-Agent")
c.Next()
logger.Info("incoming request",
zap.String("path", c.Request.URL.Path),
zap.String("Ua", ua),
zap.Int("status", c.Writer.Status()),
zap.Duration("elapsed", time.Now().Sub(oldTime)),
)
}
model/book.go
bookの構造体を定義してます。
xorm
パッケージ使用して、テーブルの初期化でも使用します。
package model
type Book struct {
Id int64 `xorm:"pk autoincr int(64)" form:"id" json:"id"`
Title string `xorm:"varchar(40)" json:"title" form:"title"`
Content string `xorm:"varchar(40)" json:"content" form:"content"`
}
service/init.go
データベースへの接続とテーブルの初期化を実装します。
package service
import (
"errors"
"fmt"
"github.com/go-xorm/xorm"
"qiita/model"
"log"
)
var DbEngine *xorm.Engine
func init() {
driverName := "mysql"
DsName := "root:root@(192.168.99.100:3306)/gin?charset=utf8"
err := errors.New("")
DbEngine, err = xorm.NewEngine(driverName,DsName)
if err != nil && err.Error() != ""{
log.Fatal(err.Error())
}
DbEngine.ShowSQL(true)
DbEngine.SetMaxOpenConns(2)
DbEngine.Sync2(new(model.Book))
fmt.Println("init data base ok")
}
service/book.go
機能としてはコントローラから振られたdb操作を引き受け、結果を返します。
package service
import (
"qiita/model"
)
type BookService struct {}
func (BookService) SetBook(book *model.Book) error {
_, err := DbEngine.Insert(book)
if err!= nil{
return err
}
return nil
}
func (BookService) GetBookList() []model.Book {
tests := make([]model.Book, 0)
err := DbEngine.Distinct("id", "title", "content").Limit(10, 0).Find(&tests)
if err != nil {
panic(err)
}
return tests
}
func (BookService) UpdateBook(newBook *model.Book) error {
_, err := DbEngine.Id(newBook.Id).Update(newBook)
if err != nil {
return err
}
return nil
}
func (BookService) DeleteBook(id int) error {
book := new(model.Book)
_, err := DbEngine.Id(id).Delete(book)
if err != nil{
return err
}
return nil
}
サーバを立ち上げて、APIの動作を見てみます。
go run main.go
起動後、以下のlogも確認できるはずです。
内容はdbの初期化完了と実装されたAPI情報です。
[xorm] [info] 2020/01/01 00:40:53.621508 [SQL] SELECT `TABLE_NAME`, `ENGINE`, `TABLE_ROWS`, `AUTO_INCREMENT`, `TABLE_COMMENT` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE
_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB') [gin]
[xorm] [info] 2020/01/01 00:40:53.655668 [SQL] SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`, `COLUMN_KEY`, `EXTRA`,`COLUMN_COMMENT` FROM `INFORMATION
_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? [gin book]
[xorm] [info] 2020/01/01 00:40:53.656645 [SQL] SELECT `INDEX_NAME`, `NON_UNIQUE`, `COLUMN_NAME` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NA
ME` = ? [gin book]
init data base ok
[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] POST /book/v1/add --> qiita/controller.BookAdd (4 handlers)
[GIN-debug] GET /book/v1/list --> qiita/controller.BookList (4 handlers)
[GIN-debug] PUT /book/v1/update --> qiita/controller.BookUpdate (4 handlers)
[GIN-debug] DELETE /book/v1/delete --> qiita/controller.BookDelete (4 handlers)
[GIN-debug] Listening and serving HTTP on :3000
APIのテストはpostman
を使います。
新規追加 POST /book/v1/add
データベースにもちゃんとデータが入りました。データ一覧 GET /book/v1/list
データ修正 PUT /book/v1/update
データの削除 DELETE /book/v1/delete
長くなりましたが、以上となります。
ORMに関しては xorm
パッケージを使用してます、ドキュメントは公式リポジトリを参考にしてください -- リンク
最後に
最後まで読んで下さり、ありがとうございます。
実際ginを使ってみて、ライトな感覚に結構惹かれています。
フレームワークを使用してるけれど、過依存することもなく、素のGoの感覚でコーディングできます。
2020/09/11の追記
Ginに関する内容を追記します。
【簡単なREST風のAPIサーバー】と被る内容もありますが本記事をクックブックとして読みやすくするために敢えて抜粋しています。
Getリクエストからクエリを取得
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
serve := gin.Default()
serve.GET("/test", func(c *gin.Context) {
firstName := c.Query("first_name")
lastName := c.DefaultQuery("last_name", "default_name")
c.JSON(http.StatusOK, gin.H{"firstName":firstName, "lastName":lastName})
})
serve.Run("127.0.0.1:8080")
}
サーバーを立ち上げ、http://localhost:8080/test?first_name=山田
にアクセスすると下記のような結果になります。
DefaultQuery
関数を使用すれば、クエリ欠損する場合は設定したデフォルト値を使用します。
PostリクエストからFormデータを取得
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
serve := gin.Default()
serve.POST("/test", func(c *gin.Context) {
firstName := c.PostForm("first_name")
lastName := c.DefaultPostForm("last_name", "default_last_name")
c.JSON(http.StatusOK, gin.H{"firstName": firstName, "lastName": lastName})
})
serve.Run("127.0.0.1:8080")
}
サーバーを立ち上げ、127.0.0.1:8080/test
にPOSTリクエスト送信すれば、以下のレスポンスが戻ってきます。
テストツールはPOSTMAN使用をしています。
データのバリデーション
構造体を使用したバリデーション
package main
import (
"github.com/gin-gonic/gin"
)
type Person struct {
Age int `form:"age" validate:"required,gt=10,max=100"`
Name string `form:"name" validate:"required"`
Address string `form:"address" validate:"required"`
}
func main() {
serve := gin.Default()
serve.GET("/testing", func(c *gin.Context) {
var person Person
if err := c.ShouldBind(&person);err!=nil{
//c.String(500, "%v",err)
c.JSON(500, gin.H{"msg": err.Error()})
return
}
//c.String(200, "%v", person)
c.JSON(200, gin.H{"person": person})
})
serve.Run("127.0.0.1:8080")
}
構造体を使用してバリデーション行う際に、検証条件をタグのvalidate
に追加すれば、リクエスト本文をバインドする際に自動的にバリデーション行えます。
また、複数の検証条件がある際に,
を使て分けることができます。
サーバーを立ち上げてhttp://127.0.0.1:8080/testing?age=11&name=山田&address=どこか
にGetリクエストを送信。
ageは11になってるため、以下の結果が帰ってきました。
ageを9に修正して、再度リクエスト送信しますと、500エラーが戻ってきました。
認証ルールちゃんと機能してます。
下記のように、子構造体の検証条件を使用する際に、親構造体の検証条件にdive
を追加する必要があります。
...
type user struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type data struct {
User []user `json:"user" binding:"required,dive"` // use dive tag
}
...
検証条件一覧は 公式ドキュメントを参考にしてください。
検証条件のカスタマイズ
検証条件のカスタマイズをするにはvalidator
パッケージが必要です、本記事はv10系使用してます。
go get gopkg.in/go-playground/validator.v10
でインストールすることができます。ドキュメント公式サイト
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
"time"
)
// bookabledateというカスタマイズ検証条件を追加
type Booking struct {
CheckIn time.Time `form:"check_in" validate:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" validate:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
//カスタマイズ検証条件の関数
//チェックインする時間は現在時間より先である必要があります
func bookableDate(fl validator.FieldLevel) bool {
today:=time.Now()
if date,ok:=fl.Field().Interface().(time.Time);ok{
if date.Unix()>today.Unix(){
fmt.Println("date unix :",date.Unix())
return true
}
}
return false
}
func main() {
serve := gin.Default()
v := validator.New()
v.RegisterValidation("bookabledate", bookableDate)
serve.GET("/bookable", func(c *gin.Context) {
var b Booking
if err := c.ShouldBind(&b);err!=nil{
c.JSON(500,gin.H{"error":err.Error()})
c.Abort()
return
}
if err := v.Struct(b); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
c.Abort()
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok!", "booking":b})
})
serve.Run("127.0.0.1:8080")
}
bookableDate
というカスタマイズ検証条件関数では現在時間のタイムスタンプとチェックイン時間のを比較し、チェックイン時間の方が大きければfalseを返します。
サーバーを立ち上げ、http://localhost:8080/bookable?check_in=2020-09-13&check_out=2020-09-15
へリクエスト送信。
check_inの値が2020-09-13
のタイムスタンプが現在時間(記事の更新時間)より大きいため、trueが戻ります。
check_inを十年前の2010-09-13
に修正して、再度リクエスト送信すると検証に引っかかりました。
バリデーションの多言語対応
検証条件のカスタマイズと同じようにvalidator
パッケージが必要です。
インストール: go get gopkg.in/go-playground/validator.v10
package main
import (
"github.com/gin-gonic/gin"
en2 "github.com/go-playground/locales/en"
ja2 "github.com/go-playground/locales/ja"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
ja_translations "github.com/go-playground/validator/v10/translations/ja"
"net/http"
)
type Person struct {
Age int `form:"age" validate:"required,gt=10"`
Name string `form:"name" validate:"required"`
Address string `form:"address" validate:"required"`
}
var (
Uni *ut.UniversalTranslator
Validate *validator.Validate
)
func main() {
Validate = validator.New()
ja := ja2.New()
en := en2.New()
Uni = ut.New(ja, en)
serve := gin.Default()
serve.GET("/testing", func(c *gin.Context) {
locale := c.DefaultQuery("locale", "ja")
trans, _ := Uni.GetTranslator(locale)
switch locale {
case "ja":
ja_translations.RegisterDefaultTranslations(Validate,trans)
case "en":
en_translations.RegisterDefaultTranslations(Validate,trans)
default:
ja_translations.RegisterDefaultTranslations(Validate,trans)
}
var person Person
if err := c.ShouldBind(&person);err!=nil{
c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()})
c.Abort()
return
}
if err := Validate.Struct(person);err!=nil{
errs := err.(validator.ValidationErrors)
sliceErrs :=[]string{}
for _, e := range errs{
sliceErrs = append(sliceErrs, e.Translate(trans))
}
c.JSON(http.StatusInternalServerError, gin.H{"err": sliceErrs})
c.Abort()
return
}
c.JSON(http.StatusOK, gin.H{"msg": person})
})
serve.Run("127.0.0.1:8080")
}
サーバーを起動し、http://localhost:8080/testing?locale=en
にアクセスすると、エラーメッセージが英語になっています。
localeをjaにすれば、エラーメッセージが日本語になります。
また、何も渡さない場合でも、エラーメッセージが日本語になります。