ホームページ >バックエンド開発 >Golang >Go ジェネリックス: ディープダイブ

Go ジェネリックス: ディープダイブ

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2025-01-01 01:51:091010ブラウズ

Go Generics: A Deep Dive

1. ジェネリック医薬品を使用しない

ジェネリックが導入される前は、さまざまなデータ型をサポートするジェネリック関数を実装するためのアプローチがいくつかありました。

アプローチ 1: データ型ごとに関数を実装する
このアプローチでは、コードが非常に冗長になり、保守コストが高くなります。変更を行うには、すべての機能に対して同じ操作を実行する必要があります。さらに、Go 言語は同じ名前の関数のオーバーロードをサポートしていないため、これらの関数を外部モジュール呼び出しに公開することも不便です。

アプローチ 2: 最大範囲のデータ型を使用する
コードの冗長性を回避するための別の方法は、最大範囲のデータ型を使用することです (アプローチ 2)。典型的な例は math.Max で、これは 2 つの数値のうち大きい方を返します。さまざまなデータ型のデータを比較できるようにするために、 math.Max は Go の数値型の中で最も範囲が広いデータ型である float64 を入出力パラメーターとして使用し、精度の低下を回避します。これによりコードの冗長性の問題はある程度解決されますが、どのような種類のデータでも最初に float64 型に変換する必要があります。たとえば、int と int を比較する場合、型キャストが依然として必要ですが、パフォーマンスが低下するだけでなく、不自然に見えます。

アプローチ 3: インターフェース タイプを使用する
インターフェース タイプを使用すると、上記の問題は効果的に解決されます。{}ただし、interface{} タイプは実行時に型アサーションまたは型判断を必要とするため、実行時に一定のオーバーヘッドが発生し、パフォーマンスの低下につながる可能性があります。{}さらに、interface{} 型を使用する場合、コンパイラは静的型チェックを実行できないため、一部の型エラーは実行時にのみ発見される可能性があります。

2. ジェネリック医薬品のメリット

Go 1.18 ではジェネリックのサポートが導入されました。これは、Go 言語のオープンソース化以来の重要な変更です。
ジェネリックスはプログラミング言語の機能です。これにより、プログラマはプログラミングで実際の型の代わりにジェネリック型を使用できるようになります。その後、実際の呼び出し中に明示的な受け渡しまたは自動推論を通じてジェネリック型が置き換えられ、コードの再利用の目的が達成されます。ジェネリックスを使用するプロセスでは、操作対象のデータ型がパラメーターとして指定されます。このようなパラメーターの型は、クラス、インターフェイス、メソッドではそれぞれジェネリック クラス、ジェネリック インターフェイス、ジェネリック メソッドと呼ばれます。
ジェネリックの主な利点は、コードの再利用性と型安全性が向上することです。従来の形式パラメータと比較して、ジェネリックを使用すると、ユニバーサル コードをより簡潔かつ柔軟に作成できるようになり、さまざまな種類のデータを処理できるようになり、Go 言語の表現力と再利用性がさらに向上します。同時に、ジェネリックの特定の型がコンパイル時に決定されるため、型チェックを提供して型変換エラーを回避できます。

3. ジェネリックとインターフェースの違い{}

Go 言語では、インターフェースとジェネリックの両方が複数のデータ型を処理するためのツールです。{}それらの違いについて説明するために、まずインターフェースとジェネリックの実装原則を見てみましょう。

3.1 インターフェース{}実装原則

interface{} は、インターフェイス タイプにメソッドのない空のインターフェイスです。すべての型はインターフェース{}を実装しているため、任意の型を受け入れることができる関数、メソッド、またはデータ構造を作成するために使用できます。実行時のインターフェースの基礎となる構造は eface として表され、その構造は以下に示されており、主に _type と data の 2 つのフィールドが含まれています。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

