ぺい

渋谷系アドテクエンジニアの落書き

登壇経験の棚卸ししてみた

自分的に一段落着いたと感じたこともあり、一度、経験を棚卸ししてみた 今までの登壇の種類とかをまとめてみると以下のような感じ。

  • 座学
  • ワークショップ
  • ハンズオン
  • カンファレンストーク
  • ライブコーディング
  • LT(これは登壇?なのか?)

それぞれについて、詳しく経験をまとめたいところですが、非常に長くなりそうなのでry

登壇は学びの宝庫

登壇は最大のアウトプットにして、最大のインプット。つまり、学びの宝庫であると僕は思っています。もちろん、良いことばかりではありません。事前に資料とか構成を決めるのは大変ですし、発表に対するアンケート結果とかを見て、胸を痛めることもあります。しかし、そこで得られる経験は挑戦しなければ得られないものなので、後で自分のためになることばかりです。

ちなみに、アンケート結果などで心が痛まないのか?という人向けに先に解答をしておくと、私も人間なので当然痛いです。私なりにやっているアンケート結果の使い方は、すごく極端な評価をしている人の意見に注目するのではなく。(極端に高いとか低い点数のこと)中間あたりの評価をつけている人たちをどうすれば満足と思わせることが出来たのか?ということに注目するようにして、次回の発表に活かすようにしています。極端に悪いところばかりに目が行きがちですが、大半はどっちでもない人達なので、その人達に向けて何が出来るか考える方が建設的です。

他人に説明することで、知識が整理される

俺は理解した!と感じつつも、いざ発表するとなると、アレ?これってどういう風に言えばよかったんだっけ?みたいなことが起きがち。なので、理解したという自信をつけたいトピックをテーマにするのは結構ありだと感じています。

分からないが見える化

人に説明するとなると、大体の人が自分で理解する時以上に、丁寧に調べると思います。そうすると、アレ?これってなんだろうが出て来ることがあります。そうした過程で、知識が穴ぼこになっていたところが綺麗に埋められ、発表する前よりも深く理解が出来た状態になったりします。

発表をすると、フィードバックが得られる

登壇後などの懇親会などで、直接やSNS上で、自分の話した内容に対しての意見やフィードバックが得られることが結構あります。これが一番の学びの瞬間。また、そこから新たな議論が生まれたり、改善点が見えたり、新しく話すべきトークテーマが出てきたりします。発表をした時こそ、色々な人と話すようにすると学びが加速するので、積極的に話すと効果が最大化すると感じている。

それぞれの発表形式ある気をつけるべき点

今後、発表したいなーと感じている人向けにちょっとアドバイスっぽいものを書いてみた。

座学

普通にやると暇になりがちなので、参加者へ質問を投げかけたり、質問をたくさん受けるようにした方が良さそう。

ハンズオン

どんなOSでも動くようにしておくか、参加者へ前提条件を提示しておく。

カンファレンストーク

みんなに満遍なくウケる話を意識しすぎると、つまらなくなりがちなので、俺の思う面白い話!みたいなノリくらいで良かったり。

ライブコーディング

色んな人に理解してもらうために、丁寧にやりすぎる時間が無限に足りないので、ある程度見切りをつけてガンガン進めて、本来やりたかったところまで達成するようにした方がいい。また、ある程度どうやって進めるのかは台本を用意して、がっつり準備するくらいで良いw

LT

勢い命。

まとめ

4月から社会人になったので、これからは質も上げれるように頑張っていきたい。

登壇で今後大事にすること

登壇で伝えたいことは事前に言語化して、聴衆にそれを持ち帰ってもらうことを意識する。
未だによくある失敗なのですが、当日想定していたよりも、寄り道が増えてしまったりして(寄り道というのは前提知識のインプットとかで色々派生してとか) 、本来伝えたかったことがうまくまとまらず終わってしまうことがあるので、気をつけていきたい。

【swaggo】GoのGoDocからSwaggerを書き出そう(基本編)

swaggo

swaggo

今回紹介するswaggoyvasiyarov/swaggerにインスパイアを受けて、作成したOSSになります。現在、yvasiyarov/swaggerは開発が止まっているので、いくつかの問題が放置されたままになっています。(これはOSSなので仕方ないです) swaggoは元の構文をそのまま流用して、機能追加をしているものになります。

  • Swagger 2.0の対応
  • 複雑な構造体の解析
  • カスタムヘッダー
  • example value
  • セキュリティ

私がざっと見た限り、上の内容はswaggoで無ければ使えないっぽいです。

Swagger便利だけど、書くのがそもそもだるい

最近になって、よくSwaggerというワードを目にするようになりました。これはとても画期的で、APIに実際にリクエストを投げれるドキュメントが出来上がります。 しかし、このドキュメントのコードを書くのが結構面倒!

