Goの標準とスタイルガイドライン

この文書では、Go言語を使ったGitLabプロジェクトのさまざまなガイドラインとベストプラクティスについて説明します。

概要

GitLabはRuby on Railsの上に構築されていますが、意味のあるプロジェクトではGoも使っています。 Goは非常に強力な言語で、多くの利点があり、IO(ディスク/ネットワークアクセス)やHTTPリクエスト、並列処理などを多用するプロジェクトに最適です。GitLabにはRuby on RailsとGoの両方があるので、この2つのうちどちらがそのジョブに最適かを慎重に評価する必要があります。

このPagesは、私たちのさまざまな経験に基づいて、私たちの囲碁ガイドラインを定義し、整理することを目的としています。 いくつかのプロジェクトは、異なる基準で開始され、まだ仕様がある可能性があります。 それらは、それぞれのREADME.md またはPROCESS.md ファイルに記載されています。

依存関係の管理

Go は依存関係の管理にソースベースの戦略を採用しています。 依存関係はソースリポジトリからソースとしてダウンロードされます。 これは、依存関係が依存関係のソースリポジトリとは別のパッケージリポジトリからアーティファクトとしてダウンロードされる、より一般的なアーティファクトベースの戦略とは異なります。

Goは1.11以前にはバージョン管理のファーストクラスのサポートを持っていませんでした。 このバージョンでは、Goモジュールとセマンティックバージョン管理の使用が導入されました。 Go 1.12では、クライアントとソースバージョン管理システムの中間的な役割を果たすモジュールプロキシと、依存関係ダウンロードの整合性を検証するために使用できるチェックサムデータベースが導入されました。

詳細はGoの依存関係管理を参照してください。

コードレビュー

Goコードレビューコメントの共通原則に従っています。

レビュアーとメンテナーは、以下の点に注意してください:

  • defer 機能:必要なときに存在を確認し、err チェック後。
  • 依存関係をパラメータとして注入します。
  • JSON へのマーシャリング時に構造体を無効にします ([]の代わりにnull を生成します)。

セキュリティ

セキュリティはGitLabの最優先事項です。 コードレビューの際には、コードにセキュリティ違反の可能性がないか注意しなければなりません:

  • テキスト/テンプレート使用時のXSS
  • ゴリラによるCSRF保護
  • 既知の脆弱性のないGoバージョンを使用してください。
  • シークレットトークンを漏らさないこと
  • SQLインジェクション

プロジェクトのSAST依存関係スキャンを実行することを忘れないでください。を実行し (あるいは少なくともgosec アナライザを実行し)、 セキュリティ要件に従うことを忘れないでください。

ウェブサーバーはSecureのようなミドルウェアを活用することができます。

レビュアー探し

GitLabのプロジェクトの多くは小規模で、専任のメンテナーを置くことができません。 そのため、GitLabではGoのレビュアーを共有しています。 レビュアーを探すには、ハンドブックのエンジニアリングプロジェクトのページにある “GitLab “プロジェクトの“Go “セクションを使います。

このリストに自分を加えるには、team.ymlファイルの自分のプロフィールに以下を追加し、マネージャーにレビューとマージをしてもらいます。

projects:
  gitlab: reviewer go

コードのスタイルと形式

  • パッケージの中であっても、グローバル変数は避けましょう。 そうすることで、パッケージが複数回インクルードされた場合に副作用が発生します。
  • コミットする前にgoimports を使ってください。goimportsGofmt を使ってGo ソースコードを自動的にフォーマットするツールで、インポート行のフォーマット、欠落行の追加、参照されない行の削除も行います。

    ほとんどのエディター/IDEでは、ファイルを保存する前後にコマンドを実行できます。goimports 、保存時にすべてのファイルに適用されるように設定できます。

  • ソースファイルの最初の呼び出し元メソッドの下に非公開メソッドを配置します。

自動リンティング

すべてのGoプロジェクトはこれらのGitLab CI/CDジョブを含めるべきです:

lint:
  image: registry.gitlab.com/gitlab-org/gitlab-build-images:golangci-lint-alpine
  stage: test
  script:
    # Use default .golangci.yml file from the image if one is not present in the project root.
    - '[ -e .golangci.yml ] || cp /golangci/.golangci.yml .'
    # Write the code coverage report to gl-code-quality-report.json
    # and print linting issues to stdout in the format: path/to/file:line description
    - golangci-lint run --out-format code-climate | tee gl-code-quality-report.json | jq -r '.[] | "\(.location.path):\(.location.lines.begin) \(.description)"'
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
    paths:
      - gl-code-quality-report.json
  allow_failure: true

