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

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

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

このページでは、私たちの様々な経験に基づき、Goのガイドラインを定義し、整理することを目的としています。いくつかのプロジェクトは異なる基準で開始され、まだ仕様がある場合があります。それらはそれぞれのREADME.mdPROCESS.md ファイルに記述されています。

Go言語バージョン

Go アップグレードドキュメントでは、GitLab がどのように Go バイナリサポートを管理し、出荷しているかの概要を説明しています。

GitLabコンポーネントがより新しいバージョンのGoを必要とする場合、顧客、チーム、コンポーネントに悪影響が及ばないよう、アップグレードプロセスに従ってください。

時には、個々のプロジェクトが複数のバージョンのGoでビルドを管理しなければならないこともあります。

依存関係の管理

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

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

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

コードレビュー

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

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

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

セキュリティ

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

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

SAST依存関係スキャンを実行することを忘れないでください。 を実行し(少なくともgosec アナライザ)、](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)セキュリティ要件に](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)従うようにしてください。

ウェブサーバは、Secure.

レビュアー探し

私たちのプロジェクトの多くは小規模で、専任のメンテナーを置くことができません。そのため、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
    # remove `--issues-exit-code 0` or set to non-zero to fail the job if linting issues are detected
    - golangci-lint run --issues-exit-code 0 --print-issued-lines=false --out-format code-climate:gl-code-quality-report.json,line-number
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
    paths:
      - gl-code-quality-report.json

プロジェクトのルートディレクトリに.golangci.yml を含めることで、golangci-lint. golangci-lintGitLab CI/CD ジョブの設定を行うことができます。 この例ではgolangci-lint.GitLab CI/CD ジョブのすべてのオプションを golangci-lintリストアップしています。

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

Go GitLab リンタープラグインはgitlab-org/language-tools/go/linters 名前空間で管理されています。

ヘルプテキストスタイルガイド

Go プロジェクトでユーザー向けのヘルプテキストを作成する場合は、gitaly プロジェクトのヘルプテキストスタイルガイドに記載されているアドバイスに従うことを検討してください。

依存関係

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

モジュール

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

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

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

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

ORM

GitLabではオブジェクトリレーショナルマッピングライブラリ(ORM)は使いません(Ruby on RailsのActiveRecordを除く)。pgx で PostgreSQL データベースとやりとりできれば十分です。

マイグレーション

ホストされたデータベースを管理するという稀なイベントでは、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 の場合)。

エラーを扱うためのリファレンス

CLI

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

ライブラリ

LabKit

LabKitはGoサービスの共通ライブラリを置いておく場所です。LabKit の使用例については、workhorsegitalyを参照してください。LabKit は 3 つの関連する機能をエクスポートします:

これによって、Workhorse、Gitaly、そしておそらく他のGoサーバーでも一貫性のある、基本的な実装を薄く抽象化することができます。たとえば、gitlab.com/gitlab-org/labkit/tracing の場合、Opentracing を直接使用することから、Zipkin または Go キット独自のトレース ラッパーを使用することにアプリケーション コードを変更することなく切り替えることができ、同じ一貫した設定メカニズム (GITLAB_TRACING 環境変数) を維持することができます。

構造化された(JSON) ロギング

すべてのバイナリは、ログの検索とフィルタリングに役立つ構造化された(JSON) ロギングを持つことが理想的です。LabKitはLogrusの抽象化を提供します。私たちは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 を使ってください。

コンテキスト

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

ドッカーファイル

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

  • ユーザーが適切なGoバージョンと依存関係でプロジェクトをビルドできるようにします。
  • Scratch から派生した、自己完結型の小さなイメージを生成します。

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

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

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

コードのスタイルとフォーマット

コミットする前に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 を実行するように設定すると、保存時にすべてのファイルに適用されます。

ブランチの命名

GitLab のブランチ名のルールに加えて、ブランチ名にはa-z,0-9,- のいずれかの文字だけを使うようにしてください。この制限は、ブランチ名にスラッシュ/ などの特定の文字が含まれているとgo get が期待通りに動作しないからです:

