🐘

PHPエンジニア視点で見るGolangの魅力とPHPとの違い

2024/11/13に公開

昨年の9月、私はPHPエンジニアからGolangエンジニアへと転向しました。

それから1年が経ち、技術的な視点はもちろんのこと、開発プロセスへのアプローチにも変化が生まれました。

この記事では、Golangに触れ、得た経験や気づきを振り返ります。

PHPエンジニアの皆さんが、これからGolangを学びたいと思った際の参考になれば幸いです。

循環インポート

PHPでは、循環依存に対する強制的な制約がないため、柔軟にクラスやファイル間で参照し合うことができます。

ただし、この柔軟さは設計が複雑になった場合に依存関係がスパゲッティ状になるリスクもはらんでいます。

そのため、PHPでも設計の段階で依存関係に気を配ることが求められます。

また、状況によってはPHPでも循環参照に起因するエラーが発生する場合があります。

一方、Golangでは循環インポートが明確に禁止されており、具体的には以下のようなケースがエラーとなります

  • PackageAPackageB を参照
  • PackageBPackageA を参照

これにより、Golangは依存関係を単純化し、設計ミスを未然に防ぐ仕組みを提供しています。

私自身、最初のうちはこの制約に戸惑い、パッケージ構成を何度も見直すことになりました。

しかし、次第に依存の方向性が明確になるこの仕組みの良さを実感しました。

複雑なプロジェクトにおいても、依存の方向性が自然と整理されるため、複雑なプロジェクトでもどのモジュールがどこに依存しているかが把握しやすくなり、保守性が格段に向上しました。

言語のシンプルさ

PHPの開発では、Laravelのような多機能なフレームワークがよく使われます。

Laravelは豊富な機能を持ち、開発を加速させますが、同じ処理を複数の方法で実装できる柔軟さがあります。

その結果、プロジェクトごとに実装スタイルが異なり、「完全に理解した」と思っていても、新しいプロジェクトに参加するたびに、まったく違うやり方に直面するという経験を何度もしました。



一方、Golangはシンプルで統一された設計を重視し、フレームワーク自体も必要最小限の機能に絞られています。

これにより、開発者ごとの実装スタイルの差異が少なく、コードの一貫性が自然と保たれます。

個人的には、このシンプルさが非常に心地よく、レビューや保守が楽になったと感じています。

変数定義

Golangに移行してまず感じたのは、変数名のシンプルさです。

例えば、HTTPリクエストオブジェクトがrreqといった短い名前で表現されることが一般的です。

ただし、「短い変数名が正義」というわけではありません。

狭いスコープ内での使用に限ってこそ有効であり、どんな場合でも省略するのが良いわけではないです。

広いスコープで曖昧な名前を使うと、後から読む人が理解に苦労する原因になります。

Golangでは、このバランスを取ることがエンジニアの判断力として求められます。


また、Golangには略語の表記に関する独自のルールもあります。

PHPではuserIdのようにキャメルケースで記述することが一般的ですが、GolangではuserIDのように単語の途中で現れる略語はすべて大文字で記述します。

これにより、特に技術的なメリットがあるわけではありませんが、Golangのスタイルに合わせるため、この表記に気をつける必要があります。

下記はGoの公式ガイドラインです。

Words in names that are initialisms or acronyms (e.g. “URL” or “NATO”) have a consistent case. For example, “URL” should appear as “URL” or “url” (as in “urlPony”, or “URLPony”), never as “Url”. As an example: ServeHTTP not ServeHttp. For identifiers with multiple initialized “words”, use for example “xmlHTTPRequest” or “XMLHTTPRequest”.

This rule also applies to “ID” when it is short for “identifier” (which is pretty much all cases when it’s not the “id” as in “ego”, “superego”), so write “appID” instead of “appId”.

引用元:https://go.dev/wiki/CodeReviewComments#initialisms

switch case

Golangに限らず、他言語を触る際には構文の違いに悩まされることがよくあります。

その中でも、私はGolangのswitch-case文をPHPの感覚で実装し、間違った挙動に戸惑った経験があるので、ここでその違いをご紹介します。

PHPのswitch-case文では、複数の条件に対して同じ処理を割り当てることができます。

次のように、case 2case 3の両方で同じ処理が実行されるコードがよく使われます。

PHPのswitch文の例

switch ($value) {
    case 1:
        echo "Case1";
        break;
    case 2:
    case 3:
        echo "Case2 もしくは Case3";
        break;
}

PHPではこのように、caseを連続して記述することで、複数の条件で同じ処理が実行されるようになります。

$value2または3の場合、同じ"Case2 もしくは Case3"が出力されます。

一方、Golangで同じように書くと、意図通りには動作しないため、注意が必要です。

Golangのswitch文では、case間で暗黙のbreakが入っているため、PHPと同じような実装を行うと、何も処理がされないケースが出てきます。

Golangのswitch文の誤った例

