PHPからGoへのリライトで学んだこと

サムネイル

はじめに

DMM グループ Advent Calendar 2024 の 8 日目を担当する、shion0625 です。

プラットフォーム開発本部の MSA グループの認可チームに所属しており、DMM Platform の認可のマイクロサービスの開発・運用を担当しています。現在、私たちのチームでは DMM Platform のリソース管理をする認可サービスを PHP から Go へリライトするプロジェクトを進めています。

この記事では、そのプロジェクトを通じて得られた経験や知見をご紹介します。

リライトの概要

リライトとは、既存のコードを新しい言語に書き換えることを指します。ただし、単に言語を変えるだけではなく、システム全体の性能向上や保守性の改善、開発効率の向上を目指しています。特に、PHP から Go への移行では、Go の並行処理能力や静的型付けによるコードの安全性、コンパイル型言語としてのパフォーマンス向上など、多くの利点を活かすことができます。しかし、言語の違いによる学習コストや既存ライブラリとの互換性といった課題も存在します。

大変だったこと

PHP のコードを理解することが前提

PHP のコードを Go に書き換えるには、まず PHP のコード自体をしっかりと理解する必要があります。その上で、PHP 特有の言語機能や使用しているライブラリ、フレームワークを Go でどのように再現するかを考える必要があります。

今回のリライトでは、FuelPHP のコードを Go に置き換える作業をしました。その中で特に苦労したのが、バリデーション機能の実装です。FuelPHP は高度で柔軟なバリデーションライブラリを提供しており、これと同等の機能を Go で実現するためには、FuelPHP の実装を詳細に理解したうえで、独自のバリデーションロジックを実装する必要がありました。

FuelPHP では簡潔に定義できるバリデーションルールも、Go では手動で詳細なコードを書く必要があり、実装の手間とバグのリスクが増大しました。これにより、開発速度の低下と保守性の確保が課題となりました。

そのため、予期せぬバグの発生を防ぐために、テストの充実も求められました。認可チームでは、E2E テストを活用してリライト後のサービスが旧サービスと同様の動作をすることを確認し、信頼性を確保しています。

Go の言語特性を活かせない

Go の型システムやコンパイル時のエラーチェックを十分に活用せず、PHP の実装方法をそのまま Go に移植してしまうと、本来の Go の利点を活かせません。例えば、Go のフレームワークである Echo には、入力データのバリデーションを簡単に行うメソッドが用意されていますが、PHP での柔軟なバリデーションとは異なる動作をする可能性があります。

PHP では動的型付けを活かして柔軟なデータ処理やエラーハンドリングが可能ですが、Go では静的型付けにより型の整合性が厳密にチェックされます。このため、フレームワークの標準バリデーションをそのまま使用すると、期待通りの動作をしない場合があります。 具体例を挙げると、PHP のコードで下記のように型判定をしている実装があるとします。

<?php
function add($a, $b) {
    if (!is_numeric($a) || !is_numeric($b)) {
        throw new InvalidArgumentException("引数は数値でなければなりません。");
    }
    return $a + $b;
}

Go のコードでは下記のようなコードとして書く必要がありました。

func add(a any, b any) (int, error) {
    aInt, ok := a.(int)
    if !ok {
        return 0, errors.New("引数は数値でなければなりません。")
    }
    bInt, ok := b.(int)
    if !ok {
        return 0, errors.New("引数は数値でなければなりません。")
    }
    return aInt + bInt, nil
}

関数の引数を any 型にしているのは、PHP のコードでは関数の呼び出しの際に型判定をしていないためです。引数を any 型にしておくことで、違う型の引数を受け取った際に同様のエラーを返すことができます。 Go のコードを書くうえでは冗長な書き方ですが、PHP のコードのリライトという観点ではこのような書き方が必要になりました。

良かったこと

フレームワークに頼りきらない実装方法が身につく

リライトプロジェクトを通じて、Go のフレームワークに依存しない実装方法に挑戦する機会がありました。既存フレームワークでは対応しきれない特定の要件に対して、自前で機能を実装する必要がありました。

この経験を通じて、フレームワークに依存しないコーディングの重要性と、その利点を実感しました。フレームワークが提供する抽象化層を介さずにコードを書くことで、Go の標準ライブラリや言語特性をより深く理解できました。また、機能を自作する過程で、柔軟な設計思考や問題解決能力が向上し、より堅牢で拡張性の高いシステムを構築するスキルを身につけることができました。