_type は、サイズ、種類、ハッシュ関数、実際の値の文字列表現などの情報を含む _type 構造体へのポインタです。 data は実際のデータへのポインタです。実際のデータのサイズがポインターのサイズ以下の場合、データはデータ フィールドに直接格納されます。それ以外の場合、データ フィールドには実際のデータへのポインタが格納されます。
特定の型のオブジェクトがインターフェイス型の変数に割り当てられると、Go 言語は暗黙的に eface のボックス化操作を実行し、_type フィールドを値の型に設定し、データ フィールドを値のデータに設定します。{} 。たとえば、ステートメント var iinterface{} = 123 が実行されると、Go は eface 構造体を作成します。ここで、_type フィールドは int 型を表し、data フィールドは値 123 を表します。
格納された値をinterface{}から取得する際には、アンボックス化処理、つまり型アサーションまたは型判定が発生します。このプロセスでは、予期されるタイプを明示的に指定する必要があります。 Interface{} に格納されている値の型が期待される型と一致する場合、型アサーションは成功し、値を取得できます。そうしないと、型アサーションが失敗し、この状況では追加の処理が必要になります。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

インターフェース{}は、実行時のボックス化およびボックス化解除操作を通じて複数のデータ型の操作をサポートしていることがわかります。

3.2 ジェネリックスの実装原則

Go コア チームは、Go ジェネリックの実装スキームを評価する際に非常に慎重でした。合計 3 つの実装スキームが提出されました:

  • ステンシルスキーム
  • 辞書スキーム
  • GC シェイプ ステンシル スキーム

ステンシル スキームは、ジェネリックスを実装するために C や Rust などの言語で採用されている実装スキームでもあります。その実装原理は、コンパイル中に、ジェネリック関数が呼び出されるときの特定の型パラメータまたは制約内の型要素に従って、型の安全性と最適なパフォーマンスを確保するために型引数ごとにジェネリック関数の個別の実装が生成されるというものです。 。ただし、この方法ではコンパイラの速度が低下します。多数のデータ型が呼び出される場合、ジェネリック関数はデータ型ごとに独立した関数を生成する必要があり、その結果、コンパイルされたファイルが非常に大きくなる可能性があるためです。同時に、CPU キャッシュ ミスや命令分岐予測などの問題により、生成されたコードが効率的に実行されない可能性があります。

Dictionaries スキームは、ジェネリック関数の関数ロジックを 1 つだけ生成しますが、最初のパラメーターとしてパラメーター dict を関数に追加します。 dict パラメーターは、ジェネリック関数の呼び出し時に型引数の型関連情報を格納し、関数呼び出し中に AX レジスタ (AMD) を使用して辞書情報を渡します。このスキームの利点は、コンパイル段階のオーバーヘッドが軽減され、バイナリ ファイルのサイズが増加しないことです。ただし、実行時のオーバーヘッドが増加し、コンパイル段階で関数の最適化が行えず、辞書再帰などの問題があります。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

Go は最終的に上記 2 つのスキームを統合し、一般的な実装のための GC Shape Stenciling スキームを提案しました。型の GC Shape 単位で関数コードを生成します。同じ GC Shape を持つ型は、同じコードを再利用します (型の GC Shape は、Go メモリ アロケーター/ガベージ コレクターでのその表現を参照します)。すべてのポインター型は *uint8 型を再利用します。同じ GC Shape を持つ型の場合、共有インスタンス化された関数コードが使用されます。また、このスキームは、同じ GC Shape を持つ異なる型を区別するために、インスタンス化された各関数コードに dict パラメーターを自動的に追加します。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

3.3 相違点

インターフェース{}とジェネリックの基本的な実装原理から、これらの主な違いは、インターフェース{}が実行時に異なるデータ型の処理をサポートするのに対し、ジェネリックはコンパイル段階で静的に異なるデータ型の処理をサポートすることであることがわかります。実際の使用においては、主に次のような違いがあります:
(1) パフォーマンスの違い: さまざまな種類のデータがインターフェイスに割り当てられるか、インターフェイスから取得されるときに実行されるボックス化およびボックス化解除の操作はコストがかかり、追加のオーバーヘッドが発生します。対照的に、ジェネリックはボックス化およびボックス化解除操作を必要とせず、ジェネリックによって生成されるコードは特定の型に最適化され、実行時のパフォーマンスのオーバーヘッドを回避します。
(2) 型安全性:interface{} 型を使用する場合、コンパイラは静的型チェックを実行できず、実行時に型アサーションのみを実行できます。したがって、一部の型エラーは実行時にのみ発見される場合があります。対照的に、Go のジェネリック コードはコンパイル時に生成されるため、ジェネリック コードはコンパイル時に型情報を取得でき、型安全性が確保されます。