もし、既に実装されているコードからSwaggerを生み出すことが出来るものがあったら、最高ですよね?ということで、今回はswaggoを使ったドキュメント生成を紹介します。

実装方法

だらだら書いても仕方ないので、さくっとコード例を書きます。 今回の記事に使ったコードはこちらにあります。 また、今回、紹介する内容は、公式ドキュメントに詳しく記載があります。

General API Info

Swaggerの基本情報をmain.go(オプションで変えることも出来ます)に書きます。 設定出来る項目

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/swaggo/gin-swagger"
    "github.com/swaggo/gin-swagger/swaggerFiles"
    "github.com/swaggo/swag/example/celler/controller"
    _ "github.com/swaggo/swag/example/celler/docs"
)

// @title Swagger Example API
// @version 1.0
// @description This is a sample server celler server.
// @termsOfService http://swagger.io/terms/

// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host localhost:8080
// @BasePath /api/v1

// @securityDefinitions.basic BasicAuth

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

// @securitydefinitions.oauth2.application OAuth2Application
// @tokenUrl https://example.com/oauth/token
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.implicit OAuth2Implicit
// @authorizationurl https://example.com/oauth/authorize
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/oauth/token
// @scope.read Grants read access
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information

// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
// @tokenUrl https://example.com/oauth/token
// @authorizationurl https://example.com/oauth/authorize
// @scope.admin Grants read and write access to administrative information
func main() {
    r := gin.Default()

    c := controller.NewController()

    v1 := r.Group("/api/v1")
    {
        accounts := v1.Group("/accounts")
        {
            accounts.GET(":id", c.ShowAccount)
            accounts.GET("", c.ListAccounts)
            accounts.POST("", c.AddAccount)
            accounts.DELETE(":id", c.DeleteAccount)
            accounts.PATCH(":id", c.UpdateAccount)
            accounts.POST(":id/images", c.UploadAccountImage)
        }
        /...
    }
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    r.Run(":8080")
}

API Operation

エンドポイントに関する情報書きます。 設定出来る項目 よく使う項目をピックアップして説明致します。

package controller

//...

// ShowAccount godoc
// @Summary Show a account
// @Description get string by ID
// @Accept  json
// @Produce  json
// @Param  id path int true "Account ID"
// @Success 200 {object} model.Account
// @Failure 400 {object} controller.HTTPError
// @Failure 404 {object} controller.HTTPError
// @Failure 500 {object} controller.HTTPError
// @Router /accounts/{id} [get]
func (c *Controller) ShowAccount(ctx *gin.Context) {
    id := ctx.Param("id")
    aid, err := strconv.Atoi(id)
    if err != nil {
        NewError(ctx, http.StatusBadRequest, err)
        return
    }
    account, err := model.AccountOne(aid)
    if err != nil {
        NewError(ctx, http.StatusNotFound, err)
        return
    }
    ctx.JSON(http.StatusOK, account)
}
エンドポイントの説明
// @Summary Show a account`
// @Description get string by ID`

MimeType

// @Accept  json
// @Produce  json

APIが許可しているMimeTypeを設定できます。 設定出来るMimeType

Param

// @Param パラメーター名 種類 型 必須要素か? コメント 
// @Param id path int true "Account ID"
// @Param q query string false "name search by q"
// @Param account body model.AddAccount true "Add account"
// @Param Authorization header string true "Authentication header"
  • path: パスパラメーター
  • query: クエリパラメータ
  • body: そのまんまです
  • header: カスタムリクエストヘッダー

パラメーターの型として、string, int, fileなどが利用が出来ます。

Result

// @Success ステータスコード パラメーターの型 データの型 コメント 
// @Success 200 {object} model.Account
// @Failure 400 {object} controller.HTTPError
// @Failure 404 {object} controller.HTTPError
// @Failure 500 {object} controller.HTTPError

成功のパターンと失敗のパターンを必要分だけ用意します。

構造体の指定の仕方

// @Success 200 {object} model.Account

上記のように書くと、package名.構造体名のような感じで探しに行って、ヒモ付を行なってくれます。

package model

// ...

type Account struct {
    ID   int    `json:"id" example:"1"`
    Name string `json:"name" example:"account name"`
}

exampleに設定された値は、Swaggerで出した時の具体例として設定されます。 jsonに設定された値は、JSONのキー名を決定します。 つまり、上記の例の場合、{"id": 1, "name": "account name"}というJSONが出来上がります。

Security

// @Security ApiKeyAuth

General API Infoで定義したセキュリティを使うことが出来ます。これの使い方については、別の記事で詳しく取り上げる予定です。
Authentication - Swagger

Router

// @Router /accounts/{id} [get]

エンドポイントのURIとメソッドを指定します。このURIはGeneral API infoでのBasePathからの相対パスを設定する必要があります。

// @BasePath /api/v1

