Microsoft AzureのVMにMySQLを入れて、GolangでWebAPIサーバを作りました。
その2ではGolangのソースについて説明します。
サーバ環境の構成、構築についてはその1を参照ください。
ベース
echoフレームワークを使うことも考えたのですが、今回はpottavaさんのマイクロサービス用のソースをベースにしました。
全体的に大きなソースではないので、ざっと読んでいて勉強になります。
最終的な構成は以下のようになりました。
実際の業務に乗せているファイルはいくつか省略しています。
(myprojectの上にsrcがあり、そこに$GOPATH通している。)
myproject
├─ main.go
└─ app
├─ config
│ ├─ config.go
│ └─ types.go
├─ controllers
│ └─ user.go
├─ database
│ └─ dbaccess.go
├─ http
│ ├─ http.go
│ └─ restapi.go
├─ logs
│ └─ logger.go
├─ misc
│ └─ functions.go
└─ models
└─ user.go
想像はつくと思いますが、自分で書くのは基本的にcontrollersとmodelsです。
リクエストハンドリング
リクエストのハンドリングは、main.goでcontrollersを読み込み、各contorollerのinit()でハンドリングする感じです。
package main
import (
"fmt"
"net/http"
"myproject/app/config"
_ "myproject/app/controllers" // ここでController読み込み
"myproject/app/logs"
)
func main() {
// configから設定取得。ポート番号はconfigで管理。
cfg := config.NewConfig()
logs.Debug.Print("[config] " + cfg.String())
logs.Info.Printf("[service] listening on port %v", cfg.Port)
logs.Fatal.Print(http.ListenAndServe(":"+fmt.Sprint(cfg.Port), nil))
}
コントローラの実装。
ユーザ取得、更新の処理です。
package controllers
import (
"io"
"net/http"
"net/url"
util "myproject/app/http"
"myproject/app/logs"
"myproject/app/misc"
"myproject/app/models"
)
func init() {
// /users/のリクエストをハンドリング
http.Handle("/users/", util.Chain(util.APIResourceHandler(users{})))
}
type users struct {
util.APIResourceBase
}
// Get
func (c users) Get(url string, queries url.Values, body io.Reader) (util.APIStatus, interface{}) {
// retrive a specified user
if id := url[len("/users/"):]; len(id) != 0 {
// id指定でユーザを1件取得
user, found := models.GetUser(id)
if !found {
return util.Success(http.StatusOK), models.User{}
}
return util.Success(http.StatusOK), user
}
// 全ユーザを取得
users, err := models.GetUsers()
if err != nil {
return util.Fail(http.StatusInternalServerError, err.Error()), nil
}
return util.Success(http.StatusOK), users
}
// Post
func (c users) Post(url string, queries url.Values, body io.Reader) (util.APIStatus, interface{}) {
// routing Post but meaning Put
user := &models.User{}
if err := misc.ReadMBJSON(body, user, 100); err != nil {
logs.Error.Printf("Could not decode response body as a json. Error: %v", err)
return util.Fail(http.StatusInternalServerError, err.Error()), nil
}
// ユーザ情報を更新(なぜPutでないかは後述)
err := user.Update()
if err != nil {
return util.Fail(http.StatusInternalServerError, err.Error()), nil
}
return util.Success(http.StatusOK), user
}
※更新処理にPUTでなくPOSTを使ってるのは、今回の要件にPepperとの連携があったのですが、どうもPepperからはGETとPOSTのリクエストしか飛ばせないらしいというのが理由です。
※また、ユーザ数が決まっていたので、あらかじめ必要なユーザのidだけをDBに登録した状態からスタートだったので、登録処理が不要でした。
※GET, POSTしか使えないクライアントの場合、リクエストヘッダ「X-HTTP-Method-Override」を使って、PUT, DELETEを区別できるようにするのがよいと思います。
データベースアクセス
モデルの実装の前に、MySQLへの接続方法。
まずはMySQLの接続モジュールをGitHubから go get する。
go get github.com/go-sql-driver/mysql
あとは上記リンク先のREADME通りに実装すればOK。
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
const connentInfo string = "user:password@/dbname"
func DbOpen() (*sql.DB, error) {
db, err := sql.Open("mysql", connentInfo)
return db, err
}
モデルの実装
- GetUsers()
- 全ユーザを取得
- GetUser(id)
- idを基にユーザを1件取得
- Update()
- リクエストボディの情報を基にユーザ情報更新
package models
import (
dba "myproject/app/database"
"myproject/app/logs"
)
// User represents user's user
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// Users is a type of User slice
type Users []*User
// GetUsers lists all users from MySQL
// @return users []models.User
func GetUsers() (users *Users, err error) {
return toUsers()
}
// GetUser retrives a specified user from MySQL
// @param id string
// @return user models.User
func GetUser(id string) (user *User, found bool) {
return toUser(id)
}
func toUsers() (*Users, error) {
// MySQL Open
db, err := dba.DbOpen()
if err != nil {
return nil, err
}
defer db.Close()
// Get user information
rows, err := db.Query("SELECT id, IFNULL(name, '') FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
users := Users{}
for rows.Next() {
user := User{}
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
logs.Fatal.Printf("Fatal: %s", err.Error())
return nil, err
}
user.ID = id
user.Name = name
users = append(users, &user)
}
return &users, nil
}
func toUser(id string) (*User, bool) {
// MySQL Open
db, err := dba.DbOpen()
if err != nil {
return nil, false
}
defer db.Close()
// Get user information
rows, err := db.Query("SELECT id, IFNULL(name, '') FROM users where id = ?", id)
if err != nil {
return nil, false
}
defer rows.Close()
user := User{}
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
logs.Fatal.Printf("Fatal: %s", err.Error())
return nil, false
}
user.ID = id
user.Name = name
}
return &user, true
}
// Update update user to MySQL
func (u *User) Update() (error) {
// MySQL Open
db, errDb := dba.DbOpen()
if errDb != nil {
return errDb
}
defer db.Close()
// update user information
_, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", u.Name, u.ID)
if err != nil {
logs.Fatal.Printf("Fatal: %s", err.Error())
return err
}
return nil
}
実行、確認
以下のコマンドで起動します。
go run main.go
とりあえずリクエストしてみるときは、Chromeのブラウザアプリ「Advanced REST Client」が便利です。