今回直面したフレームワークの機能を使用できなかった例として、リクエストボディの取得がありました。リクエストボディの型が違う際に特定のエラーメッセージを返す実装をする必要がありました。 Echo というフレームワークを使用した場合の実装が下記のようになります。

type Request struct {
  ID string `json:"id"`
}

var req Request
err := c.Bind(&req); if err != nil {
  return c.JSON(http.StatusBadRequest, map[string]string{"error": "無効なリクエスト"})
}

このように実装すると、ID が文字列でない場合に エラー文として 無効なリクエスト を返すようになります。そのため、型が違う場合に特定のエラー文を返すという実装ができません。

そのため、今回は下記のように実装する必要がありました。そうすることで、ID が文字列でない場合に ID は文字列でなければなりません。 というエラー文を返すことができます。

type Request struct {
  ID any `json:"id"`
}

var req Request
body, err := io.ReadAll(c.Request().Body)
if err != nil {
  return c.JSON(http.StatusBadRequest, map[string]string{"error": "無効なリクエスト"})
}

if err := json.Unmarshal(body, &req); err != nil {
  return c.JSON(http.StatusBadRequest, map[string]string{"error": "無効なリクエスト"})
}

ID, ok := req.ID.(string)
if !ok {
  return c.JSON(http.StatusBadRequest, map[string]string{
    "error": "無効なリクエスト",
    "validation": "ID は文字列でなければなりません。"
  })
}

既存サービスの実装理解の向上

一般的にサービスが大規模になるにつれて、コードベースの複雑化や依存関係の増加により、全体の実装を完全に理解することが難しくなります。このような状況では、新機能の追加やバグ修正が困難になるだけでなく、サービス全体の品質維持にも影響を及ぼします。

特に、認可チームではリライト対象のサービスに対する知見が不足していました。このため、リクエストシーケンスの作成や E2E テストの実装、コードの詳細な読解をしがらリライト作業を進める必要がありました。

実際のエピソードとしては、PHP のコード上からでは該当の変数にnullが入るかどうかを判別することが困難でした。この課題を解決するために、他チームと連携し、実際のデータベースを直接調査することが求められました。その調査の結果、現状は使用されていないデータの可能性が高いがnullが少数ながらも入り得るという事が判明し、リライトの文脈では、Go のコード上でnullの可能性を考慮した実装をする必要があると判断しました。

この調査プロセスを通じて、サービスの内部構造やデータフローに対する理解が深まり、今後のリライト作業における知見の向上につながりました。

命名統一によるコードレビューの効率化と生産性向上

リライト作業は、既存の命名や構造を踏襲することで、新たに変数名や関数名を考える手間を省き、作業効率を向上させることができました。 PHP の実装を Go で再現するという理由で PHP のコードと Go のコードを目視で見比べてロジックが合っているかどうかも確認する方針にしました。そのため、Go のコードにリプレースするときは既存の PHP コードで使用されていた変数名や関数名、引数の順番などを統一しています。

このリライト方法のメリットとして、PHP のコードで使用されていた変数名をコピペして、Go の IDE で検索することで該当箇所を検索することが出来るということが挙げられます。 デメリットとしては、Go のコーディング規則から外れてしまうことです。そのため、Linter に引っかかってしまい、一時的に特定の lint をコメントアウトする必要が発生しました。gocyclo 、 gocognitlll をコメントアウトする必要が発生しました。Go のコーディング規約から外れる、ロジックが PHP と似た感じになるため、技術的な負債になってしまいます。

ただし、これらの負債は認可チームのリライトプロジェクトにおいては、一時的なものとして捉えています。リライト完了後には、Go のリファクタリングを計画しています。

まとめ

この記事では、PHP から Go へのリライトプロジェクトを通じて私が経験した大変だったことと良かったことについてお話ししました。異なる言語へのリライトは確かに多くの課題を伴いますが、学びと成長の機会でもあります。別言語へのリライトを考えている方の参考になれば幸いです。

認可チームでは、DMM Platform のマイクロサービスを支える認可の仕組みを開発しており、現在サーバーサイドエンジニアを募集しています。マイクロサービスの認可の仕組みに興味がある人は、以下のリンクからカジュアル面談または本選考をぜひ申し込んでみてください。

dmm-corp.com