こんにちは、開発第2チームTabbyのイマガワです。
Paidの開発を担当しています。
最近社内の卓球大会で優勝しました。

今回は、マイクロサービスをつくる機会があったので、それについて書きたいと思います。
社内にGo言語の本を書いたエンジニアがいまして、本が出版されたばかりだったのでGo言語で実装してみました。
Go言語の基本的なことは、そちらの本がおすすめです。

starting_go

net/httpパッケージでも簡単なマイクロサービスを作るには十分ですが、せっかくなのでフレームワークを使って実装してみたいと思いgojiを使ってみました。
gojiの説明をする前に、net/httpパッケージの説明をします。

net/httpパッケージの使い方

net/httpパッケージではURLとハンドラの結びつきを簡単にするServeMuxを提供しています。
ServeMuxはhttp.Handlerのコレクションを単一のhttp.Handlerへ集めます。
ServeMuxは独自に定義することができ、特に指定がなければnet/httpパッケージが提供しているDefaultMuxが適用されます。
DefaultMuxはREST対応していないので、HTTPメソッドによって処理を振り分けたい場合は、独自にServeMuxを定義する必要があります。

http.ListenAndServeにnilを指定するとDefaultMuxが適用されます。
ListenAndServeに起動Port、muxを指定して実行すると設定した内容に基づいて起動Portでリッスンした状態になります。
まずは、DefaultMuxを使用した場合のコードをご覧ください。

package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World") } func main() { http.HandleFunc("/hello", handler) http.ListenAndServe(":3218", nil) }

下記のコマンドで確認できます。

$ curl -X GET "http://localhost:3218/hello" Hello, World

独自ServeMuxをつくる場合は、http.Handlerインターフェースを実装する必要があります。
http.HandlerインターフェースにはServeHTTP(w ResponseWriter,r *Request)が定義されています。
HTTPメソッドによって処理を振り分ける処理を実装します。
独自ServeMuxを使用した場合のコードをご覧ください。