今回の場合だと、最終的に出来上がるエンドポイントは、/api/v1/accounts/{id}となります。

Swaggerコード生成

$ go get -u github.com/swaggo/swag/cmd/swag
$ swag init

上記をmain.goのあるパスで実行すると、docsフォルダが出来上がります。そこに、swaggerドキュメントに関するコードが生成されます。

.
├── README.md
├── controller
├── docs <-- これが自動生成される
│   ├── docs.go
│   └── swagger
│       ├── swagger.json
│       └── swagger.yaml
├── main.go
└── model
i -h
NAME:
   swag init - Create docs.go

USAGE:
   swag init [command options] [arguments...]

OPTIONS:
   --generalInfo value, -g value  Go file path in which 'swagger general API Info' is written (default: "main.go")
   --dir value, -d value          Directory you want to parse (default: "./")
   --swagger value, -s value      Output the swagger conf for json and yaml (default: "./docs/swagger")

ちなみに、このinitコマンドには、上記のようなオプションがあるので、自分の環境に合わせて変更が可能です。

//...
"/accounts/{id}": {
            "get": {
                "description": "get string by ID",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "summary": "Show a account",
                "operationId": "get-string-by-int",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Account ID",
                        "name": "id",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "schema": {
                            "type": "object",
                            "$ref": "#/definitions/model.Account"
                        }
                    },
                    "400": {
                        "schema": {
                            "type": "object",
                            "$ref": "#/definitions/controller.HTTPError"
                        }
                    },
                    "404": {
                        "schema": {
                            "type": "object",
                            "$ref": "#/definitions/controller.HTTPError"
                        }
                    },
                    "500": {
                        "schema": {
                            "type": "object",
                            "$ref": "#/definitions/controller.HTTPError"
                        }
                    }
                }
            },
//...

実際にSwaggerからリクエストを投げてみよう

swaggerのコードを読み込んで、使うのも良いですが、今回はswaggoが用意している便利な関数を使って、UIを呼び出したいと思います。ちなみに、そのコードは既に例に含まれています。

r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

http://localhost:8080/swagger/index.htmlには既にSwaggerUIが展開されており、リクエストが出来る状態になっています。 このハンドラは、gin Echo net/httpをサポートしています。対応していないものを使っている場合は、自分でSwaggerUIを用意する必要があります。(PR投げれば種類を増やせます!)

スクリーンショット 2018-03-12 13.34.33.png

まとめ

現状、簡単なAPIなら十分ドキュメント生成に活用出来ます。是非使ってみてください。 あと、自分が関わっているOSSということもあり、意見募集中です!w

学生は勉強と実践どちらが大事なんだろうか: HAL Advent Calendar 2017

HAL Advent Calendar 2017

今年も頑張るぞい 。 私はいまHAL大阪という学校に通っている「ぺい」です。最近はGo言語がマイブームです。いま力を入れて勉強しているのはインタプリタと英語です。

学生は勉強と実践どちらが大事なんだろうか

結論: 陳腐化しない内容を勉強する。実践はいずれ誰でもすること。

いまの自分のままで大丈夫か?という”焦り”

就職を来年に控えて、エンジニアとして生きていくわけなんですが、「学校の授業は全く役に立たないので、さっさと働いて実力つけたい!」と結構前から思っています。その背景にあるのは、”焦り”です。そういう感情が生まれたのは、昨年のインターンで同期の優秀なエンジニア達との交流からで、このままじゃ自分は通用しない!大変だ!とどこかで焦っているからだと思います。恐らくこの記事を読んでいる人の中にも、私と同じように解消されない焦りを抱えている人や、どこかで不安を感じている人はいるでしょう。今回はそういった人向けに、今年一年間で私なりに出た結論を紹介致します。何か参考になれば幸いです。

実践で学べないことはある

「勉強するよりも、実践を積み重ねた方が実力伸びるんじゃね?」と考えた人はいると思いますが、実践では学べないこともあります。それは会社に入社する場合でも、起業をする場合であってもです。 それは、業務に直接必要がない知識です。では、自分のケースで考えてみましょう。

入社予定の会社は、アドテクやメディアをメイン事業として展開しており、社内だけで幅広い分野に携わることが出来る。また、定期的に技術審査会が行われるので、自分の実力などを客観的に評価されるタイミングを定期的に得ることが出来る。 現状は海外展開が目立って行われていないので、エンジニアは英語を業務で使うことはない。

仕事の中で学べそうなこと

  • パフォーマンスチューニング
  • BtoC BtoBのシステム開発
  • インフラ、サーバーサイド、フロントなど様々な開発
  • レビュアー、レビューイ
  • その他色々

恐らく仕事で学ぶ可能生がないこと