プロジェクトのルート・ディレクトリに.golangci.yml を含めると、golangci-lint.NET のすべてのオプションを golangci-lint設定できます

再帰的インクルードが利用可能になれば、このアナライザーのようにジョブ・テンプレートを共有できるようになります。

依存関係

依存関係は最小限に抑えるべきです。 新しい依存関係の導入は、当社の承認ガイドラインに従って、マージリクエストで主張されるべきです。ライセンス管理も依存関係スキャンも依存関係のスキャン新しい依存関係のセキュリティステータスとライセンスの互換性を保証するために、すべてのプロジェクトでライセンス管理と依存関係のスキャンをアクティビティにしてください。

モジュール

Go 1.11以降、Goモジュールという名前で標準の依存関係システムが利用できるようになりました。 これは、再現可能なビルドのために依存関係を定義し、ロックする方法を提供します。 可能な限り使用する必要があります。

Go モジュールが使用されている場合、vendor/ ディレクトリは存在しないはずです。代わりに、Go はプロジェクトのビルドに必要なときに依存関係を自動的にダウンロードします。 これは Ruby プロジェクトで Bundler を使って依存関係を処理する方法と同じで、マージリクエストをレビューしやすくします。

別のプロジェクトの CI の依存として動作するように Go プロジェクトをビルドする場合など、vendor/ ディレクトリを削除すると、コードを繰り返しダウンロードする必要があり、速度制限やネットワーク障害による断続的な問題が発生する可能性があります。 このような状況では、ダウンロードしたコードをキャッシュする必要があります。

Go < v1.11.4ではモジュールのチェックサムにバグがありました。checksum mismatch エラーを避けるために、少なくともこのバージョンを使用してください。

オーアールエム

GitLabではオブジェクトリレーショナルマッピングライブラリ(ORM)は使いません(Ruby on RailsのActiveRecordを除く)。 プロジェクトをサービスで構成することで、ORMを避けることができます。 PostgreSQLデータベースとやりとりするにはPQで十分でしょう。

移住

まれにホストされたデータベースを管理するイベントでは、ActiveRecordが提供しているようなマイグレーションシステムを使用する必要があります。Journeyのようなシンプルなライブラリは、postgres コンテナで使用するように設計されており、長時間稼働するポッドとしてデプロイすることができます。 新しいバージョンでは新しいポッドがデプロイされ、データが自動的にマイグレーションされます。

テスト

テストフレームワーク

標準ライブラリには、テストを開始するためのすべてがすでに用意されているので、テストのために特定のライブラリやフレームワークを使用するべきではありません。 より洗練されたテストツールが必要な場合は、特定のライブラリやフレームワークを使用することにした場合に備えて、以下の外部依存関係を検討する価値があるかもしれません:

サブテスト

コードの可読性とテスト出力を向上させるために、可能な限りサブテストを使用してください。

テストでの出力向上

テストで期待値と実際の値を比較する場合、testify/require.Equaltestify/require.EqualErrortestify/require.EqualValuesなどを使用すると、構造体、エラー、テキストの大部分、JSON ドキュメントを比較する際の可読性が向上します:

type TestData struct {
    // ...
}

func FuncUnderTest() TestData {
    // ...
}

func Test(t *testing.T) {
    t.Run("FuncUnderTest", func(t *testing.T) {
        want := TestData{}
        got := FuncUnderTest()

        require.Equal(t, want, got) // note that expected value comes first, then comes the actual one ("diff" semantics)
    })
}

テーブル駆動テスト

テーブル駆動テストを使うのは、同じ関数の入出力が複数ある場合に有効です。 以下に、テーブル駆動テストを書く際のガイドラインを示します。 これらのガイドラインは、主にGo標準ライブラリのソースコードから抜粋したものです。 理にかなっている場合は、これらのガイドラインに従わなくてもかまわないことを覚えておいてください。

テストケースの定義

各テーブルのエントリは、入力と期待される結果を含む完全なテストケースであり、テスト出力を読みやすくするために、テスト名などの追加情報を含むこともあります。

テストケースの内容

  • 各テストケースには、サブテストの命名に使用する一意な識別子を持つフィールドがあるのが理想的です。 Go 標準ライブラリでは、これは一般的にname string フィールドです。
  • want/expect/actual は、テストケースの中でアサーションに使用するものを指定するときに使用します。

変数名

  • 構造体の各テーブル駆動テスト マップ/スライスには、testsという名前を付けることができます。
  • tests をループする場合、匿名構造体はtt またはtcと呼ぶことができます。
  • テストの説明は、name/testName/tnのように参照できます。

ベンチマーク

