Help us understand the problem. What is going on with this article?

GoでAPIサーバーの開発からデプロイまで

はじめに

この記事は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」を運営するメイキップではエンジニアを募集しています!!!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away