ここで分かることは、開発経験などは仕事で結構出来そうです。(もし、東京在住なら内定者バイトいてますが・・・w) 逆に、英語や低レイヤな内容は、業務でやらなさそうなので、何年間やったとしても体得することは出来なさそうです。 ここで言いたいことは、実践だけでは得られない知識は存在するということです。

役に立たない = すぐ使えないもの ≠ ずっと役に立たない

いま自分は、インタプリタと英語の勉強をしています。そんな業務で使わないようなものなのに、それって意味ある?と思う方はいると思います。これは確かにお金には直結しないかもしれません。しかし、いま学んでいる内容は、今後長く使える知識だということが重要です。つまり、見えている将来では役に立たないかもしれませんが、もっと先の将来では役に立つかもしれません。 例えば、いま私が読んでいる本はWriting An Interpreter In Goで、全て英語で書かれている洋書です。これを読むモチベーションは、いままで書いてきたプログラムは動いているのかを少しでも理解したい気持ちとASTを使ったコード生成に興味が湧いたからです。また、洋書は読むのが結構辛いのですが、英語の勉強を丁度していたので、どうせならまとめて勉強出来るから、頑張ってみようと始めました。ちなみに英語は週に一回ペースで、英会話に通っています。 インタプリタにしても、英語にしても、共通しているのは、一度学べば陳腐化することはあまり無い。しかし、習得にかなりの時間を要することです。こういう特徴を持ったものを社会人になってからやるのは結構辛いです。だからこそ、いま実践よりも優先して学ぶことにしました。

時間がある内に出来ることに注目

内定先でバイトしたり、もっと実践的な場に身を置きたい!などの気持ちは分かります。私みたいに住んでいる場所や学校のカリキュラム的に無理という人は居ると思いますが、実践はいずれみんなすることです。いまだからこそ出来ることはあるので、探してみることをおすすめします。

Go言語で薄く作るAPI(go-chi/chi) #2 カスタムハンドラ

GoっぽくAPIを作る

前回

Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築 - ぺい

今回

  • カスタムハンドラ
    もっと書く予定だったのですが、ハンドラだけですごいボリュームなったので、ハンドラだけにしましたw

go-chi-api-example/02 at master · pei0804/go-chi-api-example · GitHub

$ go get github.com/pei0804/go-chi-api-example
$ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/02
$ make run
go build -o build . && ./build
2017/11/26 00:05:04 Starting app

$ make curl-members-id id=01
curl -H 'Auth:admin' http://localhost:8080/api/members/01
{
    "id": 1,
    "name": "name_1"
}

カスタムハンドラ

カスタムハンドラを導入することで、コントローラー(以降ハンドラとする)の実装が少し変わります。まずは、以下のソースを見てください。

カスタムハンドラなし

// Show endpoint
func (h *Handler) Show(w http.ResponseWriter, r *http.Request) {
    type json struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := json{ID: id, Name: fmt.Sprint("name_", id)}
    respondJSON(w, http.StatusOK, res)
}

// respondJSON レスポンスとして返すjsonを生成して、writerに書き込む
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
    response, err := json.MarshalIndent(payload, "", "    ")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    w.Write([]byte(response))
}

普通に実装すると、type HandlerFunc func(ResponseWriter, *Request)の型で実装して、ハンドラを作成しています。net/httpパッケージの実装には以下のように書かれています。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

カスタムハンドラあり

// Show endpoint
func (c *Controller) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := User{ID: id, Name: fmt.Sprint("name_", id)}
    return http.StatusOK, res, nil
}

カスタムハンドラを使った実装をしているハンドラは、type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)という型で実装しています。では、これは何に準拠された実装になるのか?また何が嬉しいのか? 実はこれも、type HandlerFunc func(ResponseWriter, *Request)に最終的に合わせて処理をしています。それを理解するには、もう少しソースを追いかける必要があります。

func (s *Server) Router() {
    c := NewController()
    s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection"))
        api.Route("/members", func(members chi.Router) {
            members.Get("/{id}", handler(c.Show).ServeHTTP) // ここでハンドラが呼ばれる
            members.Get("/", handler(c.List).ServeHTTP)
        })
    })
    s.router.Route("/api/auth", func(auth chi.Router) {
        auth.Get("/login", handler(c.Login).ServeHTTP)
    })
}

members.Get("/{id}", handler(c.Show).ServeHTTP) というのを一つずつ分解すると、

Get(pattern string, h http.HandlerFunc)

  • chiパッケージのメソッド
  • patternはエンドポイントになる文字列
  • http.HandlerFunctype HandlerFunc func(ResponseWriter, *Request)

handler(c.Show).ServeHTTP

  • type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error) という今回作成した型
  • c.Show ハンドラc.type Controller structを指し、
    Showfunc (c *Controller) Show(http.ResponseWriter, *http.Request) (int, interface{}, error)です
  • ServeHTTPfunc (handler) ServeHTTP(http.ResponseWriter, *http.Request) というメソッドを呼んでいます