4. ジェネリック医薬品のシナリオ

4.1 該当するシナリオ

  • 一般的なデータ構造を実装する場合: ジェネリックスを使用すると、コードを一度作成すれば、それをさまざまなデータ型で再利用できます。これにより、コードの重複が減り、コードの保守性と拡張性が向上します。
  • Go でネイティブ コンテナ タイプを操作する場合: 関数がスライス、マップ、チャネルなどの Go 組み込みコンテナ タイプのパラメータを使用し、関数コードがコンテナ内の要素タイプについて特定の仮定を行わない場合、ジェネリックスを使用すると、コンテナー アルゴリズムをコンテナー内の要素の型から完全に分離できます。ジェネリック構文が利用可能になる前は、実装には通常リフレクションが使用されていましたが、リフレクションによりコードが読みにくくなり、静的型チェックを実行できなくなり、プログラムの実行時のオーバーヘッドが大幅に増加しました。
  • 異なるデータ型に対して実装されたメソッドのロジックが同じ場合: 異なるデータ型のメソッドが同じ関数ロジックを持ち、唯一の違いが入力パラメーターのデータ型である場合、ジェネリックスを使用してコードの冗長性を減らすことができます。

4.2 適用されないシナリオ

  • インターフェイスの型を型パラメーターで置き換えないでください。インターフェイスは、ある意味の汎用プログラミングをサポートします。特定の型の変数に対する操作でその型のメソッドのみを呼び出す場合は、ジェネリックスを使用せずにインターフェイス型を直接使用します。たとえば、io.Reader はインターフェイスを使用して、ファイルや乱数ジェネレーターからさまざまなタイプのデータを読み取ります。 io.Readerはコード的にも読みやすく効率が高く、関数の実行効率もほとんど変わらないため型引数を使う必要がありません。
  • 異なるデータ型のメソッドの実装詳細が異なる場合: 各型のメソッド実装が異なる場合は、ジェネリックスの代わりにインターフェイス型を使用する必要があります。
  • 強力な実行時ダイナミクスがあるシナリオ: たとえば、スイッチを使用して型判定が実行されるシナリオでは、interface{} を直接使用する方が良い結果が得られます。

5. ジェネリック医薬品の罠

5.1 なし 比較

Go 言語では、型パラメータはコンパイル時に型チェックされるのに対し、nil は実行時に特別な値であるため、型パラメータを nil と直接比較することはできません。型パラメーターの基になる型はコンパイル時に不明であるため、コンパイラーは型パラメーターの基になる型が nil との比較をサポートするかどうかを判断できません。したがって、型の安全性を維持し、潜在的な実行時エラーを回避するために、Go 言語では型パラメーターと nil を直接比較することはできません。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.2 無効な基礎要素

基になる要素の型 T は基本型である必要があり、インターフェイス型にすることはできません。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.3 無効な共用体型要素

Union 型要素は型パラメータにすることはできず、非インターフェイス要素はペアごとに素である必要があります。複数の要素がある場合、空ではないメソッドを持つインターフェイス タイプを含めることはできません。また、比較したり比較したりすることもできません。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

5.4 インターフェース型は再帰的に埋め込むことができない

type Op interface{
       int|float 
}
func Add[T Op](m, n T) T { 
       return m + n
} 
// After generation =>
const dict = map[type] typeInfo{
       int : intInfo{
             newFunc,
             lessFucn,
             //......
        },
        float : floatInfo
} 
func Add(dict[T], m, n T) T{}

6. ベストプラクティス

