banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Go言語初上手(三)コーディング規約とパフォーマンス最適化 | 青訓営

本節では、より簡潔で明確なコードを書く方法について説明しました。各言語には独自の特性と独自のコーディング規範があり、Go においては、どのようなパフォーマンス最適化手段や便利なツールがあるかも紹介しました。

高品質なコードは、正確で信頼性があり、簡潔で明確な特性を持つ必要があります。

  • 正確性:さまざまな境界条件が考慮されているか、誤った呼び出しが処理できるか
  • 信頼性:異常事態やエラー処理が明確で、依存するサービスの異常が迅速に処理できるか
  • 簡潔さ:ロジックが単純で、後続の機能追加が迅速にサポートできるか
  • 明確さ:他の人がコードを読み理解する際に明確で、リファクタリング時に予期しない事態を心配しないか
    これにはコーディング規範が必要です。

コーディング規範#

フォーマットツール#

コーディング規範を語る上で、コードフォーマットツールについて触れざるを得ません。Go 公式が提供するフォーマットツール gofmt の使用を推奨します。Goland にはその機能が内蔵されており、一般的な IDE でも簡単に設定できます。

  • もう一つのツールは goimports で、これは gofmt に依存パッケージの管理機能を加えたもので、自動的に依存パッケージを追加または削除します。

image.png

js にも似たようなフォーマットツール Prettier があり、ESLint と組み合わせてコードフォーマットを行うことができます。

コメント規範#

良いコメントは以下を必要とします。

  • コードの役割を説明する

  • 複雑で明確でないロジックを説明する

  • コード実装の理由を説明する(これらの要因は文脈から外れると理解が難しい)

  • コードがどのような状況でエラーになるかを説明する(制限条件を説明する)

  • 公共シンボルのコメントを説明する(パッケージ内で宣言された各公共シンボル:変数、定数、関数、構造体など)

    • 例外:インターフェースの実装メソッドにはコメントを付ける必要はありません

Google スタイルガイドには 2 つのルールがあります:

  • 明白でもなく短くもない 公共機能 はコメントを付ける必要があります。
  • 長さや複雑さに関わらず、ライブラリ内の任意の関数にはコメントを付ける必要があります。

避けるべき状況は以下の通りです:

  • 可視名知義の関数に冗長なコメントを付ける
  • 明らかなプロセスを直接翻訳する

要するに、コードが最良のコメントです

  • コメントは コードが表現していない文脈情報を提供するべきです。
  • 簡潔で明確なコードはプロセスコメントの要求がありませんが、なぜそうするのか、コードの関連背景などはコメントで補足し、有効な情報を提供できます。

命名規範#

変数名#

  • 簡潔さは冗長さに勝る

    • iindex の作用範囲では、index の余分な冗長さは必要ありません。
// Bad
for index := 0; index < len(s); index++ {
    // do something
}
// Good
for i := 0; i < len(s); i++ {
    // do something
}
  • 略語は全て大文字ですが、変数の先頭にあり、エクスポートする必要がない場合は、全て小文字を使用します。

    • 例えば ServeHTTP を使用し、ServeHttp ではなく
    • XMLHTTPRequest または xmlHTTPRequest を使用します。
  • 変数名が使用される場所から遠くなるほど、より多くの文脈情報を持つ必要があります。

    • 例えば、グローバル変数では、その名前により多くの文脈情報が必要で、異なる場所でその意味を容易に識別できるようにします。
// Bad
func (c *Client) send(req *Request, t time.Time)

// Good
func (c *Client) send(req *Request, deadline time.Time)

関数命名#

  • 関数名は パッケージ名の文脈情報を持たない、なぜならパッケージ名と関数名は常にペアで現れるからです。

    • 例えば、http パッケージ内でサービスを作成する関数は、 Serve よりも ServeHTTP です。なぜなら呼び出すときは常に http.Serve だからです。
  • 関数名は できるだけ短く

  • foo というパッケージのある関数が戻り値の型 T を返す場合(TFoo でない場合)、関数名に戻り値の型情報を追加できます。

    • Foo 型を返す場合は、省略しても誤解を招きません。