つまり

Get(pattern string, h http.HandlerFunc)の型にハマるように、ハンドラを作成する。 逆にいうと、カスタムしなければ、members.Get("/{id}", h.Show)とそのまま格納しても問題なく動きます。
逆にカスタムハンドラの方は、members.Get("/{id}", handler(c.Show).ServeHTTP)というように少し書き方が変わっています。重要なのは、ここです。

ServeHTTPが重要

type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    defer func() {
        // panic対応
        if rv := recover(); rv != nil {
            debug.PrintStack()
            log.Printf("panic: %s", rv)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
    }()
    // h(w, r)はhandler型のハンドラの実行 
    // func(http.ResponseWriter, *http.Request) (int, interface{}, error)
    status, res, err := h(w, r)
    if err != nil {
        log.Printf("error: %s", err)
        respondError(w, status, err)
        return
    }
    respondJSON(w, status, res)
    return
}

ServeHTTPhandlerという型から使えるメソッドです。
ServeHTTPfunc ServeHTTP(http.ResponseWriter, *http.Request)という型のメソッドです。 そして、status, res, err := h(w, r)で、c.Show()が実行されています。自分はここで何が起こっているのか、理解に時間がかかりました。 ちなみに、Method(method, pattern string, h http.Handler)というメソッドを使う場合は、Method("GET", "/{id}", handler(c.Show))と書くだけでいい感じに実行されます。何故か?

// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and hangs up the connection.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

net/httpパッケージには、Handlerというinterfaceがあり、これに準拠しているメソッドを定義している場合、Handlerとして使えるという記述がある。しかし、HanderFuncの場合は、そういったインターフェースは用意されていないので、最終的にHandlerFuncの型に合わせる。
そのため、members.Get("/{id}", handler(c.Show).ServeHTTP)と書くに至る。

カスタムハンドラを作ると何が嬉しい?

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 前後自由に書ける
    status, res, err := h(w, r)
    // 前後自由に書ける
}

ざっくり言うと、上みたいな感じでハンドラは前後でやりたいことを書くことが出来ます。案件に合わせた柔軟なハンドラを作れるので、かなり重宝します。 他にも、入口と出口のルールづくりが出来るので、必ずこの値がほしい、必ずこの値を返してほしいというインターフェース的なものが出来るので、忘れて本来やるべきだったものをやっていなかったということを防げます。例えば、以下のようなものです。

// Show endpoint
func (h *Handler) Show(w http.ResponseWriter, r *http.Request) {
    type json struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := json{ID: id, Name: fmt.Sprint("name_", id)}
    response, err := json.MarshalIndent(payload, "", "    ")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }
    // 以下の2行を書き忘れるても、処理は正常に終わってしまうなど
    // w.Header().Set("Content-Type", "application/json")
    // w.WriteHeader(status)
    w.Write([]byte(response))
}

カスタムハンドラは非常に便利なので、必要な入り口と必要な出口を案件に合わせて定義すると良さそうです。この記事を書く上で、記事を読んだり、GitHub上のソースを見てハンドラを参考にしたり、自分なりに考えてヘルパーを作ったりしたのですが、結局使えないというドツボにハマったので、シンプルに最低限必要なもので対応するのが良さそうです。

一応解説しておくと、今回のハンドラは以下のように使います。 func(http.ResponseWriter, *http.Request) (int, interface{}, error)

次回

エラーハンドリングについて書く予定です

Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築

GoっぽくAPIを作る

Goには、様々なWAFが存在しますが、今ひとつデファクトスタンダードなものが未だにありません。その背景としてあるのは、標準パッケージで十分実装出来る。WAFは開発速度を上げてくれますが、そのWAFの開発そのものが製作者のモチベーションに左右されたり、内部実装が分かりにくくなるなど、そんなこと気にするくらいなら、小さいライブラリを組み合わせて、ある程度書いちゃうという人を結構見かけます。

でも、WAFを使わずにGoらしく実装する時ってどうやって書けばいい感じに出来るんだろうというのは、自分の中で結構気になるトピックでした。様々な実装例などから、自分なりにAPIを組んでみたので、シリーズものにして、実装を解説していきたいと思います。

go-chi/chi

今回使うのは、go-chi/chiというnet/httpのインターフェースに準拠したルーティングライブラリを使います。 採用理由としては、高速であること、ルーティングが見やすいなどと言った点です。

Goには、他にも薄いライブラリ系で有名なものがあるので紹介しておきます。

この記事で書くこと

実行

ソースコード

$ go get github.com/pei0804/go-chi-api-example
$ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/01
$ make run
go build -o build . && ./build
2017/11/26 00:05:04 Starting app

$ make curl-members-id id=01
curl -H 'Auth:admin' http://localhost:8080/api/members/01
{
    "id": 1,
    "name": "name_1"
}