多くのIOや複雑なオペレーションを扱うプログラムでは、常にベンチマークを実施し、長期間にわたるパフォーマンスの一貫性を確保する必要があります。

エラー処理

コンテキストの追加

単にエラーを返すのではなく、エラーを返す前にコンテキストを追加すると便利です。 これにより開発者は、エラー状態になったときにプログラムが何をしようとしていたのかを理解することができ、デバッグがより簡単になります。

使用例:

// Wrap the error
return nil, fmt.Errorf("get cache %s: %w", f.Name, err)

// Just add context
return nil, fmt.Errorf("saving cache %s: %v", f.Name, err)

コンテキストを追加する際に注意すべき点がいくつかあります:

  • 内部エラーを呼び出し側に公開するかどうかを決定します。公開する場合は%wを使用し、公開しない場合は%vを使用します。
  • failed,error,didn'tのような言葉は使わないでください。エラーなので、ユーザーは何かが失敗したことをすでに知っており、failed xx failed xx failed xxのような文字列を持つことになるかもしれません。 代わりに_何が_失敗したのかを説明してください。
  • エラー文字列は、大文字であってはなりませんし、句読点や改行で終わってはなりません。golint を使ってこれをチェックすることができます。

ネーミング

  • センチネルエラーを使用する場合は、常にErrXxxのような名前を付ける必要があります。
  • 新しいエラータイプを作成する場合は、常にXxxErrorのような名前を付ける必要があります。

エラーの種類のチェック

  • エラーの等質性をチェックするには、==を使用しないでください。 代わりにerrors.Is を使用してください(Go バージョン >= 1.13 の場合)。
  • エラーが特定の型であるかどうかを調べるには、型アサーションを使わず、errors.As を使ってください (Go バージョン >= 1.13 の場合)。

エラーに関する参考文献

コマンドライン

すべてのGoプログラムはコマンドラインから起動されます。cliはコマンドラインアプリを作成するのに便利なパッケージです。 プロジェクトがデーモンであっても、単純なcliツールであっても使用する必要があります。 フラグは環境変数に直接マッピングすることができ、プログラムとのコマンドラインでのやり取りを文書化し、同時に一元化します。os.GetEnvは使用しないでください。コードの奥深くに変数を隠します。

デーモン

ロギング

デーモンには、ロギングライブラリの使用を強く推奨します。 標準ライブラリにlog パッケージがあるとしても、一般的にはLogrusを使用します。 そのプラグイン(”フック”)システムにより、ロガーレベルでノーティファイアとフォーマッタを直接追加できる、強力なロギングライブラリになります。

構造化(JSON) ロギング

ログの検索やフィルタリングに役立つので、すべてのバイナリは理想的には構造化された(JSON) ロギングが必要です。 GitLab では JSON 形式の構造化されたロギングを使っています。Logrusを使うときは、ビルドのJSON フォーマッターを使うだけで構造化されたロギングを有効にできます。 これは、Rubyアプリケーションで使っているのと同じロギングタイプに従います。

Logrusの使い方

Logrusパッケージを使用する際には、いくつかのガイドラインに従う必要があります:

  • エラーを印刷する場合はWithErrorを使用します。例えば、logrus.WithError(err).Error("Failed to do something")
  • 構造化ロギングを使うので、WithFieldWithFieldsを使って、リクエストのURIのような、そのコードパスのコンテキストにあるフィールドをロギングすることができます。 例えば、logrus.WithField("file", "/app/go").Info("Opening dir")。 複数のキーをロギングしなければならない場合は、WithField を複数回呼び出す代わりに、常にWithFields を使ってください。

トレースと相関性

LabKitはGoサービスの共通ライブラリを保管する場所です。 現在、WorkhorseとGitalyの2つのプロジェクトに分かれており、2つの主要な(しかし関連する)機能をエクスポートします:

GITLAB_TRACING これにより、Workhorse、Gitaly、そして将来的には他のGoサーバでも一貫性のある、基本的な実装に対する薄い抽象化が可能になります。 例えば、gitlab.com/gitlab-org/labkit/tracing の場合、アプリケーションコードを変更することなく、Opentracingを直接使用することから、ZipkinやGokit独自のトレースラッパーを使用することに切り替えることができます。

コンテクスト

デーモンは長時間実行されるアプリケーションなので、キャンセルを管理し、不必要なリソース消費(DDOS脆弱性につながる可能性がある)を避けるメカニズムが必要です。Go Contextは、ブロックできる関数で使用し、最初のパラメータとして渡す必要があります。

ドッカーファイル

