# Web/ネイティブAPIでのGolang導入事例
**[@ysugimoto](https://github.com/ysugimoto) / 2015.12.05 Remixing vol.3**
# Agenda
- Introduction
- About "HAIR"
- Case 1: モバイルトークン管理サーバの開発
- Case 2: 外部向け公開APIサーバの開発
- Case 3: レコメンドエンジン(beta)の開発
- まとめ
# [http://bit.ly/1m2HK8p](http://bit.ly/1m2HK8p)
# Introduction
- 杉本 吉章 / Yoshiaki Sugimoto
- Richmedia inc. engineer
- Architect / Server-Side / Front-end
- PHP / Golang / JavaScript / more...
### [ysugimoto/aws-lambda-image](https://github.com/ysugimoto/aws-lambda-image)
# HAIR [iOS](https://itunes.apple.com/jp/app/hair-torendo-heakatarogu/id836755631?mt=8) / [Android](https://play.google.com/store/apps/details?id=jp.co.rich.android.hair&hl=ja) / [WEB](https://hair.cm)
- ヘアスタイルに特化したヘアカタログアプリ
- エンジニア4名・ディレクター1名の小さなチーム
- Appサーバサイド、WEB全般担当
# Architecture
- **AWS**
- **APP**: PHP (Zend Framework) / Golang
- **WEB**: PHP (WordPress / Symfony / ZF / Laravel / ('A`))
## Case 1:
モバイルトークン管理サーバの開発
# Problem
- モバイルトークン管理のためAWS SNSとコミュニケーションする必要
[>> Amazon SNS のモバイルトークン管理についてのベストプラクティス](http://dev.classmethod.jp/cloud/aws/sns-mobile-token/)
[>> MERYのアプリができるまで](http://www.slideshare.net/kazutoyo/mery-54980360)
- HAIRでの実行タイミングは起動時
- PHPで同期処理してたのでトップ画面が出るまで時間がかかってた
- 非同期にしたけどBGプロセス増えすぎてつらい
# こんな状態が継続する
### **php /path/to/token.php > /dev/null 2>&1 &**
[>> いい加減、>/dev/null 2>&1と書くのをやめたらどうか (追記あり)](http://dqn.sakusakutto.jp/2012/06/shell_dev_null_2_1_crontab.html)
# Improvement
- トークン更新と管理のみを行う小さなデーモンを常駐
- PHPからはソケットにメッセージ投げるだけ(ブロッキングしない)
- 成功/失敗はこのプロセスが管理・ログを取る
```Go
package main
import (
"net"
"os"
)
func main() {
UDSFile := os.Getenv("UNIX_DOMAIN_FILE")
defer os.Remove(UDSFile)
addr := &net.UnixAddr{Name: UDSFile, Net: "unix"}
sock, err := net.ListenUnix("unix", addr)
if err != nil {
panic(err)
}
for {
acceptSocket(sock)
}
}
...以下省略
# Spec
- Unix Domain SocketなプロセスをAppサーバに常駐
- supervisorでデーモン化
- PHPからのメッセージ(JSON)を受けて処理実行
- ゆくゆくは基盤にしたい(Push配信も含めて)
- 使ってる途中でGolangのAWS SDKが公式になった!
[>> AWS SDK for Go](https://aws.amazon.com/jp/sdk-for-go/)
## Case 2:
外部向け公開APIサーバの開発
# Requirement
- 外部から投稿画像をキーワード検索、ページャ付き
- アプリ内は結構複雑な認証機構があって外部向けAPIは作りづらい
- レスポンスは速くするべき
- 新規インスタンスではなく既存のリソース内でやりくりする
# Improvement
- 内製のマイクロフレームワーク
- エンドポイントは少ないので小さな実装でOK
- ElasticSearchで検索
- PHP: 200ms >>> Golang: **30ms**
# Micro Framework
- http.Request/http.ResponseWriter周りを薄くラップ
- クエリビルダ
- レスポンスキャッシュ
- ElasticSearch連携(自作)
- Viewは無し
# Application
- Dispatchするパスと実行する構造体を定義、正規表現も使える
- Dispatch前にValidateが可能
- アクセストークンの検証と問い合わせはFW側で担保
(GET/POST/Custom Header)
- 分散はELBで
```Go
package handler
import (
api "xxxxxxxx/richmedia/hair-api/framework"
)
type SearchHandler struct {
api.AppHandler
}
func init() {
// Register the handler for "GET /search"
api.Register("GET", "/search", SearchHandler{})
}
...
```Go
...
// Implement interface method
func (s SearchHandler) Validate(req *api.Request) (bool, string) {
// validate some inputs
return true, ""
}
// Implement interface method
func (s SearchHandler) Dispatch(res *api.Response, req *api.Request) {
// Dispatch request
}
```
## Case 3:
レコメンドエンジン(beta)の開発
# Spec
- いわゆる「おすすめユーザ」のサジェストエンジン
- 一般ユーザの行動データ x スタイリスト or 投稿画像
- 今後ちゃんと機械学習に切り替えていく
# Improvement
- 現時点ではAndroid版で体験できます
- 精度はイマイチ…(データ少ないのと、まだbeta)
# Etc...
# Tools
- 社内のプロジェクト周りの各種メンテツールなどはほぼGolangで
- ビルドしてバイナリ渡して実行してもらうだけ
…なんだけど今はほぼ自分用
- Hello, Worldのバイナリで2MBはちょっと大きい
# Studies
# 社内勉強会 x 2
- 書ける人がまだ少ないので地道に啓蒙
- プロジェクトのコードは社内から見られるので、質問は随時
# 第一回
# 第二回
# Conclusion
# PHP+Golang なかなか良い
- Viewが得意なPHP、非同期可能で速いGolang
- フロントAPIをPHPで、BGタスク周りはGolangで作ると捗った
- PHPの一部処理も少しずつGolangに載せ替えしていきたい
# Tipsなど
## Golangバイナリを稼働させるときはビルドハッシュを含めておく
- $ [binary] hash
>> 527bcd55aab2e53314f1a15092(commit hash)
- gitのコミットハッシュ確認をすることで、どのrevのビルドかがわかる
- git hookでやると良い
# レスポンス(JSON)用のエンティティは別パッケージに集約する
- JSONのフィールドを網羅するの結構面倒
- 基底構造体を埋め込んで共通化するなど
- 必要ならメソッドを実装して処理できるようにする
```Go
// response_base.go
package entity
type ResponseBase struct {
CreatedAt string `json:"created_at"`
Version string `json:"version"`
}
```
```Go
// image_search.go
pakcage entity
type ImageSearchResponse struct {
ResponseBase
Image ImageData `json:"image"`
}
```
# 環境別の構造体を注入してDEV/STG/PRODをスイッチする
- 環境変数でスイッチする
- PROD > STG > DEVで設定をオーバーライドして使うと漏れがない
- init()で初期化するので即座に構造体にアクセスできる
- 設定値のGetterを作るならInterfaceでもう少しスッキリできる
```Go
// env.go
package env
import "os"
var Env Environment
func init() {
switch os.Getenv("APP_ENV") {
case "development":
ENV = genDevelopmentEnv()
case "staging":
ENV = genStagingEnv()
case "production":
ENV = genProductionEnv()
default:
panic("Unexpected APP_ENV")
}
}
```
```Go
// env/base.go
package env
// Basic settings
type Environment struct {
AppBase string
DBHost string
DBPort int
DBUser string
DBPass string
DBName string
...
}
```
```Go
// env/production.go
package env
func GenProductionEnv() Environment {
return Environment {
AppBase: "https://example.com/",
DBHost: "prod.db.local",
DBPort: 3306,
DBUser "dbuser",
DBPass "dbpass",
DBName "dbname",
}
}
```
```Go
// env/staging.go
package env
func GenStagingEnv() Environment {
e := GenProductionEnv()
// override
e.AppBase = "https://staging.example.com/"
e.DBName = "dbname_stg"
return e
}
```
```Go
// env/development.go
package env
func GenDevelopmentEnv() Environment {
e := GenStagingEnv()
// override
e.AppBase = "http://localhost:8888/"
e.DBHost = "dev.prod.local"
e.DBName = "dbname_dev"
return e
}
```
```Go
// main.go
package main
import (
"xxxxxxxx/richmedia/hair-api/env"
"net/http"
"fmt"
)
func main() {
http.HandleFunc("/", handler)
host := fmt.Sprintf("%s:%d", env.Env.DBHost, env.Env.DBPort)
if err := http.ListenAndServe(host, nil); err != nil {
fmt.Println(err)
}
}
// $ APP_ENV=staging go run main.go
```
# And more...
- [ysugimoto/husky](https://github.com/ysugimoto/husky) - HTTP Framework
- [ysugimoto/gang](https://github.com/ysugimoto/gang) - Command Snipet Tool
- [ysugimoto/gssp](https://github.com/ysugimoto/gssp) - Golang port of PostCSS
# Thanks!
### Any questions?
###### This slide built by [Talkie](https://github.com/ahomu/talkie)
- [https://flic.kr/p/9gXaCU](https://flic.kr/p/9gXaCU)
- [https://flic.kr/p/9vpnUA](https://flic.kr/p/9vpnUA)
- [https://flic.kr/p/qF4y2g](https://flic.kr/p/qF4y2g)
- [https://flic.kr/p/5ZjTUz](https://flic.kr/p/5ZjTUz)
- [https://flic.kr/p/cpia2J](https://flic.kr/p/cpia2J)
- [https://flic.kr/p/qbYimc](https://flic.kr/p/qbYimc)
- [https://flic.kr/p/cBrzzb](https://flic.kr/p/cBrzzb)
- [https://flic.kr/p/9VFaPx](https://flic.kr/p/9VFaPx)