package main import ( "fmt" "net/http" ) type MyMux struct { } func (mux MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": switch r.URL.Path { case "/test_get": fmt.Fprintf(w, "HTTP METHOD GET called") return } case "POST": switch r.URL.Path { case "/test_post": fmt.Fprintf(w, "HTTP METHOD POST called") return } case "PUT": switch r.URL.Path { case "/test_put": fmt.Fprintf(w, "HTTP METHOD PUT called") return } case "DELETE": switch r.URL.Path { case "/test_delete": fmt.Fprintf(w, "HTTP METHOD DELETE called") return } } http.NotFound(w, r) } func main() { mux := MyMux{} http.ListenAndServe(":3218", mux) }

下記のコマンドで確認できます。

$ curl -X GET "http://localhost:3218/test_get" HTTP METHOD GET called $ curl -X POST "http://localhost:3218/test_post" HTTP METHOD POST called $ curl -X PUT "http://localhost:3218/test_put" HTTP METHOD PUT called $ curl -X DELETE "http://localhost:3218/test_delete" HTTP METHOD DELETE called

これでnet/httpパッケージの説明を終わりにします。
次はgojiの説明をします。

Install

go get github.com/zenazn/goji

使い方

ルーティングを設定して、処理を実装します。
以下のことができます。
 ・Get、Post等のルーティング
 ・net/httpパッケージとの互換性
 ・正規表現を使用したルーティング

package main import ( "fmt" "github.com/zenazn/goji" "github.com/zenazn/goji/web" "net/http" "regexp" ) func hello(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"]) } func hello_post(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", r.FormValue("foo")) } func regex(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Regex Test") } func main() { //Get,Postによるルーティング goji.Get("/hello/:name", hello) goji.Post("/hello_post", hello_post) //net/httpパッケージとの互換性 goji.Get("/redirect", http.RedirectHandler("/hello/world", 301)) //正規表現ルーティング goji.Get(regexp.MustCompile(`^/regex/[a-zA-Z0-9]+$`), regex) goji.Serve() }

下記のコマンドで確認できます。

$ curl -X GET "http://localhost:8000/hello/goji" Hello, goji! $ curl -X POST "http://localhost:8000/hello_post" -d "foo=goji" Hello, goji! $ curl -LX GET "http://localhost:8000/redirect" Hello, world! $ curl -X GET "http://localhost:8000/regex/hoge" Regex Test

middleware

gojiはリクエストに共通した機能をmiddlewareという仕組みで共通化することができます。
デフォルトで以下の4つの機能が追加されています。
また不要な場合はAbandon(middleware web.MiddlewareType)を使用して削除することができます。

項番 middleware 説明
1 RequestID ユニークなリクエストIDを作成します。
GetReqID(c web.C)で作成したリクエストIDを取得できます。
2 Logger ログを出力します。
リクエストを受け取ったときとリクエストを返したときに以下のようなことを出力します。
リクエストID、パス、HTTPメソッド、HTTPステータスコード等。
デフォルトのログだけだと足りない場合はこのmiddlewareを削除して新規にmiddlewareを作成しましょう。
3 Recoverer 内部エラーをキャッチします。
エラーハンドリングが失敗した場合にHTTPステータスコードを500で返してくれます。
4 AutomaticOptions HTTPメソッドのOPTIONがリクエストされた場合に使用可能なHTTPメソッドの一覧を返します。
デフォルトでは何も返しません。

middlewareの追加

簡単なサーバーにmiddlewareを追加してみます。

package main import ( "fmt" "net/http" "github.com/zenazn/goji" "github.com/zenazn/goji/web" ) func hello(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"]) } func MyMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { fmt.Println("MyMiddleware!!!") h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func main() { goji.Get("/hello/:name", hello) goji.Use(MyMiddleware) goji.Serve() }

下記のコマンドで確認できます。

$ curl -X GET "http://localhost:8000/hello/goji" Hello, goji! # サーバープロセスの標準出力 MyMiddleware!!!

MUXの追加

指定したパス以下にだけ認証処理など独自処理を実装したいことは多いと思います。
そういった場合はデフォルトのMUXだけでなく新しくMUXを追加して処理を実装します。
middlewareパッケージ直下にあるSubRouterを使用します。
SubRouterは対象のパス以下をまとめることができるmiddlewareです。
下記のコードでは「/test」以下に処理をまとめています。

package main import ( "fmt" "net/http" "github.com/zenazn/goji" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" ) func hello(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"]) } func MyMuxMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { fmt.Println("MyMuxMiddleware!!!") h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func MyMuxSubMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { fmt.Println("MyMuxSubMiddleware!!!") h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func main() { myMux := web.New() goji.Handle("/test/*", myMux) goji.Use(MyMuxMiddleware) goji.Get("/hello/:name", hello) myMux.Use(middleware.SubRouter) myMux.Use(MyMuxSubMiddleware) myMux.Get("/mymux/:name", hello) goji.Serve() }

下記のコマンドで確認できます。

$ curl -X GET "http://localhost:8000/test/mymux/goji" Hello, goji! # サーバープロセスの標準出力 MyMuxMiddleware!!! MyMuxSubMiddleware!!! $ curl -X GET "http://localhost:8000/hello/goji" Hello, goji! # サーバープロセスの標準出力 MyMuxMiddleware!!!

シグナルを受け取った時の制御

gojiはデフォルトでSIGINTを受け取りシャットダウンの前後の挙動を制御することができます。

  • シャットダウン前:PreHook
  • シャットダウン後:PostHook

その他のシグナルの追加はAddSignal(sig ...os.Signal)で行うことができます。
下記のようにシグナルを受け取った時の関数を定義します。

import "github.com/zenazn/goji/graceful" ・ ・ ・ graceful.PostHook(func() { fmt.Println("PostHook!!!") }) graceful.PreHook(func() { fmt.Println("PreHook!!!") })

簡単なWebAPIサーバーの作成

上記のgojiの機能を利用してメンバー情報を登録、参照、更新、削除機能をもったWebAPIサーバーを作ってみます。
それぞれHTTPメソッドPOST、GET、PUT、DELETEでアクセスします。
認証はBasic認証で行います。
json形式で結果を返します。

ユーザ名:admin パスワード:admin

事前準備

データベースはMysqlを使用するので、Mysqlのインストールをしてください。
またMysqlのドライバが必要になります。
Mysqlのドライバは以下のコマンドを実行してください。

$ go get github.com/go-sql-driver/mysql

DDL、DMLは以下になります。

CREATE TABLE MEMBERS ( ID INT UNSIGNED NOT NULL AUTO_INCREMENT, NAME VARCHAR(200) NOT NULL, PRIMARY KEY (id) ); INSERT INTO MEMBERS (NAME) VALUES ('raccoon paid'); COMMIT;

データベース接続

Mysqlへの接続の設定を行います。各環境に合わせた設定を行います。

GlobalDb, err := sql.Open("mysql", "root:mysql@/test") if err != nil { panic(err.Error()) }

シグナルを受け取った時の制御

今回はシグナルを受け取った時にデータベース接続を閉じる処理を追加します。

graceful.PostHook(func() { GlobalDb.Close() })

認証用Middlewareの作成

認証はBasic認証で行うのでヘッダを確認してユーザIDとパスワードが設定されているか確認します。
設定されていない場合は、ユーザIDとパスワードを設定するように レスポンスを返します。

func BasicAuthMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { auth(w, r) return } if loginUserId == username && loginpasswd == password { h.ServeHTTP(w, r) } else { auth(w, r) } } return http.HandlerFunc(fn) } func auth(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", `Basic realm="Secret"`) w.WriteHeader(http.StatusUnauthorized) createResponse(r, w, Err, "", FailAuthentication) }

ルーティングの設定

「member/* 」下にアクセスする場合は認証を行うように先ほど作成した認証用Middlewareを設定します。
登録、参照、更新、削除ハンドラを設定します。

admin := web.New() goji.Handle("/member/*", admin) admin.Use(middleware.SubRouter) //Basic認証用Middleware admin.Use(BasicAuthMiddleware) //ルーティングの設定 admin.Get("/:id", GetMember) admin.Delete("/:id", DeleteMember) admin.Post("/", PostMember) admin.Put("/:id", PutMember)

レスポンスの作成

json形式でアクセス結果を返します。
Go言語には構造体をjson形式に変換してくれる標準パッケージ「encoding/json」があるので、そちらを使用します。
Response構造体を定義します。
Body.Messageはエラーメッセージや結果を格納できるようにinterface型で定義してあります。

type Response struct { Header Header `json:"header"` Body Body `json:"body"` } type Header struct { Status string `json:"status"` } type Body struct { Message interface{} `json:"message"` Method string `json:"method"` Path string `json:"path"` MemberId string `json:"memberId"` }

メンバー情報の構造体を定義します。
MemberはBody.Messageに格納します。

type Member struct { Id int `json:"id"` Name string `json:"name"` } type Members struct { Member []Member `json:"members"` }

レスポンスの作成を行います。
レスポンスの具体的な内容は以下のようになっています。

項番 Header/Body 名称 説明
1 header status リクエスト成功の場合はsuccess。
リクエスト失敗の場合はerr
2 body message レスポンスの結果
3 body method リクエストHTTPメソッド
4 body path リクエストパス
5 body memberId リクエストしたmemberId
func createResponse(r *http.Request, w http.ResponseWriter, status string, memberId string, message interface{}) { response := Response{ Header: Header{ Status: status}, Body: Body{ Message: message, Method: r.Method, Path: r.RequestURI, MemberId: memberId, }, } result, _ := json.Marshal(response) io.WriteString(w, string(result)) }

ソースコード

下記に今回作成したソースコードの全文を載せておきます。

  • server.go:WebAPIサーバー

server.go

package main import ( "database/sql" "encoding/json" "io" "net/http" _ "github.com/go-sql-driver/mysql" "github.com/zenazn/goji" "github.com/zenazn/goji/graceful" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" ) type Member struct { Id int `json:"id"` Name string `json:"name"` } type Members struct { Member []Member `json:"members"` } type Header struct { Status string `json:"status"` } type Body struct { Message interface{} `json:"message"` Method string `json:"method"` Path string `json:"path"` MemberId string `json:"memberId"` } type Response struct { Header Header `json:"header"` Body Body `json:"body"` } const ( loginUserId = "admin" loginpasswd = "admin" //Header Status Err = "err" Success = "success" //ErrorMessage FailAuthentication = "認証エラー。正しいユーザID、パスワードを指定してください。" FailSearchMember = "指定したIDをもつメンバーがいません。" FailRegisterMember = "メンバーの登録に失敗しました。" FailUpdateMember = "メンバーの更新に失敗しました。" FailDeleteMember = "メンバーの削除に失敗しました。" ) var ( GlobalDb *sql.DB ) func init() { db, err := sql.Open("mysql", "root:mysql@/test") GlobalDb = db if err != nil { panic(err.Error()) } } func main() { //Signalを受け取った時にコネクションを閉じる graceful.PostHook(func() { GlobalDb.Close() }) admin := web.New() goji.Handle("/member/*", admin) admin.Use(middleware.SubRouter) //Basic認証用Middleware admin.Use(BasicAuthMiddleware) //ルーティングの設定 admin.Get("/:id", GetMember) admin.Delete("/:id", DeleteMember) admin.Post("/", PostMember) admin.Put("/:id", PutMember) goji.Serve() } func PutMember(c web.C, w http.ResponseWriter, r *http.Request) { queryId := c.URLParams["id"] //メンバーの存在チェック members := getMembers(queryId) if len(members) == 0 { createResponse(r, w, Err, queryId, FailSearchMember) return } name := r.PostFormValue("name") tx, _ := GlobalDb.Begin() stmt, _ := tx.Prepare("UPDATE MEMBERS SET NAME = ? WHERE ID = ?") res, _ := stmt.Exec(name, queryId) result, _ := res.RowsAffected() if result == 0 { tx.Rollback() createResponse(r, w, Err, queryId, FailUpdateMember) return } tx.Commit() createResponse(r, w, Success, queryId, "") } func PostMember(c web.C, w http.ResponseWriter, r *http.Request) { name := r.PostFormValue("name") tx, _ := GlobalDb.Begin() stmt, _ := tx.Prepare("INSERT INTO MEMBERS (NAME) VALUES (?)") res, _ := stmt.Exec(name) result, _ := res.RowsAffected() if result == 0 { tx.Rollback() createResponse(r, w, Err, "", FailRegisterMember) return } tx.Commit() lastInsertId, _ := res.LastInsertId() createResponse(r, w, Success, "", lastInsertId) } func DeleteMember(c web.C, w http.ResponseWriter, r *http.Request) { queryId := c.URLParams["id"] //メンバーの存在チェック members := getMembers(queryId) if len(members) == 0 { createResponse(r, w, Err, queryId, FailSearchMember) return } tx, _ := GlobalDb.Begin() stmt, _ := GlobalDb.Prepare("DELETE FROM MEMBERS WHERE ID = ?") res, _ := stmt.Exec(queryId) result, _ := res.RowsAffected() if result == 0 { tx.Rollback() createResponse(r, w, Err, queryId, FailDeleteMember) return } tx.Commit() createResponse(r, w, Success, queryId, "") } func GetMember(c web.C, w http.ResponseWriter, r *http.Request) { queryId := c.URLParams["id"] members := getMembers(queryId) if len(members) == 0 { createResponse(r, w, Err, queryId, FailSearchMember) return } createResponse(r, w, Success, queryId, Members{members}) return } func getMembers(memberId string) []Member { stmt, _ := GlobalDb.Prepare("SELECT * FROM MEMBERS WHERE ID = ?") res, err := stmt.Query(memberId) if err != nil { panic(err.Error()) } var members []Member for res.Next() { var id int var name string member := Member{} err = res.Scan(&id, &name) if err != nil { panic(err.Error()) } member.Id = id member.Name = name members = append(members, member) } return members } func createResponse(r *http.Request, w http.ResponseWriter, status string, memberId string, message interface{}) { response := Response{ Header: Header{ Status: status}, Body: Body{ Message: message, Method: r.Method, Path: r.RequestURI, MemberId: memberId, }, } result, _ := json.Marshal(response) io.WriteString(w, string(result)) } func BasicAuthMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { auth(w, r) return } if loginUserId == username && loginpasswd == password { h.ServeHTTP(w, r) } else { auth(w, r) } } return http.HandlerFunc(fn) } func auth(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", `Basic realm="Secret"`) w.WriteHeader(http.StatusUnauthorized) createResponse(r, w, Err, "", FailAuthentication) }

server.goをビルドして実行し、下記のコマンドを実行すると以下のような結果が得られると思います。

# GET(no auth)
$ curl -X GET "http://localhost:8000/member/1"
{"header":{"status":"err"},"body":{"message":"認証エラー。正しいユーザID、パスワードを指定してください。","method":"GET","path":"/member/1","memberId":""}}

# GET
$ curl -u admin:admin -X GET "http://localhost:8000/member/1"
{"header":{"status":"success"},"body":{"message":{"members":[{"id":1,"name":"raccoon paid"}]},"method":"GET","path":"/member/1","memberId":"1"}}

# POST
$ curl -u admin:admin -X POST "http://localhost:8000/member/" -d "name=corec"
{"header":{"status":"success"},"body":{"message":2,"method":"POST","path":"/member/","memberId":""}}

# PUT
$ curl -u admin:admin -X PUT "http://localhost:8000/member/1" -d "name=superdelivery"
{"header":{"status":"success"},"body":{"message":"","method":"PUT","path":"/member/1","memberId":"1"}}

# DELETE
$ curl -u admin:admin -X DELETE "http://localhost:8000/member/1"
{"header":{"status":"success"},"body":{"message":"","method":"DELETE","path":"/member/1","memberId":"1"}}

gojiには紹介していない機能やmiddlewareがあるのでこれらを使ってみたり、自分で実装してみたりするとオリジナルなWebサーバーをつくることができます。
またgojiだけでなくgorilla、echoといった他のフレームワークもあるので、そちらを使ってみるのも面白いと思います。
弊社ではあまりGo言語を使用した事例が少ないですが、Go言語の本の著者もいるので少しずつ取り入れていきたいと思います。