パッケージ名#

  • 小文字のみで構成される。大文字やアンダースコアなどの文字を含まない。
  • 短く、一定の文脈情報を含む。例えば schematask など。
  • 標準ライブラリと同名にしない。例えば syncstrings を使用しない。以下の規則をできるだけ満たすこと。標準ライブラリのパッケージ名を例に:
  • 一般的な変数名をパッケージ名として使用しない。例えば bufio を使用し、buf ではなく。
  • 複数形ではなく単数形を使用する。例えば encoding を使用し、encodings ではなく。
  • 略語の使用には注意する。例えば、文脈を壊さない範囲で fmt を使用することは format よりも短くなります。

全体的に、良い命名はコードの理解コストを下げ、主なプロセスに焦点を当て、プログラムの機能を明確に理解できるようにし、頻繁にブランチの詳細に切り替える必要がなくなります。

制御フロー#

  • ネストを避け、通常のフローを明確に読みやすく保つ。

    • エラー状況や特殊な状況を優先的に処理し、早期に戻ったりループを続けたりしてネストを減らします。
 // Bad
 if foo {
    return x
 } else {
    return nil
 }
 ​
 // Good
 if foo {
    return x
 }
 return nil
  • 通常のコードパスを最小のインデントに保ち、ネストを減らす。
 // Bad
 func OneFunc() error {
    err := doSomething()
    if err == nil {
       err := doAnotherThing()
       if err == nil {
          return nil // normal case
       }
       return err
    }
    return err
 }
 ​
 // Good
 func OneFunc() error {
    if err := doSomething(); err != nil {
       return err
    }
    if err := doSomething(); err != nil {
       return err
    }
    return nil // normal case
 }

要するに、プログラム内のフロー処理はできるだけ直線的にし、複雑なネスト分岐を避け、通常のフローコードを画面に沿って下に移動させます。これにより、コードの保守性と可読性が向上します。なぜなら、故障問題は大抵複雑な条件文やループ文に現れるからです。

エラー処理#

  • 簡単なエラー

    • 簡単なエラーは 一度だけ発生する エラーであり、他の場所で そのエラーを捕捉する必要がない
    • 優先的に errors.New を使用して匿名変数を作成し、簡単なエラーを直接表現します。
    • フォーマットが必要な場合は、fmt.Errorf を使用します。
 func defaultCheckRedirect(req *Request, via []*Request) error {
    if len(via) >= 10 {
       return errors.New("10回のリダイレクト後に停止しました")
    }
    return nil
 }
  • 複雑なエラー:エラーの WrapUnwrap を使用します。

    • エラーの Wrap は、実際には error を別の error にネストする能力を提供し、エラーの追跡チェーンを生成します。
    • fmt.Errorf%w キーワードを使用して、エラーをエラーのチェーンに関連付けます。
    • errors.Is を使用して、エラーが特定のエラーであるかどうかを判定し、エラーのチェーン上のすべてのエラーを判定できます(go/wrap_test.go · golang/go)。
    • errors.As を使用して、エラーのチェーン上で特定の種類のエラーを取得し、エラーを定義された変数に割り当てます(go/wrap_test.go · golang/go)。

Go において、エラーよりも深刻なのは panic であり、その発生はプログラムが正常に動作できないことを示します。

  • ビジネスコード内での panic の使用は推奨されません。

    • panic が発生すると、呼び出しスタックの頂上に向かって伝播します。
    • 呼び出し関数が全て recover を含まない場合、プログラム全体がクラッシュします。
    • 問題が隠蔽または解決できる場合は、panic の代わりに error を使用することを推奨します。
  • プログラムの起動段階不可逆的なエラーが発生した場合は、init または main 関数内で panic を使用できます(sarama/main.go · Shopify/sarama)。