コンストラクタ

// Server Server
type Server struct {
    router *chi.Mux
}

// New Server構造体のコンストラクタ
func New() *Server {
    return &Server{
        router: chi.NewRouter(),
    }
}

アプリケーション内で、保持したいものや、handlerに渡したいものなどをServerという構造体に作成して、簡単なコンストラクタを作成しています。これらをの構造体を作成せずに、アプリケーションを作成していくと、後でテストコードを書く際に辛いことになるので、基本的にはこういうものは作成した方がいいです。

ルーティング

// Router ルーティング設定
func (s *Server) Router() {
    h := NewHandler()
    s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア
        api.Route("/members", func(members chi.Router) {  // /api/members/* でグループ化
            members.Get("/{id}", h.Show)  // /api/members/1  などで受け取るハンドラ
            members.Get("/", h.List) // /api/members で受け取るハンドラ
        })
    })
        // Authする何かのエンドポイントという想定
    s.router.Route("/api/auth", func(auth chi.Router) {
        auth.Get("/login", h.Login)
    })
}

ルーティング情報はchiを使うと自動生成することが出来たので、貼っておきます。

/api/*/members/*

/api/*/members/*/{id}

/api/auth/*/login

ルーティングの考えかたやGET POSTなどのメソッドについて分からない場合は、過去にAPI設計についてのワークショップ開催した時の資料あるので、そちらを見てください。

www.slideshare.net

ミドルウェア

ルーティングで登場したミドルウェアについて軽く触れます。 サーバーサイドの開発を何度かしたことがある人なら知っているとは思いますが、認証を通したい、処理を通る前にRequestIDのようなものを発行しておきたいなど、APIをラップする機能の総称です。

http://stackphp.com/img/onion.png 引用元:Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com

よくある具体例として、panicのrecoverが分かりやすいです。

// Recoverer is a middleware that recovers from panics, logs the panic (and a
// backtrace), and returns a HTTP 500 (Internal Server Error) status if
// possible. Recoverer prints a request ID if one is provided.
//
// Alternatively, look at https://github.com/pressly/lg middleware pkgs.
func Recoverer(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            // panicがあればrvrに入っているエラー内容をかき出す
            // 注意点として遅延関数内で実行する必要がある
            if rvr := recover(); rvr != nil { //https://golang.org/pkg/builtin/#recover

                logEntry := GetLogEntry(r)
                if logEntry != nil {
                    logEntry.Panic(rvr, debug.Stack())
                } else {
                    fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr)
                    debug.PrintStack()
                }
            // 500エラーを返す
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
        }()
        //  ハンドラを実行する
        next.ServeHTTP(w, r)
    }
    // 上記の処理をhandlerFuncに渡す
    return http.HandlerFunc(fn)
}

上記のような処理を本来は全てのエンドポイントに書く必要がありますが、それは大変過ぎますし、また同じようなコードを全箇所に書くのはナンセンスです。そういった問題を解決してくれたのがミドルウェアです。 ちなみに、使い方は以下のような感じです。

// Middleware ミドルウェア
func (s *Server) Middleware() {
    s.router.Use(middleware.RequestID)
    s.router.Use(middleware.Logger)
    s.router.Use(middleware.Recoverer)
    s.router.Use(middleware.CloseNotify)
    s.router.Use(middleware.Timeout(time.Second * 60))
}

// ~~~~
s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア
        api.Route("/members", func(members chi.Router) {
            // ~~~
        })
    })
       // ~~~
})

今回作成したものは、データベースのコネクションを渡して、何かデータベースと通信して〜〜ということを想定したようなものを作っています。ミドルウェアに何か引数を渡したい時は、以下のような感じで出来ます。

func Auth(db string) (fn func(http.Handler) http.Handler) { // 引数名を指定してるのでreturnのみでおk
    fn = func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Auth") // Authというヘッダの値を取得する
            if token != "admin" { // adminという文字列か見る
                // エラーレスポンスを返す
                // この関数については後で書きます
                respondError(w, http.StatusUnauthorized, fmt.Errorf("利用権限がありません"))
                return
            }
            // 何も無ければ次のハンドラを実行する
            h.ServeHTTP(w, r)
        })
    }
    return
}

ハンドラ

ここまで何度も登場してきたハンドラというワードですが、GoのAPIではこのハンドラによって様々な処理を可能にしています。分かりやすい記事があったのでリンクを貼っておきます。

// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and hangs up the connection.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ServeHTTPというのが重要で、これに最終的に合わせてキャストすると、http.Handlerが使えます。便利過ぎる! なので、今回は以下のような記述になっています。

http.ListenAndServe(fmt.Sprint(":", *port), s.router)

