はじめに
この記事はGo Advent Calendar 2016 の14日目の記事です。
最近Ergodoxを購入したGopherです。
今年の3月からGoを使用してサービスの開発をしています。
Goを使用したのは初めてで、色々と調べながら開発環境の作り方、開発からデプロイまで一通りやってきたので、まとめて書き出したいと思います。
作ったもの
REST APIを作りました。フロントエンドはriot.jsを使用したSPAで、そのアプリケーションから呼び出されています。
サーバー構成
環境はAWSです。前段にELBを置き、GoのアプリケーションはEC2上で動作します。EC2はAuto Scalingグループに属し、CodeDeployによりアプリケーションがデプロイされます。Auto ScalingとCodeDeployの連携が絶妙で、スケールアウト時にはCodeDeployが最新のアプリケーションをデプロイしてくれます。
CIにはWerkerを使用しています。ゆるいgit flowでやっていて、developとmasterブランチへのpushにフックしてそれぞれdev環境、prod環境へデプロイされます。
開発環境
エディタはIntelliJ
Visual Studio Codeが最近良さげですが、IntelliJを使用して開発しています。(PHP、Scalaなど別の言語書くときもJetBrain製品を使ってきたので)
補完やコードジャンプ、リファクタ機能などが問題なく使えて今のところ不満はありません。
GOPATHは2つ設定
GoのソースコードはワークスペースディレクトリであるGOPATH以下に配置する必要があります。
gofmtなどツールとして使うものやライブラリとして使うものと、開発中のものを別々のディレクトリで扱い、環境変数のGOPATHは2つを参照するようにしています。
[追記]ややこしくなってしまうため、現在はGOPATHは一つだけ設定するようにしています。
設定例
export GOPATH_LIB=$HOME/.go
export GOPATH_DEV=$HOME/go
export GOPATH=$GOPATH_DEV:$GOPATH_LIB
export PATH=$PATH:$GOPATH_LIB/bin
はじめはgbを使用してGOPATHとは関係ないところにコードを置いて開発していましたが、IntelliJを使用したときvendor配下に配置した外部ソースコードを補完で利用できなかったりなど色々不都合があったのでこのようにしました。
(その時はvendor配下とGOPATHの両方にライブラリをインストールしてIntelliJで参照できるようにしていました。。)
gvt
プロジェクトで使用しているライブラリはグローバルにはインストールせず、vendor配下に配置してそのプロジェクト固有で使用するようにしています。
依存性解決にはgvtを使用しています。シンプルで使いやすいというのが採用の理由です。外部ライブラリはコミットには含めず、vendor/manifest
だけをバージョン管理しています。
外部ライブラリをバージョン管理に含めるvendoring
を行うか行わないか論争があると思いますが、この辺はRubyやScala、Javaなど多言語でもそうだと思うので特に違和感を感じていません。
以下使用例です。
# ライブラリ取得
gvt fetch github.com/aws/aws-sdk-go/aws
# ライブラリアップデート
gvt update github.com/aws/aws-sdk-go/aws
# vendor/manifestからライブラリを取得
gvt restore
# ライブラリをすべてアップデート
gvt update -all
[追記]現在はdep を使用しています。今後は Modules(vgo)が標準になっていくと思われるので、そちらを使用するのも良いと思います。
makefile
makefileは改善の余地がある感じがするのですが、現状以下のようにしています。
PATH_API_MAIN = application/api/server/api/main.go
PATH_RESEARCH_API_MAIN = application/research_api/main.go
PATH_PARTNER_ITEM_IMPORT_MAIN = application/partner_item_import/main.go
build:
go build -o bin/api $(PATH_API_MAIN)
go build -o bin/research_api $(PATH_RESEARCH_API_MAIN)
go build -o bin/partner_item_import $(PATH_PARTNER_ITEM_IMPORT_MAIN)
build_linux:
GOOS=linux GOARCH=amd64 go build -o bin/api $(PATH_API_MAIN)
GOOS=linux GOARCH=amd64 go build -o bin/research_api $(PATH_RESEARCH_API_MAIN)
GOOS=linux GOARCH=amd64 go build -o bin/partner_item_import $(PATH_PARTNER_ITEM_IMPORT_MAIN)
restore:
go get -u github.com/FiloSottile/gvt
gvt restore
test:
go test -v ./application/...
go test -v ./domain/...
go test -v ./infrastructure/...
go test -v ./util/...
開発中のビルドはキャッシュが効くためgo install
の方が速度の面でいい選択かもしれません。
コードデザイン
DDDのレイヤードアーキテクチャを使用しています。
.
├── application
│ ├── api
│ ├── partner_item_import
│ └── research_api
├── bin
├── conf
├── deploy_script
├── documents
├── domain
│ ├── lifecycle
│ ├── models
│ └── service
├── infrastructure
│ ├── api
│ ├── lifecycle
│ │ └── factory
│ └── mysql
├── resources
│ ├── assets
│ ├── keys
│ └── templates
├── util
└── vendor
Application層
Webからのリクエストを受け付けたりバッチを起動したりというコードを書き、Domain層のコードを呼び出します。Webフレームワークにはginを使用しているのですが、HTTPサーバーを立ち上げたりRouterを書きURIをきったりController処理を書いたりというのはこの層の責務としています。
コード例(一部だけ抜粋しています)
// application/api
package api
import (
"net/http"
"strconv"
"github.com/brandfolder/gin-gorelic"
"github.com/gin-gonic/gin"
)
func Main() {
router := gin.New()
// Middlewareの登録
router.Use(AccessLogger())
if util.Env.IsLocal() {
router.Use(gin.Logger(), Recovery())
} else {
router.Use(Errbit())
}
router.Use(gorelic.Handler)
router.Use(CORSMiddleware())
// Router
router.GET("/items", getItems)
router.GET("/items/:item_id", getItem)
router.GET("/brands", getBrands)
port := strconv.Itoa(util.Conf.Server.Port)
router.Run(":" + port)
}
func getItem(c *gin.Context) {
itemID := models.ItemID{c.Param("item_id")}
item, err := itemRepository.Resolve(itemID)
if err != nil {
panic(err)
}
if item == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, item)
}
func getBrand(c *gin.Context) {
var err error
brandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "item_brand_id allow only integer"})
return
}
brand, err := brandRepository.Resolve(models.BrandID(brandID))
if err != nil {
panic(err)
}
if brand == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, brand)
}
Domain層
ドメインモデルとそのドメインモデルを取得するリポジトリのインターフェースを実装します。
コードのイメージはこんな感じです。
RepositoryはInterfaceだけを定義し、実装はInfrastructureに書いています。(依存関係逆転)
// domain/lifecycle/interface.go
package lifecycle
type ItemRepository interface {
Resolve(id models.ItemID) (*models.Item, error)
}
type UserRepository interface {
Resolve(id models.UserID) (*models.User, error)
ResolveByFacebookID(facebookID string) (*models.User, error)
ResolveByTwitterID(twitterID string) (*models.User, error)
ResolveByGoogleID(googleID string) (*models.User, error)
ResolveByUUID(uuid string) (*models.User, error)
Store(user models.User) (*models.User, error)
}
type BrandRepository interface {
ResolveAll() ([]*models.Brand, error)
ResolveByTrend(subCategoryID int) ([]*models.Brand, error)
ResolveByGender(gender models.Gender) ([]*models.Brand, error)
ResolveBySubCategoryID(subCategoryID int) ([]*models.Brand, error)
SearchAll(name string, subCategoryID int, genderID int) ([]*models.Brand, error)
}
// domain/models/user.go
package models
type UserID struct {
UserID int64
}
type User struct {
UserID int64 `json:"user_id"`
UUID string `json:"uuid"`
Sp int64 `json:"sp"`
FacebookID string `json:"facebook_id"`
TwitterID string `json:"twitter_id"`
GoogleID string `json:"google_id"`
RegisterDate time.Time `json:"register_date"`
UpdateDate time.Time `json:"update_date"`
registerType int `json:"-"`
}
func (u *User) GetUserID() *UserID {
if u == nil {
return nil
}
return &UserID{u.UserID}
}
func (u *User) AssignUUID() {
u.UUID = generateUUID()
}
func generateUUID() string {
u4, err := uuid.NewV4()
if err != nil {
log.Println("[error] UUID generate.", err)
panic(err)
}
now := strconv.FormatInt(time.Now().UnixNano(), 10)
encoder := sha1.New()
encoder.Write([]byte(u4.String() + now))
return fmt.Sprintf("%x", encoder.Sum(nil))
}
Infrastructure層
Repositoryの実装、MySQLへアクセスするDAO、外部APIへのアクセスなどの実装を書いています。
コードはこんな感じ。
RepositoryのMySQL実装
// infrastructure/lifecycle/user_repository_mysql.go
package lifecycle
import (
"database/sql"
"github.com/gocraft/dbr"
"github.com/pkg/errors"
)
type UserRepositoryMySQL struct{}
func NewUserRepositoryMySQL() UserRepositoryMySQL {
return UserRepositoryMySQL{}
}
func (repo UserRepositoryMySQL) Resolve(id models.UserID) (*models.User, error) {
var user *models.User
record, err := mysql.NewTUserDao().FindByID(id.UserID)
if record != nil {
user = repo.recordToEntity(record)
}
return user, err
}
func (repo UserRepositoryMySQL) recordToEntity(tuser *mysql.TUser) *models.User {
user := models.User{
UserID: tuser.UserID,
UUID: tuser.UUID.String,
Sp: tuser.Sp.Int64,
FacebookID: tuser.FacebookID.String,
TwitterID: tuser.TwitterID.String,
GoogleID: tuser.GoogleID.String,
RegistDate: tuser.RegistDate.Time,
UpdateDate: tuser.UpdateDate.Time,
}
return &user
}
MySQLとやり取りを行うDAO
// infrastructure/mysql/t_user.go
package mysql
import (
"database/sql"
"github.com/gocraft/dbr"
)
type TUser struct {
UserID int64 `db:"user_id"`
UUID dbr.NullString `db:"uuid"`
Sp dbr.NullInt64 `db:"sp"`
FacebookID dbr.NullString `db:"facebook_id"`
TwitterID dbr.NullString `db:"twitter_id"`
GoogleID dbr.NullString `db:"google_id"`
RegistDate dbr.NullTime `db:"regist_date"`
UpdateDate dbr.NullTime `db:"update_date"`
}
type TUserDao struct {
conn *dbr.Connection
table string
}
func NewTUserDao() TUserDao {
return TUserDao{
conn: ConnMaster,
table: "t_user",
}
}
func (t TUserDao) FindByID(userID int64) (*TUser, error) {
sess := t.conn.NewSession(nil)
var record *TUser
_, err := sess.Select("*").From(t.table).Where("user_id = ?", userID).Load(&record)
return record, err
}
使っている主なライブラリ
役割 | リポジトリ |
---|---|
Package Management | https://github.com/FiloSottile/gvt |
Web Framework | https://github.com/gin-gonic/gin |
DataBase/ORM | https://github.com/gocraft/dbr |
Config | https://github.com/BurntSushi/toml |
Logger | https://github.com/sirupsen/logrus |
Logger | https://github.com/doloopwhile/logrusltsv |
JWT | https://github.com/dgrijalva/jwt-go |
OAuth2 | https://golang.org/x/oauth2 |
最近困っていること
- Repositoryをまたいだトランザクション管理。Application層でトランザクション管理したい。
- Clean Architectureで書きたい(DDDの理解を深めたい)
- DomainはInfrastructureに依存しないようにしているので意外とさっくりいける(?!)
最後に
こんな感じでやってます!
アパレル EC 向けサイズレコメンドエンジン「unisize」を運営するメイキップではエンジニアを募集しています!!!