ジェネリックを有効に活用するには、使用時に次の点に注意する必要があります。

  1. 過度の一般化は避けてください。 ジェネリックはすべてのシナリオに適しているわけではないため、どのシナリオにジェネリックが適しているかを慎重に検討する必要があります。リフレクションは必要に応じて使用できます。Go にはランタイム リフレクションがあります。リフレクション メカニズムは、ある意味の汎用プログラミングをサポートします。特定の操作で次のシナリオをサポートする必要がある場合は、リフレクションを検討できます。 (1) インターフェースの型が適用できない、メソッドのない型の操作。 (2) 型ごとに演算ロジックが異なる場合、ジェネリックは適用されません。例としては、encoding/json パッケージの実装があります。エンコードされる各型が MarshalJson メソッドを実装することは望ましくないため、インターフェイス型は使用できません。また、タイプごとにエンコード ロジックが異なるため、ジェネリックスは使用しないでください。
  2. T にポインター型、スライス、またはマップを表すのではなく、*T、[]T、map[T1]T2 を明確に使用してください。 C の型パラメーターはプレースホルダーであり、実際の型に置き換えられるのとは異なり、Go の型パラメーター T の型は型パラメーターそのものです。したがって、これをポインタ、スラ​​イス、マップ、およびその他のデータ型で表すと、以下に示すように、使用中に多くの予期しない状況が発生します。
type V interface{
        int|float|*int|*float
} 
func F[T V](m, n T) {}
// 1. Generate templates for regular types int/float
func F[go.shape.int_0](m, n int){} 
func F[go.shape.float_0](m, n int){}
// 2. Pointer types reuse the same template
func F[go.shape.*uint8_0](m, n int){}
// 3. Add dictionary passing during the call
const dict = map[type] typeInfo{
        int : intInfo{},
        float : floatInfo{}
} 
func F[go.shape.int_0](dict[int],m, n int){}

上記のコードはエラーを報告します: 無効な操作: ptr (*int | *uint によって制約される型 T の変数) のポインターは同一の基本型を持つ必要があります。このエラーの理由は、T が型パラメーターであり、その型パラメーターがポインターではなく、逆参照操作をサポートしていないためです。これは、定義を次のように変更することで解決できます:

// Wrong example
func ZeroValue0[T any](v T) bool {
    return v == nil  
}
// Correct example 1
func Zero1[T any]() T {
    return *new(T) 
}
// Correct example 2
func Zero2[T any]() T {
    var t T
    return t 
}
// Correct example 3
func Zero3[T any]() (t T) {
    return 
}

まとめ

全体として、ジェネリック医薬品の利点は次の 3 つの側面に要約できます。

  1. 型はコンパイル中に決定され、型の安全性が確保されます。入れたものは取り出されます。
  2. 可読性が向上しました。実際のデータ型はコーディング段階から明示的にわかります。
  3. ジェネリックは同じタイプの処理コードをマージし、コードの再利用率を向上させ、プログラムの全体的な柔軟性を高めます。 ただし、ジェネリックは一般的なデータ型には必須ではありません。ジェネリック医薬品を使用するかどうかは、実際の使用状況に応じて慎重に検討する必要があります。

Leapcell: Go Web ホスティング、非同期タスク、Redis のための高度なプラットフォーム

Go Generics: A Deep Dive

最後に、Go サービスのデプロイに最適なプラットフォームである Leapcell を紹介します。

1. 多言語サポート

  • JavaScript、Python、Go、または Rust を使用して開発します。

2. 無制限のプロジェクトを無料でデプロイ

  • 使用料金のみお支払いください。リクエストや料金はかかりません。

3. 比類のないコスト効率

  • アイドル料金なしの従量課金制。
  • 例: $25 は、平均応答時間 60 ミリ秒で 694 万のリクエストをサポートします。

4. 合理化された開発者エクスペリエンス

  • 直感的な UI でセットアップが簡単。
  • 完全に自動化された CI/CD パイプラインと GitOps の統合。
  • 実用的な洞察を得るリアルタイムのメトリクスとログ。

5. 容易な拡張性と高性能

  • 自動スケーリングにより、高い同時実行性を簡単に処理できます。
  • 運用上のオーバーヘッドがゼロ - 構築だけに集中できます。

ドキュメントでさらに詳しく見てみましょう!

Leapcell Twitter: https://x.com/LeapcellHQ

以上がGo ジェネリックス: ディープダイブの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。