今回はハンドラをカスタムをしていませんが、ハンドラに渡したい、返してほしいものを変えることが出来ます。以下に分かりやすいサンプルがあったので貼っておきます。 次回の記事では、カスタムハンドラの一例を示す予定です。

type Handler func(w http.ResponseWriter, r *http.Request) error

// 共通の処理などを受け口を作るとロギングにも良い
// WAFのここらへんの設計は結構面白い
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h(w, r); err != nil {
        // handle returned error here.
        w.WriteHeader(503)
        w.Write([]byte("bad"))
    }
}

func main() {
    r := chi.NewRouter()
    r.Method("GET", "/", Handler(customHandler))
    http.ListenAndServe(":3333", r)
}

func customHandler(w http.ResponseWriter, r *http.Request) error {
    q := r.URL.Query().Get("err")

    if q != "" {
        return errors.New(q)
    }

    w.Write([]byte("foo"))
    return nil
}

コマンドラインのフラグからの動作変更

ちょっとした動作変更などは、フラグを使うと便利です。ケースによっては設定ファイルでやるべきこともありますが、簡単なアプリケーションなら、フラグパッケージでサクッとやると便利です。絶対にやるべきでないのは、変える可能生の高いものをハードコーディングしてしまうことです。それをするくらいなら、flagパッケージを使ってください。

var (
    port   = flag.String("port", "8080", "addr to bind") // -port=9000 などにするとポート変更が出来る
    env    = flag.String("env", "develop", "実行環境 (production, staging, develop)") // 実行環境変えたり
    gendoc = flag.Bool("gendoc", true, "ドキュメント自動生成")
)
flag.Parse() <- これを必ず書く

まとめ

もう少し具体例にしたいので、段々アプリケーションっぽいものにしていきます。

次回やること

開発環境をDockerにして良かったこと

f:id:tikasan0804:20171101212036p:plain

Dockerいいぞ

Vagrantすげー便利ーー!!」から、「Docker最高!」になったのですが、個人的にもっと早く知りたかった系の話だったので、記事にしておこうと思います。 ちなみに、筆者は学生です。プロダクトに関わるタイミングとして、個人で作っているものとか、アルバイトや学校の作品などです。

プロジェクトを抱えれば抱えるほどDockerは神

私の場合、色んなところでモノ作ったりしていることもあって、Dockerの恩恵を得ることが出来ています。 逆に言うと、単一プロジェクトしか関わっていない人なら、Vagrantのが本番環境の再現性が高いので、Dockerに頑張って移行しなくても良いと思います。

ぶっ壊して、すぐ作り直すが出来る

壊して作り直す面で、本当に素晴らしいパフォーマンスを発揮します。最初のビルドは構成によっては、時間がかかりますが、二回目からはキャッシュされたイメージから作り直しが行われるので、驚く程、短時間で終わらせることが出来ます。一方で、Vagrantは作り直すことは出来ますが、環境が壊れた時に作り直しに時間を要するので萎えます。(気長に待てば終わりますが)

容量が小さい

まともに環境を用意するよりは容量を小さく抑えることが出来ます。なので、複数のプロジェクトを抱えていたとして、容量で死ぬという問題は起きにくいです。また容量が問題になったとして、一時的に削除すれば良いだけで、必要になった時に、また環境を戻せば良いだけです。

データストア系をクリーンな状態で使える

私は現在の学校に入ってからプログラムを勉強し始めたのですが、当初は「XAMPP」で開発をしていました。そして、色んな開発をしていく中で起きたのが、MySQL周りがカオスになるという問題でした。また、これがチーム制作になるとさらに悲惨を極める状態になります。色々ありすぎたので、以下に箇条書きします。

  • 指定されたユーザーが存在しません
  • 文字コードが違います
  • XAMPP動かない
  • MySQLの謎のエラー
  • XAMPPのバージョン違い
  • 設定などをコードにして残しにくい

Dockerにすると、こういった問題は一切起きません。例えば、以下の要件でデータベースを用意する必要があったとします。

  • mysqlのバージョンは5.7
  • 専用ユーザー user: sample_user password: password
  • 3306ポートでアクセス出来るようにしたい
  • mysqlというフォルダ名でデータを永続化させたい
  • 文字コードはutf8

docker-compose.yml にすると以下のような感じになります。あとは、これをメンバーに配布すれば終了です。最高に楽ちん。

version: '3'
services:
  mysql:
    image: mysql:5.7
    environment:
        - MYSQL_ROOT_PASSWORD=password
        - MYSQL_DATABASE=sample
        - MYSQL_USER=sample_user
        - MYSQL_PASSWORD=password
    ports:
        - 3306:3306
    volumes:
        - "./mysql:/var/lib/mysql"
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci

これと同じ要件を、XAMPPで実現するのは辛いですし。ローカルでMySQLインストールしたとして、一体どんな設定がされているかも分かったもんじゃありませんし、Vagrantで用意するにしては手間がかかり過ぎます。こういった小規模なレベルだと、Dockerはとても手軽で便利です。

