- 概要
- 依存関係の管理
- コードレビュー
- コードのスタイルと形式
- 依存関係
- テスト
- エラー処理
- コマンドライン
- デーモン
- ドッカーファイル
- Goバイナリのディストリビューション
- セキュアチームのスタンダードとスタイルガイドライン
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
を使ってください。goimportsはGofmt を使って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.Equal
、testify/require.EqualError
、testify/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標準ライブラリのソースコードから抜粋したものです。 理にかなっている場合は、これらのガイドラインに従わなくてもかまわないことを覚えておいてください。
テストケースの定義
各テーブルのエントリは、入力と期待される結果を含む完全なテストケースであり、テスト出力を読みやすくするために、テスト名などの追加情報を含むこともあります。
- テストの内部で匿名構造体のスライスを定義します。
- テストの外部で匿名構造体のスライスを定義します。
- コード再利用のための名前付き構造体。
-
map[string]struct{}
を使用します。
テストケースの内容
- 各テストケースには、サブテストの命名に使用する一意な識別子を持つフィールドがあるのが理想的です。 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 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")
。 -
構造化ロギングを使うので、
WithField
やWithFields
を使って、リクエストのURIのような、そのコードパスのコンテキストにあるフィールドをロギングすることができます。 例えば、logrus.WithField("file", "/app/go").Info("Opening dir")
。 複数のキーをロギングしなければならない場合は、WithField
を複数回呼び出す代わりに、常にWithFields
を使ってください。
トレースと相関性
LabKitはGoサービスの共通ライブラリを保管する場所です。 現在、WorkhorseとGitalyの2つのプロジェクトに分かれており、2つの主要な(しかし関連する)機能をエクスポートします:
-
gitlab.com/gitlab-org/labkit/correlation
サービス間の相関 ID の伝播と抽出を行います。 -
gitlab.com/gitlab-org/labkit/tracing
: ディストリビューション・トレース用のGoライブラリのインストルメンテーション用です。
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をアップデートする手順は次のとおりです:
-
CNGプロジェクトでマージリクエストを作成し、
ci_files/variables.yml
のGO_VERSION
を更新します。 -
gitlab-omnibus-builder
プロジェクトでマージリクエストを作成し、docker/
ディレクトリのすべてのファイルを更新して、GO_VERSION
が適切に設定されるようにします。以下はその例です。 - 変更を含む
gitlab-omnibus-builder
の新しいリリースにタグを付けます。 -
omnibus-gitlab
プロジェクト でマージリクエストを作成し、新しく作成されたタグに合わせて を更新します。BUILDER_IMAGE_REVISION
2つのディストリビューション間の不必要な差異を減らすため、OmnibusとCNGは常に同じGoバージョンを使用する必要があります。
複数のGoバージョンをサポート
個々のGolangプロジェクトは、以下の理由から複数のGoバージョンをサポートする必要があります:
- 新しいGoのリリースが出たら、新しいコンパイラとの互換性を検証するために、CIパイプラインへのインテグレーションを始めるべきです。
- Omnibusの公式Goバージョンをサポートしなければなりませんが、最新のマイナーリリースより遅れている可能性があります。
- OmnibusがGoのバージョンを切り替えた場合でも、セキュリティバックポートのために古いバージョンをサポートする必要があるかもしれません。
これら3つの要件は、Goの最新3マイナーバージョンのサポートを維持することで簡単に満たすことができます。
直近の3つのGitLabマイナーリリースへのバックポートをサポートするのに十分であれば、最も古いGoバージョンのサポートをやめて、2つの最新リリースだけをサポートしても構いません。
使用例:
GitLab12.10
でgo 1.11
のサポートを停止する場合、12.9
、12.8
、12.7
で使用している Go のバージョンを確認する必要があります。
クリティカルなセキュリティリリースの場合には、12.7
のバックポートが必要になるため、マイルストーンである12.10
については考慮しません。
- もしOmnibusとCNGの両方がGitLab
12.7
以降、Go1.12
を使っていたのであれば、1.11
のサポートは安全です。 - もし、Omnibus や CNG が GitLab
12.7
の1.11
を使っていたのであれば、セキュリティ修正のバックポートが簡単にできるように、Go1.11
のサポートを維持する必要があります。
セキュアチームのスタンダードとスタイルガイドライン
以下は、セキュアチーム特有のスタイルガイドラインです。
コードのスタイルと形式
コミットする前にgoimports -local gitlab.com/gitlab-org
を使ってください。goimportsはGofmt を使ってGo のソースコードを自動的にフォーマットするツールで、インポート行のフォーマット、欠落した行の追加、参照されていない行の削除も行います。-local gitlab.com/gitlab-org
オプションを使うと、goimports
はローカルで参照されているパッケージを外部のパッケージとは別にグループ化します。詳しくは Go wiki の Code Review Comments ページのインポートセクションを参照してください。ほとんどのエディタ/IDE ではファイルを保存する前/後にコマンドを実行できますが、goimports -local gitlab.com/gitlab-org
を実行するように設定すると、保存時にすべてのファイルに適用されます。
開発者のドキュメントに戻る