ジェネリックが導入される前は、さまざまなデータ型をサポートするジェネリック関数を実装するためのアプローチがいくつかありました。
アプローチ 1: データ型ごとに関数を実装する
このアプローチでは、コードが非常に冗長になり、保守コストが高くなります。変更を行うには、すべての機能に対して同じ操作を実行する必要があります。さらに、Go 言語は同じ名前の関数のオーバーロードをサポートしていないため、これらの関数を外部モジュール呼び出しに公開することも不便です。
アプローチ 2: 最大範囲のデータ型を使用する
コードの冗長性を回避するための別の方法は、最大範囲のデータ型を使用することです (アプローチ 2)。典型的な例は math.Max で、これは 2 つの数値のうち大きい方を返します。さまざまなデータ型のデータを比較できるようにするために、 math.Max は Go の数値型の中で最も範囲が広いデータ型である float64 を入出力パラメーターとして使用し、精度の低下を回避します。これによりコードの冗長性の問題はある程度解決されますが、どのような種類のデータでも最初に float64 型に変換する必要があります。たとえば、int と int を比較する場合、型キャストが依然として必要ですが、パフォーマンスが低下するだけでなく、不自然に見えます。
アプローチ 3: インターフェース タイプを使用する
インターフェース タイプを使用すると、上記の問題は効果的に解決されます。{}ただし、interface{} タイプは実行時に型アサーションまたは型判断を必要とするため、実行時に一定のオーバーヘッドが発生し、パフォーマンスの低下につながる可能性があります。{}さらに、interface{} 型を使用する場合、コンパイラは静的型チェックを実行できないため、一部の型エラーは実行時にのみ発見される可能性があります。
Go 1.18 ではジェネリックのサポートが導入されました。これは、Go 言語のオープンソース化以来の重要な変更です。
ジェネリックスはプログラミング言語の機能です。これにより、プログラマはプログラミングで実際の型の代わりにジェネリック型を使用できるようになります。その後、実際の呼び出し中に明示的な受け渡しまたは自動推論を通じてジェネリック型が置き換えられ、コードの再利用の目的が達成されます。ジェネリックスを使用するプロセスでは、操作対象のデータ型がパラメーターとして指定されます。このようなパラメーターの型は、クラス、インターフェイス、メソッドではそれぞれジェネリック クラス、ジェネリック インターフェイス、ジェネリック メソッドと呼ばれます。
ジェネリックの主な利点は、コードの再利用性と型安全性が向上することです。従来の形式パラメータと比較して、ジェネリックを使用すると、ユニバーサル コードをより簡潔かつ柔軟に作成できるようになり、さまざまな種類のデータを処理できるようになり、Go 言語の表現力と再利用性がさらに向上します。同時に、ジェネリックの特定の型がコンパイル時に決定されるため、型チェックを提供して型変換エラーを回避できます。
Go 言語では、インターフェースとジェネリックの両方が複数のデータ型を処理するためのツールです。{}それらの違いについて説明するために、まずインターフェースとジェネリックの実装原則を見てみましょう。
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") }
インターフェース{}は、実行時のボックス化およびボックス化解除操作を通じて複数のデータ型の操作をサポートしていることがわかります。
Go コア チームは、Go ジェネリックの実装スキームを評価する際に非常に慎重でした。合計 3 つの実装スキームが提出されました:
ステンシル スキームは、ジェネリックスを実装するために 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") }
インターフェース{}とジェネリックの基本的な実装原理から、これらの主な違いは、インターフェース{}が実行時に異なるデータ型の処理をサポートするのに対し、ジェネリックはコンパイル段階で静的に異なるデータ型の処理をサポートすることであることがわかります。実際の使用においては、主に次のような違いがあります:
(1) パフォーマンスの違い: さまざまな種類のデータがインターフェイスに割り当てられるか、インターフェイスから取得されるときに実行されるボックス化およびボックス化解除の操作はコストがかかり、追加のオーバーヘッドが発生します。対照的に、ジェネリックはボックス化およびボックス化解除操作を必要とせず、ジェネリックによって生成されるコードは特定の型に最適化され、実行時のパフォーマンスのオーバーヘッドを回避します。
(2) 型安全性:interface{} 型を使用する場合、コンパイラは静的型チェックを実行できず、実行時に型アサーションのみを実行できます。したがって、一部の型エラーは実行時にのみ発見される場合があります。対照的に、Go のジェネリック コードはコンパイル時に生成されるため、ジェネリック コードはコンパイル時に型情報を取得でき、型安全性が確保されます。
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 }
基になる要素の型 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 }
Union 型要素は型パラメータにすることはできず、非インターフェイス要素はペアごとに素である必要があります。複数の要素がある場合、空ではないメソッドを持つインターフェイス タイプを含めることはできません。また、比較したり比較したりすることもできません。
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
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{}
ジェネリックを有効に活用するには、使用時に次の点に注意する必要があります。
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 つの側面に要約できます。
最後に、Go サービスのデプロイに最適なプラットフォームである Leapcell を紹介します。
ドキュメントでさらに詳しく見てみましょう!
Leapcell Twitter: https://x.com/LeapcellHQ
以上がGo ジェネリックス: ディープダイブの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。