すべてのプロジェクトは、プロジェクトをビルドして実行するために、リポジトリのルートにDockerfile を持つべきです。 Go プログラムは静的なバイナリなので、外部依存を必要とすべきではなく、最終イメージの shell は役に立ちません。 私たちは多段階ビルドを推奨しています:

  • これにより、ユーザーは正しいGoバージョンと依存関係でプロジェクトをビルドできます。
  • これらは、Scratchに由来する、自己完結した小さな画像を生成します。

生成されたDockerイメージには、ポータブルなコマンドを作成するためのプログラムをEntrypoint 。そうすれば、誰でもイメージを実行することができ、パラメータがなくてもヘルプメッセージが表示されます(cli が使用されている場合)。

Goバイナリのディストリビューション

独自のバイナリを公開しているGitLabRunnerを除き、私たちのGoバイナリはディストリビューショングループが管理するプロジェクトによって作成されています。

Omnibus GitLabプロジェクトは、すべてのバイナリを含む単一のモノリシックなオペレーティングシステムパッケージを作成します。一方、Cloud-Native GitLab(CNG)プロジェクトは、DockerイメージとHelmチャートのセットを公開し、それらを接着します。

どちらのアプローチでも、すべてのプロジェクトで同じバージョンの Go を使用するため、Go を使用するすべてのプロジェクトのテストマトリクスで、少なくとも 1 つの Go バージョンが共通であることを確認することが重要です。Omnibusで現在使用されている Go のバージョンと、CNGで使用されているバージョンを確認できます。

囲碁バージョンの更新

Goのサポートされているバージョン、つまり、最新の3つのマイナーリリースのいずれかを常に使用すべきです。また、セキュリティ修正が含まれている可能性があるため、そのバージョンの最新のパッチレベルを常に使用すべきです。

バージョンの変更はコンパイルされるすべてのプロジェクトに影響するため、パッケージビルダーを変更して新しいバージョンを使用する前に、すべてのプロジェクトが新しいGoバージョンに対してテストされるように更新されていることを確認することが重要です。Goの互換性が約束されているにもかかわらず、マイナーバージョン間の変更によってバグが露呈したり、プロジェクトに問題が発生したりする可能性があります。

使用する新しいGoバージョンを選択したら、OmnibusとCNGをアップデートする手順は次のとおりです:

2つのディストリビューション間の不必要な差異を減らすため、OmnibusとCNGは常に同じGoバージョンを使用する必要があります。

複数のGoバージョンをサポート

個々のGolangプロジェクトは、以下の理由から複数のGoバージョンをサポートする必要があります:

  1. 新しいGoのリリースが出たら、新しいコンパイラとの互換性を検証するために、CIパイプラインへのインテグレーションを始めるべきです。
  2. Omnibusの公式Goバージョンをサポートしなければなりませんが、最新のマイナーリリースより遅れている可能性があります。
  3. OmnibusがGoのバージョンを切り替えた場合でも、セキュリティバックポートのために古いバージョンをサポートする必要があるかもしれません。

これら3つの要件は、Goの最新3マイナーバージョンのサポートを維持することで簡単に満たすことができます。

直近の3つのGitLabマイナーリリースへのバックポートをサポートするのに十分であれば、最も古いGoバージョンのサポートをやめて、2つの最新リリースだけをサポートしても構いません。

使用例:

GitLab12.10go 1.11 のサポートを停止する場合、12.912.812.7で使用している Go のバージョンを確認する必要があります。

クリティカルなセキュリティリリースの場合には、12.7 のバックポートが必要になるため、マイルストーンである12.10については考慮しません。

  1. もしOmnibusとCNGの両方がGitLab12.7以降、Go1.12 を使っていたのであれば、1.11のサポートは安全です。
  2. もし、Omnibus や CNG が GitLab12.71.11 を使っていたのであれば、セキュリティ修正のバックポートが簡単にできるように、Go1.11 のサポートを維持する必要があります。

セキュアチームのスタンダードとスタイルガイドライン

以下は、セキュアチーム特有のスタイルガイドラインです。

コードのスタイルと形式

コミットする前にgoimports -local gitlab.com/gitlab-org を使ってください。goimportsGofmt を使ってGo のソースコードを自動的にフォーマットするツールで、インポート行のフォーマット、欠落した行の追加、参照されていない行の削除も行います。-local gitlab.com/gitlab-org オプションを使うと、goimports はローカルで参照されているパッケージを外部のパッケージとは別にグループ化します。詳しくは Go wiki の Code Review Comments ページのインポートセクションを参照してください。ほとんどのエディタ/IDE ではファイルを保存する前/後にコマンドを実行できますが、goimports -local gitlab.com/gitlab-org を実行するように設定すると、保存時にすべてのファイルに適用されます。


開発者のドキュメントに戻る