実務から大体一ヶ月経ったので振り返り
振り返りをしてみた
やりっぱなしも良くないなーと思ったので、一ヶ月ごとくらいに振り返りをしていこうと思う。 期間は大体、仕事が始まった4月中旬から、今日までです。
よかったこと
- 質問を遠慮せず、他のクルーにガンガンできた。(弊社では社員のことをクルーと呼んでいます)
- 分からないことを分からないと言うようにした
- 業務中に眠いことがなくなった
- 生活リズムを崩さなかった
- 睡眠に本気出した
- 自分のやっている作業をissueとPRから分かるようにした
- 黙々とやらずに、ガンガン書くようにした
- 口頭で行った意思決定も書く
- AWSのコンソールに慣れた
- 様々な言語が出てくるのに対して、何も感じない(毛嫌いしない)
- アドテク用語のキャッチアップ頑張った
- 何でこうなってるんですか?を解消した
- 違和感をそのままにしない
- 違うチームの人へもコミュニケーションを取りに行った
- 周りを巻き込む意識(いい意味で)
- 予定が無ければ、毎朝毎晩勉強した
- 早寝早起き
- OSSも地味に継続して活動した
気をつけたいこと
- なんでこうしたの?が答えられないことがある
- 知識不足もあるけど、意識次第で回避できそう
- 作業の影響範囲に対しての認識が曖昧
- これをやることによって誰が困るんだろう?を考える
- 自信が無いせいか、違う意見が出てくると不安になる
- なぜやるのか?をもっと掘り下げて、自信を持つ
- 本番へのリリース作業に心労しすぎ
- ほんと、慣れたい。。。
- 複数タスクをこなしたくなる。
- 一つ一つやっていこう
- 焦りから、意思決定を急ぎすぎ
- 結果は地道に出していこう
- 問題が起きた時の、問題の切り分け
- 感情先行しがちなので、客観視しよう
- リスクに対しての意識を高める
- どういうリスクがあるか?
- どこまでのリスクは許容できるのか?
- 実際に問題が起きた時に戻せるか?
- 営業さんが言っていることが、理解しきれていない
- 営業さんにも、話を聞いていこう
- 話がまとまっていない
- 結論から言う
- 簡潔に伝える
- 要点を押さえる
6月もガンガンやってくぞー。
登壇経験の棚卸ししてみた
自分的に一段落着いたと感じたこともあり、一度、経験を棚卸ししてみた 今までの登壇の種類とかをまとめてみると以下のような感じ。
- 座学
- ワークショップ
- ハンズオン
- カンファレンストーク
- ライブコーディング
- LT(これは登壇?なのか?)
それぞれについて、詳しく経験をまとめたいところですが、非常に長くなりそうなのでry
登壇は学びの宝庫
登壇は最大のアウトプットにして、最大のインプット。つまり、学びの宝庫であると僕は思っています。もちろん、良いことばかりではありません。事前に資料とか構成を決めるのは大変ですし、発表に対するアンケート結果とかを見て、胸を痛めることもあります。しかし、そこで得られる経験は挑戦しなければ得られないものなので、後で自分のためになることばかりです。
ちなみに、アンケート結果などで心が痛まないのか?という人向けに先に解答をしておくと、私も人間なので当然痛いです。私なりにやっているアンケート結果の使い方は、すごく極端な評価をしている人の意見に注目するのではなく。(極端に高いとか低い点数のこと)中間あたりの評価をつけている人たちをどうすれば満足と思わせることが出来たのか?ということに注目するようにして、次回の発表に活かすようにしています。極端に悪いところばかりに目が行きがちですが、大半はどっちでもない人達なので、その人達に向けて何が出来るか考える方が建設的です。
他人に説明することで、知識が整理される
俺は理解した!と感じつつも、いざ発表するとなると、アレ?これってどういう風に言えばよかったんだっけ?みたいなことが起きがち。なので、理解したという自信をつけたいトピックをテーマにするのは結構ありだと感じています。
分からないが見える化
人に説明するとなると、大体の人が自分で理解する時以上に、丁寧に調べると思います。そうすると、アレ?これってなんだろうが出て来ることがあります。そうした過程で、知識が穴ぼこになっていたところが綺麗に埋められ、発表する前よりも深く理解が出来た状態になったりします。
発表をすると、フィードバックが得られる
登壇後などの懇親会などで、直接やSNS上で、自分の話した内容に対しての意見やフィードバックが得られることが結構あります。これが一番の学びの瞬間。また、そこから新たな議論が生まれたり、改善点が見えたり、新しく話すべきトークテーマが出てきたりします。発表をした時こそ、色々な人と話すようにすると学びが加速するので、積極的に話すと効果が最大化すると感じている。
それぞれの発表形式ある気をつけるべき点
今後、発表したいなーと感じている人向けにちょっとアドバイスっぽいものを書いてみた。
座学
普通にやると暇になりがちなので、参加者へ質問を投げかけたり、質問をたくさん受けるようにした方が良さそう。
ハンズオン
どんなOSでも動くようにしておくか、参加者へ前提条件を提示しておく。
カンファレンストーク
みんなに満遍なくウケる話を意識しすぎると、つまらなくなりがちなので、俺の思う面白い話!みたいなノリくらいで良かったり。
ライブコーディング
色んな人に理解してもらうために、丁寧にやりすぎる時間が無限に足りないので、ある程度見切りをつけてガンガン進めて、本来やりたかったところまで達成するようにした方がいい。また、ある程度どうやって進めるのかは台本を用意して、がっつり準備するくらいで良いw
LT
勢い命。
まとめ
4月から社会人になったので、これからは質も上げれるように頑張っていきたい。
登壇で今後大事にすること
登壇で伝えたいことは事前に言語化して、聴衆にそれを持ち帰ってもらうことを意識する。
未だによくある失敗なのですが、当日想定していたよりも、寄り道が増えてしまったりして(寄り道というのは前提知識のインプットとかで色々派生してとか) 、本来伝えたかったことがうまくまとまらず終わってしまうことがあるので、気をつけていきたい。
【swaggo】GoのGoDocからSwaggerを書き出そう(基本編)
swaggo
今回紹介するswaggoはyvasiyarov/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投げれば種類を増やせます!)
まとめ
現状、簡単な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.HandlerFunc
はtype HandlerFunc func(ResponseWriter, *Request)
handler(c.Show).ServeHTTP
type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)
という今回作成した型c.Show
ハンドラc.
はtype Controller struct
を指し、
Show
はfunc (c *Controller) Show(http.ResponseWriter, *http.Request) (int, interface{}, error)
ですServeHTTP
はfunc (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 }
ServeHTTP
はhandler
という型から使えるメソッドです。
ServeHTTP
はfunc 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)
int
HTTPステータスコードinterface{}
レスポンスに使うJSONなど(今回はJSONだけを想定)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/*
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/*
- main.Auth.func1
- /members/*
- /
/api/*/members/*/{id}
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/*
- main.Auth.func1
- /members/*
- /{id}
/api/auth/*/login
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/auth/*
- /login
ルーティングの考えかたやGET
POST
などのメソッドについて分からない場合は、過去にAPI設計についてのワークショップ開催した時の資料あるので、そちらを見てください。
www.slideshare.net
ミドルウェア
ルーティングで登場したミドルウェアについて軽く触れます。
サーバーサイドの開発を何度かしたことがある人なら知っているとは思いますが、認証を通したい、処理を通る前にRequestID
のようなものを発行しておきたいなど、APIをラップする機能の総称です。
引用元: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にして良かったこと
Dockerいいぞ
「Vagrantすげー便利ーー!!」から、「Docker最高!」になったのですが、個人的にもっと早く知りたかった系の話だったので、記事にしておこうと思います。 ちなみに、筆者は学生です。プロダクトに関わるタイミングとして、個人で作っているものとか、アルバイトや学校の作品などです。
プロジェクトを抱えれば抱えるほどDockerは神
私の場合、色んなところでモノ作ったりしていることもあって、Dockerの恩恵を得ることが出来ています。 逆に言うと、単一プロジェクトしか関わっていない人なら、Vagrantのが本番環境の再現性が高いので、Dockerに頑張って移行しなくても良いと思います。
ぶっ壊して、すぐ作り直すが出来る
壊して作り直す面で、本当に素晴らしいパフォーマンスを発揮します。最初のビルドは構成によっては、時間がかかりますが、二回目からはキャッシュされたイメージから作り直しが行われるので、驚く程、短時間で終わらせることが出来ます。一方で、Vagrantは作り直すことは出来ますが、環境が壊れた時に作り直しに時間を要するので萎えます。(気長に待てば終わりますが)
容量が小さい
まともに環境を用意するよりは容量を小さく抑えることが出来ます。なので、複数のプロジェクトを抱えていたとして、容量で死ぬという問題は起きにくいです。また容量が問題になったとして、一時的に削除すれば良いだけで、必要になった時に、また環境を戻せば良いだけです。
データストア系をクリーンな状態で使える
私は現在の学校に入ってからプログラムを勉強し始めたのですが、当初は「XAMPP」で開発をしていました。そして、色んな開発をしていく中で起きたのが、MySQL周りがカオスになるという問題でした。また、これがチーム制作になるとさらに悲惨を極める状態になります。色々ありすぎたので、以下に箇条書きします。
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はコマンドだけで起動することは出来ますが、基本的に Dockerfile
や docker-compose.yml
などを作成して環境構築する場合がほとんどです。これらをgit管理に含めておくと、環境がコードで把握することが出来るので便利です。
ansibleなどのプロビジョニングツールを使えば、同様のことは出来ますが。。。
デプロイが簡単に出来る
デプロイに必要な環境が出来上がっているので、それをそのまま使ってデプロイ出来るので、とても便利です。
GKEなどを使うとそのままデプロイ出来ます。
クラスタリングが簡単に出来る
私はあまりやったことがないのですが、スケーリングなどが比較的簡単に行えるようです。 qiita.com
Dockerで出来ないこと・イケてないところ
良いことばかりではありません!
Windowsは10以外は快適ではなさそう
上記を見た限りだるそう。 自分はMacなので、よく分かりませんが、windows10じゃないと辛そうです。
カーネルの設定を細かくいじれない
Dockerはある程度の制限の中でしか設定出来ないので、こういった案件がある場合は、Vagrant案件ですということになりそうです。
まとめ
- さくっと作って壊せる
- MySQLだけがほしいみたいな小規模な案件にもすぐ使える
- 容量が軽い + 一杯になれば一時的に消せば良い
- 色んな案件抱える程ありがたい