panic があれば、自然と recover にも言及されます。他のライブラリのbugが原因でpanicが発生し、自身のロジックに影響を与える場合は、recover が必要です。

  • recoverdefer された関数内でのみ使用でき、ネストは効かず、現在の goroutine 内でのみ有効です(github.com/golang/go/b…)。
  • defer の文は後入れ先出しです。
  • さらなる文脈情報が必要な場合は、recover 後にログに現在の呼び出しスタックを記録できます(github.com/golang/webs…)。

小結#

  • error は可能な限り簡潔な文脈情報チェーンを提供し、問題の特定を容易にします。
  • panic は本当に異常な状況で使用されます。
  • recover の効果範囲は、現在の goroutine の defer された関数内で有効です。

パフォーマンス最適化の提案#

  • 前提:正確で信頼性があり、簡潔で明確な品質要因を満たした上で、プログラムの効率をできるだけ高める。
  • 妥協:時には時間効率と空間効率が対立することがあり、重要度を分析して適切に妥協する必要があります。

Go 言語の特性に基づいて、授業では多くの Go 関連のパフォーマンス最適化提案が紹介されました。

メモリの事前割り当て#

make () を使用してスライスを初期化する際は、できるだけ容量情報を提供します。

 func PreAlloc(size int) {
    data := make([]int, 0, size)
    for k := 0; k < size; k++ {
       data = append(data, k)
    }
 }

これは、スライスが本質的に配列の断片の記述であり、配列ポインタ、断片の長さ、断片の容量(メモリ割り当てを変更しない場合の最大長)を含むためです。

  • スライス操作はスライスが指す要素をコピーしません。
  • 新しいスライスを作成すると、元のスライスの基底配列を再利用します。したがって、事前に容量の値を設定することで、追加のメモリ割り当てを回避し、より良いパフォーマンスを得ることができます。

文字列処理の最適化#

strings.Builderを使用した一般的な文字列結合方法

  • + で結合する(最も遅い)

  • strings.Builder (最も速い)

  • bytes.Buffer 原理:文字列は Go 言語において不変型であり、占有するメモリサイズは固定です。

  • + で結合する際、新しい文字列を生成し、新しい空間を開放します。この新しい空間のサイズは元の 2 つの文字列のサイズの合計です。

  • strings.Builderbytes.Buffer のメモリは倍数で要求されます。

  • strings.Builderbytes.Buffer の基底はどちらも []byte 配列です。

    • bytes.Buffer は文字列に変換する際に新しい空間を再度要求し、生成された文字列変数を格納します。
    • strings.Builder は基底の []byte を直接文字列型に変換して返します。
 func PreStrBuilder(n int, str string) string {
    var builder strings.Builder
    builder.Grow(n * len(str))
    for i := 0; i < n; i++ {
       builder.WriteString(str)
    }
    return builder.String()
 }

空の構造体#

  • 空の構造体 struct インスタンスはメモリ空間を占有しません

  • 様々なシーンでのプレースホルダーとして使用できます。

    • メモリ空間を節約
    • 空の構造体自体は非常に強い意味を持ち、ここに値は必要なく、プレースホルダーとしてのみ使用されます。
  • 例えば、Set を実装する際に、map のキーを利用し、値を空の構造体に設定します。(golang-set/threadunsafe...

関連リンク#

まとめと感想#

本節では、Go や他の言語における一般的なコーディング規範を紹介し、Go 言語に関連するパフォーマンス最適化の提案を行いました。今後は、pprof ツールを使用したパフォーマンス最適化の実践演習も行いました。

ノートの内容は、第三回青訓キャンプの張雷先生の講義「高品質プログラミングとパフォーマンス調整実戦」に基づいています。
講義資料:【Go 言語原理と実践学習資料(上)】第三回バイトダンス青訓キャンプ - バックエンド専用

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。