Go経験者として次世代EC開発にジョインしてやったことを振り返ってみた

こんにちは、プロダクト開発部コアグループの井上です。
コアグループでは、次世代ECの開発を行っています。
www.makeshop.jp

私は2022年6月に入社してから現在まで、主に新管理画面のバックエンド(Go)を担当しています。チーム内では一番Goの経験期間が長かったこともあり、いろいろなことをやらせていただく機会に恵まれました。
この記事では、私が新管理画面のバックエンド開発において、自分主導で行った改善について振り返ってみようと思います。

1. ログを追えるようにした

Before
logパッケージ配下に置いた*zap.SugaredLoggerを使っていましたが、
リクエスト単位でログを追ったり、どのショップのログなのかがわからない状態でした。

func DoSomething() {
    log.L.Debug("Hello world")
}
{
    "msg": "Hello world",
    "level": "debug",
    "ts": "2023-12-20 14:20:23.394801",
    "caller": "do_something_usecase.go:29",
}

After
gRPCを使用しているので、欲しい情報を詰めたロガーを interceptorでcontext.Contextに入れておくことで、 それ以降はcontext.Contextからロガーをとるだけで済むようにしました。 contextにロガー自体を入れてしまう実装はzerologを参考にしました。 github.com

func UnaryInterceptor(idService id.IdService) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        if info.FullMethod == healthpb.Health_Check_FullMethodName {
            return handler(ctx, req)
        }
        return interceptUnary(ctx, req, info, handler, idService)
    }
}

func interceptUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, idService id.IdService) (any, error) {
    // gRPCの前にいるBFFからリクエストの一意なIDなどをmetadataで引き渡しています
    rd := claim.GetRequestDataByMetadata(ctx)
    ctx = log.WithContext(ctx, "unknown", "unknown", "unknown", rd.RequestId, rd.EndUserRemoteAddr)

    // ...
 
    ctx = log.WithContext(ctx, shopId, host.IdType, userId, rd.RequestId, rd.EndUserRemoteAddr)

    resp, err := handler(ctx, req)
    if err != nil {
        return nil, err
    }

    return resp, nil
}
func DoSomething(ctx context.Context) {
    log.FromContext(ctx).Debug("Hello world")
}
{
    "userId": "xxxx",
    "IP": "0.0.0.0",
    "msg": "Hello world",
    "level": "debug",
    "ts": "2023-12-20 14:20:23.394801",
    "caller": "do_something_usecase.go:29",
    "shopId": "makeshop",
    "container_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx",
    "shopHost": "xxx",
    "traceId": "dd6de545-1f18-492e-8e38-927c22164b1a"
}

traceIdを付与したことで、cloudwatchやAthenaでリクエストごとのログを時系列で閲覧できるようになり便利になりました。
また、別コンテナのログも同じtraceIdで検索できるのでバッチ開発時にかなり助かりました。

2. 必然性のないreflectをやめた

「とにかく遅いので、使わざるを得ない場合以外では使わない」
というのが、自分がそれまでreflectに抱いていた認識でしたが、
かなりカジュアルに使用されていてカルチャーショックを受けたような記憶があります。
2つ例をあげます。

Case1
なんとなくDeepEqual

var a int64
var b int64
...
if reflect.DeepEqual(a, b) {
    return
}

Case2
再構築のプロジェクトである為、基本的に既存のテーブルを使用しています。
SQLアンチパターンに載っているようなテーブルが多々存在するため、 扱いやすい形に変換をしますが、そこをスマートに書きたかったのかよく使われていました。

// DBの値をマッピングする構造体
type Dto struct{
    Id      int64
    Value1  string
    Value2  string
    Value3  string
    Value4  string
    Value5  string
}

func (d *Dto) GetValues() []string {
    val := reflect.ValueOf(d)
    result := make([]string, 0, 5)
    for i := 1; i < val.NumField(); i++ {
        result = append(result, val.Field(i).String())
    }
    return result
}

どちらもレビューと、リファクタリングで潰していきました。 といいつつまだ若干残っているので、隙をみて潰していきたいです。

3. DIにwireを導入した

DIを楽にする為に導入しました。
リリースが近くなってくるとかなり頻繁にコンフリクトが発生していたので、 コマンドですぐ修正できるのが助かりました。

github.com

導入時に思いのほか難しさを感じたのでREADMEを整理しておいたおかげか、 新卒のメンバーもすぐに使いこなしてくれて嬉しかった記憶があります。

4. protoからgraphqlを生成できるようにした

次世代ECの管理画面のバックエンドでは、gRPCとGraphQLを採用しています。
GraphQLのバックエンドはgqlgenで生成しているのですが、開発していく中でいくつかの課題を感じていました。

  1. @goModelディレクティブを使用しているが、フィールド名や型を間違えると、gqlgenが正しく生成してくれず、警告なども表示されないので、フィールド数が多い場合しらみつぶしに確認しなければならず、タイポが原因で30分以上無駄にしてしまうことがあった
  2. protoファイルとgraphqlのコメントが同期できておらず、両方に違うことが書いてある場合も
  3. 必須チェックや最大値などのリクエストのバリデーションがGraphQL側にしかなく、gRPCのAPIはGraphQLを通らない利用を想定できていなかった
  4. 同じような定義を書くのがシンプルに面倒くさい

上記の課題を解決するために、

  • protoc-gen-validateの導入
  • protoファイルをパースしてgraphqlのスキーマに生成するツールをGoで作成

を行いました。
できあがったツールは苦労したかいあって、チームのメンバーにもかなり好評で、
9月末にリリースした注文管理画面の開発効率にもかなり寄与してくれました。

www.magazine.makeshop.jp

生成ツールの詳細などについては、別の記事でいずれ紹介できればと思います。

5. private linterをCIに組み込んだ

次世代ECの管理画面開発ではCIをgithub actionsで走らせており、golangci-lintを使用しています。
追加でいくつかプロジェクト特有のルールを効かせたかったので、
analysis.Analyzerを使用してprivate linterを作成しました。 github actionsでは go vet -vettoolで実行しています。

pkg.go.dev

現在は3種類のlinterがあり、下記のような内容をチェックしています。

  1. logging
    • メソッドの開始、終了ログのフォーマットチェック
    • ログメソッドの間違いをチェック
      • e.g. Debugfを使うべきところでDebugになっていないか?
  2. imports
    • 誤ったパッケージをimportしていないかのチェック
      • e.g. contextを使うべきところgolang.org/x/net/contextになっていないか?
    • 依存関係のルール違反検知
      • e.g. infrastructure層以外のパッケージからDBのdtoに依存してしまっていないか?
  3. variadic
    • 特定のパッケージで可変長引数の渡し忘れがないかのチェック

自分で作っておきながら自分のPullRequestが引っかかると若干のストレスがありますが(笑)、 レビュワーは、本質的でない箇所を気にすることなくレビューに集中でき、 レビュイーは、レビュー待ち時間や修正や再レビューのやり取りを減らせていいことづくめでした。

終わりに

以上が私が新管理画面のバックエンド開発で行ってきた主な改善点です。
振り返ってみて、こういうことをやってみたい、こうしたら便利になるんじゃないかという提案を否定せずにポジティブに受け入れてくれる、積極的に動きやすい環境だなと改めて感じました。
今後も新たな課題に取り組みつつ、より良いプロダクト開発を目指していければと思います。