switch value {
case 1:
    fmt.Println("Case1")
case 2:
case 3:
    fmt.Println("Case2 もしくは Case3")
}

上記のGolangのコードでは、value2の場合、何も出力されずに終了します。

これは、case 2が空のままで次のcase 3に処理が渡らないからです。

PHPとは異なり、case間での処理の継続はデフォルトでは行われないため、複数の条件で同じ処理を行いたい場合は、設計を工夫する必要があります。

Golangのswitch文の正しい例

switch value {
case 1:
    fmt.Println("Case1")
case 2, 3:
    fmt.Println("Case2 もしくは Case3")
}

Golangでは、複数の条件をカンマで区切ることで、PHPのような「caseの連続」に近い動作を実現できます。

ポインタ型

Golangでは、structを関数に渡す際にポインタを使うことが一般的です。

これにより、構造体のコピーを避けてメモリ使用量を抑え、関数内での変更を呼び出し元に反映させることができます。

特に、大きな構造体を頻繁に扱う場合には、ポインタでのデータ共有が合理的です。

一方で、PHPでは参照渡しが一般的にアンチパターンとされています。

参照渡しは、意図しないデータの変更が起こりやすく、コードの予測性を下げるからです。そのため、PHPでは基本的に値渡しを使う方が推奨されています。

私は、この違いに最初は戸惑いました。

しかし、Golangではポインタの使用が自然な設計の一部となっており、大きな構造体や可変なデータに対して効率的な手段であることが理解できるようになりました。

とはいえ、すべてのデータをポインタで渡すことは推奨されません

Golangでも、小さな構造体や変更が必要ない場合には、値渡しを使う方がシンプルで安全です。

ポインタの多用は、どこで何が変更されるかを追いづらくし、意図しない副作用を生むリスクもあります。

また、ポインタを使う場合、nilチェックが増えるため、コードが冗長になることもあります。Golangは「暗黙よりも明示」を重視するため、この冗長さはコードの意図を明確にするためのコストと捉えられています。

ClassとStruct

継承より委譲、とよく言われますが、継承は実装方法によってはアンチパターンに陥ることが多いです。
Golangでは「継承」が存在せず、「委譲」による設計が推奨されます。

  • PHPのClassは、オブジェクトにデータとメソッドをまとめて持たせ、継承を使って別のクラスに機能を引き継ぐことができます。
  • GolangのStructは、データのまとまりを定義するだけの入れ物で、オブジェクト指向の「クラス」とは違います。
    Struct自体はメソッドを持たず、あとからメソッドを紐づける形で使います。

PHPのClass例

class User {
    public $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function greet() {
        echo "Hello, " . $this->name;
    }
}

$user = new User("Takeuchi");
$user->greet(); // Hello, Takeuchi

GolangのStruct例

type User struct {
    Name string
}

func (u User) Greet() {
    fmt.Println("Hello,", u.Name)
}

user := User{Name: "Takeuchi"}
user.Greet() // Hello, Takeuchi

エラーハンドリング

PHPのtry-catch構文では、例外を一括で処理できるため、エラーハンドリングが簡単で、コードが見やすくなります。

特に複数のエラーをまとめてキャッチし、共通の処理をしたい場合に便利です。

PHPのエラーハンドリング例

try {
    $file = fopen("example.txt", "r");
    // ファイル処理
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}

この例では、ファイルを開く際にエラーが発生すると、catchブロックで例外を処理できます。PHPのエラーハンドリングは簡潔で、メインの処理の流れを妨げません。

一方、Golangではエラーハンドリングにerror型を使い、関数呼び出しごとにエラーチェックを行います。

冗長に見えますが、エラーの発生箇所がわかりやすく、デバッグがしやすい点がメリットです。

Golangのエラーハンドリング例

file, err := os.Open("example.txt")
if err != nil {
    log.Fatalf("ファイルを開けません: %v", err)
}
defer file.Close()
// ファイル処理

このコードでは、ファイルを開けなかった場合に即座にエラーメッセージを出力してプログラムを終了します。

Golangではこのように、エラーが発生したらその場で処理し、早めに終了するのが一般的です。

Golangのエラーハンドリングは冗長的であり、PHPに比べて手間がかかるように見えますが、その分エラーの発生箇所が明確になり、特に複雑なプロジェクトで信頼性やデバッグのしやすさに役立ちます。

まとめ

Golangに挑戦してみて、冗長的な書き方が多いものの、その分シンプルでわかりやすい構成が実現されていると感じました。

継承がなく、依存関係が明確に管理できる設計となっているため、コード全体がシンプルに保たれ、エラー処理も分かりやすくなっています。

全体的に、Golangは小規模なアプリケーション開発やシンプルな構成でのスピード感ある開発に適しており、PHPとは異なるアプローチが新鮮で学びが多いと感じました。PHPエンジニア視点で見たGolangの特徴が、これから学び始める方の参考になれば幸いです。

採用情報

e-dashエンジニアチームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?

Discussion