はじめに
Hubbleでバックエンドエンジニアをしている @power3812 です。オブジェクト指向大好きマンで、神クラスを作れないかと模索の日々です
今回は普段Rubyをメイン言語としている弊社がGo導入に悪戦苦闘した話をしようと思います!
経緯
弊社ではメインでRuby、サブシステムにJava、Pythonを使用していました。
しかし、社内からエンジニア的な欲求としてGoを使用してみたいという声が多かったのと、エンジニア採用面から使用できる機会を窺っていました。
そんな中、そこまでfatではない新アプリケーションのAPIに使用してみようということになりました。
技術選定
今回の新APIは、ほぼ自分1人で担当することになったので予めエコシステム周りを決定していくことにして、以下の構成で行くことにしました。
- フレームワーク - Echo
調べた感じEchoとGinの2大巨頭っぽい
Ginだとワイルドカードパスを使用した共有のルーティングでコンフリクトが発生するので、これの段階でEcho一択になった
ex) Ginだと以下のルーティングの場合にコンフリクトして使用できない
POST /users/{userId}/like
POST /users/{userId}/unlike
-
ORM - GORM
GoのORMのデファクトスタンダード -
マイグレーション - sql-migrate
必要最低限の機能でyamlで環境ごとにhostなど接続情報を編集できるため -
バリデーション - ozzo-validation
go-playground/validatonがデファクトスタンダードっぽいが、バリデーションのために毎回structを定義しなくてはいけないため、条件指定で簡単にバリデーションできるozzo-validationにした
アーキテクチャに関しては当初MVCで行こうとしましたが、実装段階で出来なくはないが、オブジェクト指向ではないので継承など存在しなく、同じようなコードを沢山書く羽目になりそうな感じがしたため、クリーンアーキテクチャで行くことにしました。
悪戦苦闘
Go導入はどの企業でも悪戦苦闘すると言われますが、例に漏れず弊社も苦労したので、以下に悪戦苦闘したことを書きます。
その1 そもそもオブジェクト指向ではない
Goはオブジェクト指向ではなく、代わりに構造体とインターフェースという概念を持っています。
今までオブジェクト指向でしかWeb開発をしたことがなかったので、いわゆるGoっぽい書き方に慣れる必要がありました。
オブジェクト指向はおおよそ1ファイルに1クラスですが、Goは1ファイルに1構造体ではなくてパッケージとして何個も構造体やインターフェースを定義して良いのかなどそこからの学習になりました。
その2 クリーンアーキテクチャがわからない
概念としてクリーンアーキテクチャは理解出来ますが、それを実際にレイヤー分けやDIなどを実際のコードに落とし込むにはどうしたら良いかが最初はわかりませんでした。
また、ネット上で千差万別の情報があり作っては壊しを繰り返しました。
それと平行してログ機構やDBとの接続などミドルウェア周りの設定などもしていたので、実際のビジネスロジックを組み始めるのは1ヶ月後になりました。
その3 クリーンアーキテクチャとGoの言語仕様の差分を埋めなくてはいけない
これはその2につながることでもありますが、クリーンアーキテクチャの思想をそのままGoに落としこむことはできずある程度、アーキテクチャから逸脱する実装もしなければなりませんでした。
例えば、複数repositoryを跨いだtransactionの場合、依存性の逆転が起きます。
DBの管理はrepository層がしなければならないのに、Model間を跨いだtransactionではservice層がHogeRepositoryからDBを受け取ってもう一方のFugaRepository渡さなければいけません。
これは所謂依存性の逆転で、service層はrepository層に依存しなければならないのに、repository層がservice層に依存しています。
しかし、これは現状のGoやGORMの仕様ではどうやっても解決できないのでこういう実装にすることにしました。
hr.Create()
db := hr.GetDB()
fr.SetDB(db)
fr.Delete(hoge)
type HogeRepository struct {
db *gorm.DB
}
func (hr *HogeRepository) GetDB() *gorm.DB {
return hr.db
}
func (hr *HogeRepository) SetDB(*gorm.DB db) {
hr.db = db
}
func (hr *HogeRepository1) Create(hoge *Hoge) error {
if err := db.Create(&hoge).Error; err != nil {
return err
}
return nil
}
type FugaRepository struct {
db *gorm.DB
}
func (fr *FugaRepository) GetDB() *gorm.DB {
return fr.db
}
func (fr *FugaRepository2) SetDB(*gorm.DB db) {
fr.db = db
}
func (fr *FugaRepository2) Delete(fuga *Fuga) error {
if err := db.Delete(&fuga).Error; err != nil {
return err
}
return nil
}
その4 必要な設定は全て自前で実装しないといけない
Echoはマイクロフレームワークなので、最低限のルーティングでのrequest、response機構しか持っていないためModelの操作やシリアライザーなど自前で実装していかなくてはなりませんでした。
また、ここらへんの実装についてネット検索しても情報が少ない上に人によって実装方針が違い、それぞれの情報を取捨選択しなくてはいけなかったも辛かったです。
良かった所
ここまでだけだとGoのネガティブキャンペーンになってしまうので良かった所についても書きます。
その1 バグがとにかく少ない
静的型付け言語に言えることですが、これが最大の良かった所です。リリース初期の頃はGORMの設定が悪く、変更がうまく保存されないバグがありましたが、それを改修して以降は、殆どバグの問い合わせがありません。(本当に使用されているのか逆に不安になりました)
メインアプリケーションではRubyを使用していて、型がないため不正値が入り込んだり、処理でnilアクセスになったりとすることが多く比較的バグが起こりやすかったです。
その2 並行処理が簡単に実装できる
Goは標準で簡単に並行処理が実装できるgoroutineとsync.WaitGroupがあります。そのおかげでinternalで裏側でメインアプリケーションにAPIをcallする処理の並行処理を実装することができました。これをRubyで実装しようとするとGemを入れたりしなくてならないので標準で機能を搭載してくれているのは助かりました。
その3 実行速度が速い
これも静的型付け言語全般に言えてしまうことですが、Rubyで作成しているメインアプリケーションのおおよそ半分の時間でresponseを返すことが出来ています。(そもそもアプリケーションが違うので単純比較は出来ませんが)
まとめ
弊社のGo言語導入の躓いたポイントや得られたメリットについて書いてみました!
今回の経験を経て、GoはGoの良さ、RubyはRubyの良さを文書レベルではなく、コードレベルで理解することができ適材適所だということを実感しました。
今後はこの経験を活かして、このサービスだとRuby、このサービスだとGoの用に要件やアプリケーションロジックに寄って言語選定をしていけたらなと思いました。
また、ある程度のGoの開発する環境のテンプレートができたので、今後の新サービスでGoで書いてみるなど更にベストプラクティスに近づけていければなと思っています!