【LT参加レポート】「Goと面と向かう」というテーマで発表してきました!

初めまして。株式会社KAMINASHIでPMをやっている@gtongy1です。
弊社ではサーバサイドの言語としてGoを活用しています。
自分はカミナシに入って約1年ほどになりますが、カミナシは創業して以来サーバーの言語はずっとGoを使って開発しています。
カミナシではちょうど自分が入ったタイミングで新規プロダクトの開発を開始していて、このプロダクト内でもGoが大活躍してます。

改めて成長する事業に寄り添い柔軟に形を変えられる、Goの魅力をひしひしと感じます。
発表の内ではそんな自分がGoとどう向き合い、課題を解決していったのかを話してきました。

スタートアップで自分はGoとどう向き合ってきたか

僕の好きな本の「UNIXという考え方」で語られている

  • スモールイズビューティフル
  • 一つのプログラムには一つのことをやらせる
  • できるだけ早く試作を作成する

この3つが自分の中でGoを利用する理由に合致します。

  • わかりやすく, 保守しやすい状態に保ち、他ツール, システムへ繋げやすい状態にするためにシステムは小さい単位であり続ける必要があるということ
  • 単一であり曖昧さがない状態や多機能にしたい欲求に打ち勝つために、一つのプログラムには一つのことをやらせること
  • 重要なことが正しいかどうかをみるためには試作が必要で、間違っていることに早く気づくことが最大のリスク軽減となる。なのでできるだけ早く試作を作成すること

高速に成長する事業に寄り添うために、この3つの条件を満たす技術を選び続ける必要があると自分は考えています。
スタートアップは、事業に大きな梃子の力を発揮させる箇所を見出し、金脈を掘り出すのが至上命題です。仮説を立て検証していきながら、梃子の支点を何度も高速に探り探り当てていく必要があります。
そのため小さくお互いの機能を協調させ合い、一つのシステムの集合体を作っていくことが自分は大事なのかなと思っています。

Goはこの条件を満たしながら、高速に開発を進められるのがいいところです。

開発に対する拡張性、利便性の高さ

変更にとにかく強く、小さく保てるようにしたい。またそれが拡張しやすい実装を目指すために

  • 継承が存在しない
  • polyrepoなコードベースの管理を取ることが出来る

Goのこの2つの特性を活かして開発を進めていくように意識しています。

Duck Typingによるコードの振る舞いの記述

Composition over inheritance (継承より合成) の考え方がGoにはあります。
継承を使わないことによって、具体ではなく抽象に依存させることでコードの振る舞いを表現し、変更に強くします。
以下は実際のコード内でClean Architectureを適用し、interfaceを利用して抽象にコードの振る舞いを依存させた例です。

package interactor

type User struct {
    UserRepository         repository.UserRepository
}

func NewUser(
    userRepository repository.UserRepository,
) *User {
    return &User{
        UserRepository:         userRepository,
    }
}

func (u *User) GetUser(params *server.GetUserRequestParams) (*server.GetUserResponse, error) {
    // 呼び出し側では抽象に依存。
    // ここで期待されているのは具体的に内部でどんな処理が行われたかではなく、処理によって何が返ってくるかのみ
    // 簡単に付け替え可能
    user, err := u.UserRepository.GetUser(params.UserID)
    // ...
}
package repository

type UserRepository interface {
    // 抽象的な処理をメソッドとして記述。具体的な処理はgatewayに書く
    GetUser(userID int64) (*entity.User, error)
    // ...
}
package gateway

type User struct {
    tx  *gorm.GormTx
}

func NewUser() *User {
    return &User{
        tx:  tx,
    }
}

// 具体的な処理の記述
func (u *User) GetUser(userID int64) (*entity.User, error) {
    user := &orm.User{}
    var entityUser *entity.User
    if err := u.tx.Where("id = ?", userID).Find(u.ctx, user).Error; err != nil {
        return nil, errors.Wrap(err, fmt.Printf("[gateway.GetUser] does not find user: user id=%d", userID))
    }
    entityUser, err := user.ToEntity()
    if err != nil {
        return nil, errors.Wrap(err, "[gateway.GetUser] fail to parse user")
    }
    return entityUser, nil
}

interactorのpackage内で記述してあるように、呼び出し側では抽象に依存する形になっています。
呼び出し側で期待するのは、具体的に内部でどんな処理が行われたかではなく、処理がどう振る舞うかのみです。
そのため、内部の実装がどんな状態なのかを意識することなく、呼び出し側は返ってくる値にのみ意識すれば良い形となり、結合度が低い状態で実装を進めることが出来ます。
コードはDIPに従い、また記述漏れはコンパイル時に弾かれる静的型付けの言語の特性により安全に開発を進めることが出来ます。

polyrepoなコードベースの管理

また、polyrepoなコードベースの管理方法により、容易にprivate repositoryからライブラリとしてコードを切り出し、拡張性を高めることが出来ます。
(弊社の切り出しのきっかけとなったタイミングは、機能を追加していく中で身動きが取りづらくなってきたよねってチーム内で話題に上がった時でした)

githubリポジトリ単位でコードを管理し、重複した時に切り出し保守する範囲を明確に分離することで、コード変更に伴う思考コストを最小限に抑えることが出来ます。

インフラへの柔軟性

ロスコンパイルによって様々なOS上で実行可能なシングルバイナリを作れる点こそがGoの真価だと感じます。

シングルバイナリの起動を行うことによって、容易にコンテナ化を実現し、上記の2つのケースにバイナリで1コマンドで抑えられることは大きなメリットです。

https://i.gyazo.com/398a75b322ccf3da7937b95fd3ff9e87.png

上記はサーバーレスバッチ、APIのデーモン実行でのGoの活用ケースです。
コンテナ内ではマルチステージビルドにより、本番実行はalpine linuxを利用することで、ファイルサイズ自体もぐんと抑えられます。
この辺りはスライドをご覧ください。
ECRに掛かるコスト感も抑えつつ、起動しているコンテナが小さいので起動自体にも時間が掛からないことが利点ですね。

小さくコンテナ作って壊しやすくし、IaC, Deploy Tool等のクラウド周りの仕組みに乗っかりやすくなります。
これによってインフラの柔軟性をあげ、変化に強いインフラを容易に構築出来るようにします。

今後のGoとの向き合い方

新規でプロダクト開発を初めて約1年が経過し、プロダクトもその日数に伴い徐々に巨大なコードとなってきました。
今後もまだまだ課題は山積みな状態で、認証基盤の作成, RBAC/プラットフォーム化戦略等のサーバーサイド内でもさらに要件が複雑に難易度も上がっていきます。
そんな中でも小さくお互いを協調させ合い、システムの集合体をどうやって構築していくのか、生まれてまだまもないサービスをどのような形にしていくのがいいのか。
エンジニアとしても燃え上がるような最高にチャレンジングな課題と毎日触れ合えるような環境です。
そんな弊社で、伝統産業へ技術的にもサービス的にも逆張ってチャレンジしたいメンバーを募集しています。
エントリーお待ちしております!!