[インデックス 1553] ファイルの概要
このコミットは、Go言語の型システムにおける比較可能性 (comparability) とハッシュ可能性 (hashability) に関する新しい厳格な制限を実装するものです。具体的には、スライス、マップ、関数といった特定の型が、デフォルトでは比較やハッシュの対象外となるように変更され、これらの操作が不正に行われた場合にはランタイムエラーが発生するようになります。これにより、Go言語のセマンティクスがより明確になり、予期せぬ動作を防ぐことが目的です。
コミット
commit a7f6d4066e871916931af4b99f1d5a9021dbfeb9
Author: Russ Cox <rsc@golang.org>
Date: Mon Jan 26 09:56:42 2009 -0800
implement new restrictions on what
can be compared/hashed.
R=r
DELTA=351 (201 added, 80 deleted, 70 changed)
OCL=23423
CL=23481
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a7f6d4066e871916931af4b99f1d5a9021dbfeb9
元コミット内容
比較/ハッシュ可能なものに新しい制限を実装。
変更の背景
Go言語の初期段階において、型の比較可能性やハッシュ可能性に関するルールはまだ完全に固まっていませんでした。特に、スライス、マップ、関数といった複合型が、直感に反する形で比較可能であるかのように扱われたり、マップのキーとして使用されたりする可能性がありました。これらの操作は、多くの場合、プログラマの意図しない動作やバグを引き起こす原因となります。例えば、スライスやマップは参照型であり、その内容ではなくヘッダ情報(ポインタ、長さ、容量など)のみが比較されると、異なる内容を持つが同じヘッダ情報を持つインスタンスが等しいと判断されるといった混乱が生じます。
このコミットは、このような曖昧さを排除し、Go言語の型システムをより堅牢で予測可能なものにするために導入されました。具体的には、比較やハッシュが意味をなさない、あるいは危険な型に対して、明示的にこれらの操作を禁止し、ランタイムでエラーを発生させることで、開発者が早期に問題を特定できるようにすることを目的としています。これにより、Goプログラムの信頼性と安全性が向上します。
前提知識の解説
このコミットを理解するためには、Go言語における以下の概念の理解が不可欠です。
-
型の比較可能性 (Comparability): Go言語では、
==
および!=
演算子を使用して2つの値を比較できます。しかし、すべての型が比較可能であるわけではありません。- 比較可能な型: 数値型、文字列型、ブール型、ポインタ型、チャネル型、インターフェース型、構造体型(すべてのフィールドが比較可能な場合)、配列型(要素型が比較可能な場合)。
- 比較不可能な型: スライス、マップ、関数。これらの型は、その性質上、値の等価性を意味のある形で定義することが困難であるため、Go言語の仕様では比較が許可されていません。例えば、2つのスライスが同じ要素を持っていても、それらが異なる基盤配列を参照している場合、それらを「等しい」と見なすべきかは文脈に依存します。Goはこのような曖昧さを避けるために、これらの型の直接比較を禁止しています。
-
型のハッシュ可能性 (Hashability): マップのキーとして使用できる型は、ハッシュ可能である必要があります。ハッシュ可能とは、その型の値を一意のハッシュ値に変換できることを意味します。ハッシュ値は、マップ内部でキーの格納場所を効率的に決定するために使用されます。
- ハッシュ可能な型: 比較可能な型はすべてハッシュ可能です。
- ハッシュ不可能な型: スライス、マップ、関数。これらの型は比較不可能であるため、ハッシュすることもできません。
-
インターフェース型 (Interface Types): Goのインターフェースは、メソッドのセットを定義します。インターフェース型の変数は、そのインターフェースが定義するすべてのメソッドを実装する任意の具象型の値を保持できます。インターフェース値の比較は、その内部に保持されている具象型と値の両方が等しい場合に
true
を返します。しかし、インターフェースが比較不可能な具象型(例: スライスやマップ)を保持している場合、そのインターフェース値自体も比較不可能となります。 -
Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのコンポーネントです。メモリ管理、ゴルーチン管理、チャネル操作、マップ操作など、Go言語の多くの組み込み機能はランタイムによって実装されています。型の比較やハッシュもランタイムレベルで処理されるため、このコミットではランタイムのコードが大きく変更されています。
-
src/cmd/gc
(Goコンパイラ): Goのコンパイラは、ソースコードを機械語に変換するだけでなく、型チェックや最適化も行います。このコミットでは、コンパイラが型の比較可能性/ハッシュ可能性に関する新しいルールを認識し、適切なランタイム関数を呼び出すように変更されています。
技術的詳細
このコミットの技術的詳細は、Goコンパイラ (src/cmd/gc
) とGoランタイム (src/runtime
) の両方における型の扱い方の根本的な変更にあります。
-
型アルゴリズムの再分類 (
src/cmd/gc/go.h
,src/cmd/gc/subr.c
):- Goコンパイラは、各型に対して「アルゴリズムタイプ」を割り当てます。これは、その型がどのようにメモリに配置され、どのように比較・ハッシュされるかを決定するための内部的な分類です。
- 以前は
ASIMP
(単純なメモリ)、APTR
(ポインタ)、ASTRING
(文字列)、AINTER
(インターフェース)、ASLICE
(スライス)、ASTRUCT
(構造体) など、より詳細な分類がありました。 - このコミットでは、これらの分類が簡素化され、
AMEM
とANOEQ
が導入されました。AMEM
(Memory): 整数、ポインタ、チャネルなど、メモリの内容を直接比較・ハッシュできる型に割り当てられます。ANOEQ
(No Equality/Hash): スライス、マップ、関数など、比較やハッシュが許可されない型に割り当てられます。
src/cmd/gc/subr.c
のalgtype
関数が更新され、新しい分類に基づいて型にアルゴリズムタイプを割り当てるようになりました。特に、スライスやマップは明示的にANOEQ
に分類されるようになります。
-
ランタイムにおける比較・ハッシュ関数の統一とエラーハンドリング (
src/runtime/runtime.h
,src/runtime/runtime.c
,src/runtime/iface.c
):- Goランタイムは、
algarray
と呼ばれるテーブルを保持しており、各アルゴリズムタイプに対応するハッシュ関数、比較関数、プリント関数、コピー関数へのポインタが格納されています。 - このコミットでは、
algarray
の構造が大幅に変更されました。AMEM
タイプには、汎用的なメモリ比較 (memequal
) およびハッシュ (memhash
) 関数が割り当てられます。ANOEQ
タイプには、新しいnohash
およびnoequal
関数が割り当てられます。これらの関数は、呼び出されると「hash of unhashable type」または「comparing uncomparable types」というエラーメッセージを出力し、throw
(Goランタイムのパニック機構) を呼び出してプログラムを異常終了させます。- 文字列 (
ASTRING
) とインターフェース (AINTER
) には、それぞれ専用の比較・ハッシュ関数 (strhash
,strequal
,interhash
,interequal
) が割り当てられます。
- 特に重要なのは、
src/runtime/iface.c
に追加されたifacehash
とifaceeq
関数です。これらの関数は、インターフェース値のハッシュや比較を行う際に、内部に保持されている具象型のアルゴリズムタイプをチェックし、もしそれがnohash
やnoequal
に設定されている場合は、適切なエラーを発生させるようになりました。これにより、インターフェースを介して比較不可能な型を比較しようとした場合でも、ランタイムエラーが確実に発生します。
- Goランタイムは、
-
チャネルとマップの要素/キーの型チェック強化 (
src/runtime/chan.c
,src/runtime/hashmap.c
):- チャネルの要素型やマップのキー/値の型を決定するランタイム関数 (
sys·newchan
,sys·newmap
) も更新されました。 - これらの関数は、渡された型アルゴリズムが
algarray
の有効な範囲内にあるか、またマップのキー型がハッシュ可能であるか (algarray[keyalg].hash == nohash
でないか) を厳密にチェックするようになりました。これにより、不正な型の使用が早期に検出されます。
- チャネルの要素型やマップのキー/値の型を決定するランタイム関数 (
-
テストケースの追加と変更 (
test/bigalg.go
,test/cmp*.go
,test/golden.out
,test/map.go
):- 既存のテストファイル (
test/bigalg.go
,test/map.go
) から、構造体や配列をマップのキーとして使用するテストが削除またはコメントアウトされました。これは、これらの操作がもはや許可されないことを反映しています。 test/cmp1.go
からtest/cmp5.go
までの新しいテストファイルが追加されました。cmp1.go
は、文字列やチャネルなど、比較可能な型のインターフェースを介した比較が正しく動作することを確認します。cmp2.go
、cmp3.go
、cmp4.go
、cmp5.go
は、スライスやマップといった比較不可能な型を直接比較したり、インターフェースを介して比較したり、マップのキーとして使用したりした場合に、期待通りランタイムエラーが発生することを確認するためのものです。test/golden.out
には、これらのテストが生成するエラーメッセージ("comparing uncomparable type" や "hash of unhashable type")が記録されています。
- 既存のテストファイル (
これらの変更により、Go言語は型の比較可能性とハッシュ可能性に関して、より明確で厳格なセマンティクスを持つようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/cmd/gc/go.h
:- 型アルゴリズムの列挙型 (
enum
) の定義が変更されました。ASIMP
,APTR
,ASLICE
,ASTRUCT
,AARRAY
が削除され、代わりにAMEM
とANOEQ
が追加されました。
--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -37,13 +37,12 @@ enum PRIME10 = 10093, AUNK = 100, + // these values are known by runtime - ASIMP = 0, + AMEM = 0, + ANOEQ, ASTRING, - APTR, AINTER, - ASLICE, - ASTRUCT, BADWIDTH = -1000000000 };
- 型アルゴリズムの列挙型 (
-
src/cmd/gc/subr.c
:algtype
関数が、Goの型を新しいアルゴリズムタイプ (AMEM
,ANOEQ
) にマッピングするように変更されました。特に、スライス (isslice(t)
) や構造体 (t->etype == TSTRUCT
) の扱いがANOEQ
に変更されています。
--- a/src/cmd/gc/subr.c +++ b/src/cmd/gc/subr.c @@ -291,26 +291,16 @@ algtype(Type *t) { int a; - a = AUNK; - if(issimple[t->etype]) - a = ASIMP; // simple mem + if(issimple[t->etype] || isptr[t->etype] || t->etype == TCHAN) + a = AMEM; // just bytes (int, ptr, etc) else if(t->etype == TSTRING) a = ASTRING; // string else - if(isptr[simtype[t->etype]]) - a = APTR; // pointer - else - if(isslice(t)) - a = ASLICE; - else - if(t->etype == TSTRUCT) - a = ASTRUCT; - else if(isinter(t)) a = AINTER; // interface -// else -// fatal("algtype: cant find type %T", t); + else + a = ANOEQ; // just bytes, but no hash/eq return a; }
-
src/runtime/runtime.h
:- ランタイム側のアルゴリズムタイプの列挙型が更新され、
AMEM
,ANOEQ
,ASTRING
,AINTER
,Amax
が定義されました。 - 新しい比較・ハッシュ関数 (
ifaceeq
,ifacehash
,nohash
,noequal
) のプロトタイプ宣言が追加されました。
--- a/src/runtime/runtime.h +++ b/src/runtime/runtime.h @@ -226,18 +226,17 @@ struct Func */ enum { - ASIMP = 0, + AMEM, + ANOEQ, ASTRING, - APTR, AINTER, - AARRAY, - ASTRUCT, + Amax }; /* * external data */ -extern Alg algarray[]; +extern Alg algarray[Amax]; extern string emptystring; G* allg; int32 goidgen; @@ -299,6 +298,10 @@ void* stackalloc(uint32); void stackfree(void*); MCache* allocmcache(void); void mallocinit(void); +bool ifaceeq(Iface, Iface); +uint64 ifacehash(Iface); +uint64 nohash(uint32, void*); +uint32 noequal(uint32, void*, void*);
- ランタイム側のアルゴリズムタイプの列挙型が更新され、
-
src/runtime/runtime.c
:algarray
の定義が大幅に変更され、各アルゴリズムタイプに対応するハッシュ関数、比較関数が再定義されました。特にANOEQ
にはnohash
とnoequal
が割り当てられています。nohash
とnoequal
関数が追加され、これらが呼び出された場合にランタイムエラーを発生させるようになりました。- インターフェースのハッシュと比較のための新しい関数 (
interhash
,interequal
) が追加されました。
--- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -328,57 +328,52 @@ strprint(uint32 s, string *a) sys·printstring(*a); } -static void -strcopy(uint32 s, string *a, string *b) +static uint64 +interhash(uint32 s, Iface *a) { USED(s); - if(b == nil) { - *a = nil; - return; - } - *a = *b; + return ifacehash(*a); } -static uint64 -ptrhash(uint32 s, void **a) +static void +interprint(uint32 s, Iface *a) { - return memhash(s, *a); + USED(s); + sys·printinter(*a); } static uint32 -ptrequal(uint32 s, void **a, void **b) +interequal(uint32 s, Iface *a, Iface *b) { - USED(s, a, b); - prints("ptrequal\n"); - return 0; + USED(s); + return ifaceeq(*a, *b); } -static void -ptrprint(uint32 s, void **a) +uint64 +nohash(uint32 s, void *a) { - USED(s, a); - prints("ptrprint\n"); + USED(s); + USED(a); + throw("hash of unhashable type"); + return 0; } -static void -ptrcopy(uint32 s, void **a, void **b) +uint32 +noequal(uint32 s, void *a, void *b) { USED(s); - if(b == nil) { - *a = nil; - return; - } - *a = *b; + USED(a); + USED(b); + throw("comparing uncomparable types"); + return 0; } Alg algarray[] = { -[ASIMP] { memhash, memequal, memprint, memcopy }, -[ASTRING] { strhash, strequal, strprint, strcopy }, -[APTR] { memhash, memequal, memprint, memcopy }, // TODO: ptr routines -[AINTER] { memhash, memequal, memprint, memcopy }, // TODO: interface routines -[ASTRUCT] { memhash, memequal, memprint, memcopy }, // TODO: what goes here? -[AARRAY] { memhash, memequal, memprint, memcopy }, // TODO: what goes here? +[AMEM] { memhash, memequal, memprint, memcopy }, +[ANOEQ] { nohash, noequal, memprint, memcopy }, +[ASTRING] { strhash, strequal, strprint, memcopy }, +[AINTER] { interhash, interequal, interprint, memcopy }, };
-
src/runtime/iface.c
:- インターフェースの比較関数
sys·ifaceeq
がifaceeq
という新しい関数を呼び出すように変更され、ifacehash
という新しいハッシュ関数が追加されました。 - これらの新しい関数内で、インターフェースが保持する具象型が比較可能/ハッシュ可能であるかを
algarray
を参照してチェックし、不正な場合はエラーを発生させるロジックが追加されました。
--- a/src/runtime/iface.c +++ b/src/runtime/iface.c @@ -404,11 +404,32 @@ sys·ifaceI2I2(Sigi *si, Iface i, Iface ret, bool ok) FLUSH(&ok); } -// ifaceeq(i1 any, i2 any) (ret bool);\n-void\n-sys·ifaceeq(Iface i1, Iface i2, bool ret)\n+uint64 +ifacehash(Iface a) { - int32 alg, wid; + int32 alg, wid; + + if(a.type == nil) + return 0; + alg = a.type->sigt->hash; + wid = a.type->sigt->offset; + if(algarray[alg].hash == nohash) { + // calling nohash will throw too, + // but we can print a better error. + printf("hash of unhashable type %s\n", a.type->sigt->name); + throw("interface hash"); + } + if(wid <= sizeof a.data) + return algarray[alg].hash(wid, &a.data); + else + return algarray[alg].hash(wid, a.data); +} + +bool +ifaceeq(Iface i1, Iface i2) +{ + int32 alg, wid; + bool ret; if(iface_debug) { prints("Ieq i1="); @@ -438,6 +459,13 @@ sys·ifaceeq(Iface i1, Iface i2, bool ret) if(wid != i2.type->sigt->offset) goto no; + if(algarray[alg].equal == noequal) { + // calling noequal will throw too, + // but we can print a better error. + printf("comparing uncomparable type %s\n", i1.type->sigt->name); + throw("interface compare"); + } + if(wid <= sizeof i1.data) { if(!algarray[alg].equal(wid, &i1.data, &i2.data)) goto no; @@ -454,6 +482,14 @@ no: sys·printbool(ret); prints("\n"); } + return ret; +} + +// ifaceeq(i1 any, i2 any) (ret bool); +void +sys·ifaceeq(Iface i1, Iface i2, bool ret) +{ + ret = ifaceeq(i1, i2); FLUSH(&ret); }
- インターフェースの比較関数
コアとなるコードの解説
このコミットの核心は、Go言語の型システムにおける「比較可能性」と「ハッシュ可能性」の概念を、より厳密かつ安全に定義し、ランタイムレベルで強制することにあります。
-
型アルゴリズムの再定義と役割分担:
- 以前は
ASIMP
(単純な型)、APTR
(ポインタ)、ASLICE
(スライス) など、型の種類ごとに細かくアルゴリズムが定義されていました。しかし、これらの型の中には、比較やハッシュが意味をなさない、あるいは危険なもの(例: スライスやマップ)が含まれていました。 - 新しい
AMEM
とANOEQ
という分類は、この問題を解決します。AMEM
は、その値がメモリ上のバイト列として直接比較・ハッシュできる型(整数、ポインタ、チャネルなど)を包括します。これらの型はmemhash
やmemequal
といった汎用的なメモリ操作関数で処理されます。ANOEQ
は、比較やハッシュが許可されない型(スライス、マップ、関数)のために導入されました。これらの型に対して比較やハッシュ操作が試みられた場合、nohash
やnoequal
関数が呼び出され、これらは即座にランタイムエラー (throw
) を発生させます。これにより、不正な操作が実行時に明確に検出され、プログラマに通知されます。
- 以前は
-
インターフェースの比較・ハッシュの厳格化:
- Goのインターフェースは、任意の具象型の値を保持できるため、インターフェースを介した比較やハッシュは特に注意が必要です。
src/runtime/iface.c
に追加されたifacehash
とifaceeq
関数は、この厳格化の中心です。これらの関数は、インターフェースが現在保持している具象型のアルゴリズムタイプを動的にチェックします。- もし具象型が
ANOEQ
に分類される型(例: スライスやマップ)である場合、ifacehash
はnohash
を、ifaceeq
はnoequal
を呼び出し、結果としてランタイムエラーが発生します。これにより、インターフェースの動的な性質を悪用して比較不可能な型を比較しようとする試みが防止されます。
-
マップのキーとチャネルの要素の型チェック:
- マップのキーはハッシュ可能でなければなりません。このコミットでは、
src/runtime/hashmap.c
のsys·newmap
関数が、キーのアルゴリズムタイプがnohash
でないことを明示的にチェックするようになりました。これにより、ハッシュ不可能な型をマップのキーとして使用しようとすると、マップ作成時にエラーが発生します。 - 同様に、チャネルの要素型も
src/runtime/chan.c
のsys·newchan
でチェックされ、不正な型が使用されないようにします。
- マップのキーはハッシュ可能でなければなりません。このコミットでは、
これらの変更は、Go言語の型システムをより堅牢にし、開発者が型のセマンティクスをより正確に理解し、予期せぬバグを回避できるようにするための重要なステップです。これにより、Goプログラムの安全性と信頼性が向上します。
関連リンク
- Go言語の比較可能性に関する公式ドキュメント (Go 1.0以降の仕様): https://go.dev/ref/spec#Comparison_operators
- Go言語のマップに関する公式ドキュメント: https://go.dev/ref/spec#Map_types
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master?after=a7f6d4066e871916931af4b99f1d5a9021dbfeb9+34&branch=master&path%5B%5D=src%2Fcmd%2Fgc%2Fgo.h (このコミットの周辺の履歴を参照)
- Go言語の初期の設計に関する議論 (Go Mailing Listなど、当時の情報源)
- Go言語の仕様書 (Go 1.0以降の仕様): https://go.dev/ref/spec
- Go言語のインターフェースに関する解説記事 (一般的なGoの学習リソース)
- Go言語のランタイムに関する解説記事 (一般的なGoの学習リソース)