$ go get -u gitlab.com/gitlab-org/security-products/analyzers/report/v3@some-user/some-feature

go get: gitlab.com/gitlab-org/security-products/analyzers/report/v3@some-user/some-feature: invalid version: version "some-user/some-feature" invalid: disallowed version string

ブランチ名にスラッシュが含まれる場合、代わりにコミット SHA を参照することになり、柔軟性に欠けます。たとえば

$ go get -u gitlab.com/gitlab-org/security-products/analyzers/report/v3@5c9a4279fa1263755718cf069d54ba8051287954

go: downloading gitlab.com/gitlab-org/security-products/analyzers/report/v3 v3.15.3-0.20221012172609-5c9a4279fa12
...

スライスの初期化

スライスを初期化する場合、余分な割り当てを避けるために可能な限り容量を与えてください。

しないでください:

var s2 []string
for _, val := range s1 {
    s2 = append(s2, val)
}

To-Do:

s2 := make([]string, 0, len(s1))
for _, val := range s1 {
    s2 = append(s2, val)
}

新しいスライスの作成時にmake に容量が渡されなかった場合、append は値を保持できない場合にスライスのバッキング配列を継続的にサイズ変更します。容量を与えることで、割り当てを最小限に抑えることができます。prealloc golanci-lint ルールが自動的にこれをチェックすることを推奨します。

アナライザーテスト

従来の Secureアナライザには、SAST/DAST スキャナレポーターをGitLab セキュリティレポートに変換するconvert 関数 があります。convert 関数のテストを書くときには、testdata アナライザーのリポジトリのルートにあるディレクトリを使って testdata テストフィクスチャを作成します。testdata この testdataディレクトリには、expectreports.この reportsディレクトリには、テストのセットアップ中にconvert 関数に渡されるサンプル SAST/DAST スキャナレポートを格納します。expect ディレクトリには、convert が返す GitLab セキュリティレポートを格納します。例については、Secret Detection を参照してください。

もしスキャナレポートが35行以下の小さなものであれば、testdata ディレクトリを使うのではなく、自由にレポートをインライン化してください。

テスト差分

大きな構造体をテストで比較する際には、go-cmpパッケージを使用する必要があります。このパッケージを使用すると、テストログに出力される両方の構造体全体を見るのではなく、 2 つの構造体が異なっている特定の差分を出力できるようになります。以下に小さな例を示します:

package main

import (
  "reflect"
  "testing"

  "github.com/google/go-cmp/cmp"
)

type Foo struct {
  Desc  Bar
  Point Baz
}

type Bar struct {
  A string
  B string
}

type Baz struct {
  X int
  Y int
}

func TestHelloWorld(t *testing.T) {
  want := Foo{
    Desc:  Bar{A: "a", B: "b"},
    Point: Baz{X: 1, Y: 2},
  }

  got := Foo{
    Desc:  Bar{A: "a", B: "b"},
    Point: Baz{X: 2, Y: 2},
  }

  t.Log("reflect comparison:")
  if !reflect.DeepEqual(got, want) {
    t.Errorf("Wrong result. want:\n%v\nGot:\n%v", want, got)
  }

  t.Log("cmp comparison:")
  if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("Wrong result. (-want +got):\n%s", diff)
  }
}

この出力は、大きな構造体を比較する場合にgo-cmp がはるかに優れている理由を示しています。このような小さな差分で違いを見つけることができても、データが大きくなるとすぐに扱いにくくなります。

  main_test.go:36: reflect comparison:
  main_test.go:38: Wrong result. want:
      {{a b} {1 2}}
      Got:
      {{a b} {2 2}}
  main_test.go:41: cmp comparison:
  main_test.go:43: Wrong result. (-want +got):
        main.Foo{
              Desc: {A: "a", B: "b"},
              Point: main.Baz{
      -               X: 1,
      +               X: 2,
                      Y: 2,
              },
        }

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