Go言語のWebサーバーフレームワークgojiの紹介
こんにちは、開発第2チームTabbyのイマガワです。
Paidの開発を担当しています。
最近社内の卓球大会で優勝しました。
今回は、マイクロサービスをつくる機会があったので、それについて書きたいと思います。
社内にGo言語の本を書いたエンジニアがいまして、本が出版されたばかりだったのでGo言語で実装してみました。
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言語の本の著者もいるので少しずつ取り入れていきたいと思います。