設定をコードに残せる

Dockerはコマンドだけで起動することは出来ますが、基本的に Dockerfiledocker-compose.yml などを作成して環境構築する場合がほとんどです。これらをgit管理に含めておくと、環境がコードで把握することが出来るので便利です。 ansibleなどのプロビジョニングツールを使えば、同様のことは出来ますが。。。

デプロイが簡単に出来る

デプロイに必要な環境が出来上がっているので、それをそのまま使ってデプロイ出来るので、とても便利です。
GKEなどを使うとそのままデプロイ出来ます。

クラスタリングが簡単に出来る

私はあまりやったことがないのですが、スケーリングなどが比較的簡単に行えるようです。 qiita.com

Dockerで出来ないこと・イケてないところ

良いことばかりではありません!

Windowsは10以外は快適ではなさそう

qiita.com

  • Docker for Windows Windows 10 のみ対応。今後、対応OSを増やしていく予定とのこと。
  • Windows Machine上のLinux VMでDocker

上記を見た限りだるそう。 自分はMacなので、よく分かりませんが、windows10じゃないと辛そうです。

カーネルの設定を細かくいじれない

Dockerはある程度の制限の中でしか設定出来ないので、こういった案件がある場合は、Vagrant案件ですということになりそうです。

まとめ

  • さくっと作って壊せる
  • MySQLだけがほしいみたいな小規模な案件にもすぐ使える
  • 容量が軽い + 一杯になれば一時的に消せば良い
  • 色んな案件抱える程ありがたい

direnvを使ってGo開発をいい感じにする

GOPATH以下が汚くなってません?

以下のような感じで、GOPATHを設定して開発をしていると・・・

export GOPATH=$HOME/go
export PATH=$PATH::$GOPATH/bin

ghqリポジトリを一括で管理しているので、探すのに困ることはありませんが f:id:tikasan0804:20171009150638p:plain

352個もソースがありました。こういうことをずっとしていると、自分の環境だけで動くみたいなソースになっていることに気づけなかったりします。あと、Goglandのindexing終わらない事案とか・・・w

また、仕様としてimportが以下のなるのですが、リポジトリ名やユーザーが変わると全滅するソースになったりします。一応GOPATHをプロジェクトごとに設定すれば解決出来ますが、それは正直面倒ですよね?それを解決してくれるのが、direnvです

package main

import "github.com/pei0804/direnv-example/sample"

func main() {
    sample.Sample()
}

やりたいこと

  • importを必要最低限だけ記述したい
  • 必要なパッケージだけをGOPATH以下に入れたい
  • GOPATHを動的にいい感じに設定したい
package main

import "app/sample"

func main() {
    sample.Sample()
}
package sample

import "fmt"

func Sample() {
    fmt.Println("Hello World")
}

最終的に上記のようなコードで動作させます。

direnvを使ってGOPATHをいい感じにする

github.com

direnv is an environment switcher for the shell. It knows how to hook into bash, zsh, tcsh and fish shell to load or unload environment variables depending on the current directory. This allows project-specific environment variables without cluttering the ~/.profile file.

つまり、シェルの環境変数いい感じ適用するぜ!

では、READMEにあるように、シェルそれぞれに合った設定をしてください。 自分はzshなので、以下の記述を追加しました。

# ~/.zshrc
eval "$(direnv hook zshrc)"

具体例

とりあえず動かす

$ git clone https://github.com/pei0804/direnv-example.git <--- あえてgit cloneで適当に配置する
$ cd direnv-example
$ make direnv                                                                                                                                                                                                                                                    
direnv allow
direnv: loading .envrc                                                                                                                                                                                                                                                      
direnv: export ~GOPATH
$ make run
go run main.go
Hello World

上記の手順で動いたと思います。 これでどこでも動かせる+必要なパッケージのみのクリーンな環境が出来ました。 ちなみにファイル構成は以下のような感じです。

.
├── LICENSE
├── Makefile
├── README.md
└── go
    └── src
        └── app
            ├── Makefile
            ├── main.go
            └── sample
                └── sample.go

シェルに設定していないGOPATHが正しく動作した理由は、.envrvの記述にあります。 内容は簡単なもので、今いる場所/go で新たなGOPATHを追加しているだけです。

export GOPATH=$(pwd)/go:$GOPATH

この操作で、さっき追加したソースはGoのルールに乗っ取り正しく構成されたソースになります。自分は最近この手法で開発するケースが増えています。特にDockerベースの開発をする時は、この手法がとてもマッチしてていい感じです。(また後日記事にします) ちなみに、最近いい感じになってきているgolang/depは、こういう構成で開発することを想定しているっぽいです。

ソース

github.com