Go言語における`==`演算子の詳細な分析
Wenhao Wang
Dev Intern · Leapcell

Go言語における==
演算子の詳細な分析
概要
Go言語のプログラミングの実践において、==
等価演算子は非常に一般的です。しかし、フォーラムでのコミュニケーションでは、多くの開発者がGo言語における==
演算子の結果について混乱していることがよくあります。実際、Go言語が==
演算子を扱う際には、特別な注意が必要な多くの詳細があります。これらの詳細な問題は、日々の開発ではあまり遭遇しないかもしれませんが、一度遭遇すると、深刻なプログラムエラーにつながる可能性があります。この記事では、Go言語における==
演算子の関連コンテンツを体系的に深く掘り下げ、開発者の皆様に強力な支援を提供したいと考えています。
型システム
Go言語のデータ型は、次の4つのカテゴリに分類できます。
- 基本型: 整数型(
int
、uint
、int8
、uint8
、int16
、uint16
、int32
、uint32
、int64
、uint64
、byte
、rune
など)、浮動小数点数(float32
、float64
)、複素数型(complex64
、complex128
)、および文字列(string
)が含まれます。 - 複合型(集約型): 主に配列と構造体型が含まれます。
- 参照型: スライス(
slice
)、map
、チャネル、およびポインタが含まれます。 - インターフェース型:
error
インターフェースなど。
==
演算子の主要な前提条件は、2つのオペランドの型が完全に同じである必要があることを強調する必要があります。型が異なる場合、コンパイルエラーが発生します。
注意すべき点は次のとおりです。
- Go言語には厳格な型システムがあり、C/C++言語のような暗黙的な型変換メカニズムはありません。これは、コードを書く際には少し面倒かもしれませんが、後で発生する可能性のある多くのエラーを効果的に回避できます。
- Go言語では、
type
キーワードを使用して新しい型を定義できます。新しく定義された型は、基になる型とは異なり、直接比較することはできません。
型をより明確に表示するために、サンプルコードの変数定義はすべて明示的に型を指定しています。次に例を示します。
package main import "fmt" func main() { var a int8 var b int16 // コンパイルエラー: 無効な操作 a == b (型が一致しません int8 と int16) fmt.Println(a == b) }
このコードでは、a
とb
の型が異なるため(それぞれint8
とint16
)、==
比較を実行しようとするとコンパイルエラーが発生します。
別の例:
package main import "fmt" func main() { type int8 myint8 var a int8 var b myint8 // コンパイルエラー: 無効な操作 a == b (型が一致しません int8 と myint8) fmt.Println(a == b) }
ここで、myint8
の基になる型はint8
ですが、異なる型に属しているため、直接比較するとコンパイルエラーが発生します。
さまざまな型での==
演算の具体的な動作
基本型
基本型の比較演算は比較的シンプルで簡単で、値を比較するだけです。例は次のとおりです。
var a uint32 = 10 var b uint32 = 20 var c uint32 = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true
ただし、浮動小数点数の比較を扱う場合は、特別な注意を払う必要があります。
var a float64 = 0.1 var b float64 = 0.2 var c float64 = 0.3 fmt.Println(a + b == c) // false
これは、コンピューターでは、一部の浮動小数点数を正確に表現できず、浮動小数点演算の結果に特定のエラーが発生するためです。a + b
とc
の値をそれぞれ出力することで、違いを明確に確認できます。
fmt.Println(a + b) fmt.Println(c) // 0.30000000000000004 // 0.3
この問題はGo言語に固有のものではありません。IEEE 754標準に従うプログラミング言語は、浮動小数点数を扱う際に同様の状況に直面する可能性があります。したがって、プログラミングでは、直接的な浮動小数点数の比較はできるだけ避ける必要があります。どうしても比較する必要がある場合は、2つの浮動小数点数の差の絶対値を計算できます。この値が設定された非常に小さい値(1e - 9
など)より小さい場合、それらは等しいと見なすことができます。
複合型
Go言語の複合型(つまり、集約型)は、配列と構造体のみです。複合型の場合、==
演算は要素ごと/フィールドごとに比較します。
配列の長さはその型の一部であることに注意してください。長さの異なる2つの配列は異なる型に属しており、直接比較することはできません。
配列の場合、各要素の値が順番に比較されます。要素の型が異なる場合(基本型、複合型、参照型、またはインターフェース型である可能性があります)、比較は対応する型比較ルールに従って判断されます。すべての要素が等しい場合にのみ、2つの配列は等しいと見なされます。
構造体の場合、各フィールドの値も順番に比較されます。フィールド型が属する4つの主要な型カテゴリに従って、特定の型比較ルールに従ってください。すべてのフィールドが等しい場合にのみ、2つの構造体が等しくなります。
例は次のとおりです。
a := [4]int{1, 2, 3, 4} b := [4]int{1, 2, 3, 4} c := [4]int{1, 3, 4, 5} fmt.Println(a == b) // true fmt.Println(a == c) // false type A struct { a int b string } aa := A { a : 1, b : "leapcell_test1" } bb := A { a : 1, b : "leapcell_test2" } cc := A { a : 1, b : "leapcell_test3" } fmt.Println(aa == bb) fmt.Println(aa == cc)
参照型
参照型は、参照するデータを間接的に指し、変数はデータのアドレスを格納します。したがって、参照型の==
比較は、実際には2つの変数が同じデータを指しているかどうかを判断するものであり、指している実際のデータの内容を比較するものではありません。
例は次のとおりです。
type A struct { a int b string } aa := &A { a : 1, b : "leapcell_test1" } bb := &A { a : 1, b : "leapcell_test1" } cc := aa fmt.Println(aa == bb) fmt.Println(aa == cc)
この例では、aa
とbb
が指す構造体の値は等しいですが(上記の複合型の比較ルールを参照)、異なる構造体のインスタンスを指しているため、aa == bb
はfalse
です。一方、aa
とcc
は同じ構造体を指しているため、aa == cc
はtrue
です。
channel
を例にとると:
ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch3 := ch1 fmt.Println(ch1 == ch2) fmt.Println(ch1 == ch3)
ch1
とch2
は同じ型ですが、異なるchannel
インスタンスを指しているため、ch1 == ch2
はfalse
です。ch1
とch3
は同じchannel
を指しているため、ch1 == ch3
はtrue
です。
参照型に関して、2つの特別な規定があります。
- スライスは直接比較できません。スライスは
nil
値とのみ比較できます。 - マップは直接比較できません。マップは
nil
値とのみ比較できます。
スライスを直接比較できない理由は次のとおりです。参照型として、スライスは間接的にそれ自体を指す可能性があります。例:
a := []interface{}{ 1, 2.0 } a[1] = a fmt.Println(a) // !!! // runtime: goroutine stack exceeds 1000000000 - byte limit // fatal error: stack overflow
上記のコードでは、a
をa[1]
に割り当てることで、再帰的な参照が発生し、fmt.Println(a)
ステートメントを実行するとスタックオーバーフローエラーが発生します。スライスの参照アドレスが直接比較される場合、一方では、配列の比較方法とは大きく異なり、開発者を混乱させる可能性があります。他方では、スライスの長さと容量はその型の一部であり、長さと容量の異なるスライスに対して統一された比較ルールを決定するのは困難です。スライス内の要素が配列のように比較される場合、循環参照の問題に直面します。この問題は言語レベルで解決できますが、Go言語開発チームは、これにあまりにも多くの労力を投資する価値はないと考えています。上記の理由から、Go言語では、スライス型を直接比較できないことが明確に規定されており、==
を使用してスライスを比較すると、直接コンパイルエラーが発生します。例:
var a []int var b []int // 無効な操作: a == b (スライスはnilとのみ比較できます) fmt.Println(a == b)
エラーメッセージは、スライスをnil
値とのみ比較できることを明確に示しています。
map
型の場合、その値型は比較できない型(スライスなど)である可能性があるため、map
型も直接比較できません。
インターフェース型
インターフェース型は、Go言語で重要な役割を果たします。インターフェース型の値、つまりインターフェース値は、特定の型(つまり、インターフェースに格納されている値の型)とその型の値の2つの部分で構成されます。参照用語では、それらはそれぞれ動的型と動的値と呼ばれます。インターフェース値の比較には、これら2つの部分の比較が含まれます。動的型が完全に同じであり、動的値が等しい(動的値は==
を使用して比較されます)場合にのみ、2つのインターフェース値は等しくなります。
例は次のとおりです。
var a interface{} = 1 var b interface{} = 1 var c interface{} = 2 var d interface{} = 1.0 fmt.Println(a == b) // false fmt.Println(a == c) // true fmt.Println(a == d) // false
この例では、a
とb
の動的型は同じです(どちらもint
です)、動的値も同じです(どちらも1
です。これは基本型の比較に属します)。したがって、a == b
はtrue
です。a
とc
の動的型は同じですが、動的値は等しくありません(それぞれ1
と2
です)。したがって、a == c
はfalse
です。a
とd
の動的型は異なります(a
はint
で、d
はfloat64
です)。したがって、a == d
はfalse
です。
構造体がインターフェース値として使用される場合の状況を見てみましょう。
type A struct { a int b string } var aa interface{} = A { a: 1, b: "test" } var bb interface{} = A { a: 1, b: "test" } var cc interface{} = A { a: 2, b: "test" } fmt.Println(aa == bb) // true fmt.Println(aa == cc) // false var dd interface{} = &A { a: 1, b: "test" } var ee interface{} = &A { a: 1, b: "test" } fmt.Println(dd == ee) // false
aa
とbb
の動的型は同じです(どちらもA
です)、動的値も同じです(上記の複合型の構造体の比較ルールに従って)。したがって、aa == bb
はtrue
です。aa
とcc
の動的型は同じですが、動的値は異なります。したがって、aa == cc
はfalse
です。dd
とee
の動的型は同じです(どちらも*A
です)、動的値はポインタ(参照)型の比較ルールを使用します。同じアドレスを指していないため、dd == ee
はfalse
です。
インターフェースの動的値が比較できない場合、強制的に比較するとpanic
が発生することに注意してください。例:
var a interface{} = []int{1, 2, 3, 4} var b interface{} = []int{1, 2, 3, 4} // panic: runtime error: comparing uncomparable type []int fmt.Println(a == b)
ここで、a
とb
の動的値はスライス型であり、スライス型は比較できないため、a == b
を実行するとpanic
が発生します。
さらに、インターフェース値の比較では、インターフェース型(動的型ではないことに注意してください)が完全に同じである必要はありません。インターフェースを別のインターフェースに変換できる限り、比較を行うことができます。例:
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // 無効な操作: r == w (型が一致しません io.Reader と io.Writer) fmt.Println(r == w)
r
の型はio.Reader
インターフェースであり、rc
の型はio.ReadCloser
インターフェースです。ソースコードを見ると、io.ReadCloser
の定義は次のとおりです。
type ReadCloser interface { Reader Closer }
io.ReadCloser
はio.Reader
に変換できるため、r
とrc
を比較できます。一方、io.Writer
はio.Reader
に変換できないため、コンパイルエラーが発生します。
type
で定義された型
type
キーワードを使用して既存の型に基づいて定義された新しい型の場合、比較は基になる型に従って実行されます。例:
type myint int var a myint = 10 var b myint = 20 var c myint = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true type arr4 [4]int var aa arr4 = [4]int{1, 2, 3, 4} var bb arr4 = [4]int{1, 2, 3, 4} var cc arr4 = [4]int{1, 2, 3, 5} fmt.Println(aa == bb) fmt.Println(aa == cc)
ここで、myint
型は基になる型int
に従って比較され、arr4
型は基になる型[4]int
に従って比較されます。
比較できないこととその影響
上記のように、Go言語のスライス型は比較できません。これの影響は、スライスを含むすべての型も比較できないことです。具体的には、これらには以下が含まれます。
- 配列要素はスライス型です。
- 構造体にはスライス型のフィールドが含まれています。
- ポインターはスライス型を指しています。
比較できないことは推移的です。構造体がスライスフィールドを含むために比較できない場合、それを要素とする配列は比較できず、それをフィールド型とする構造体も比較できません。
map
と比較できない型の関係
map
のキーと値のペアは等価性の判断に==
演算を使用するため、比較できないすべての型はmap
のキーとして使用できません。例:
// 無効なマップキー型 []int m1 := make(map[[]int]int) type A struct { a []int b string } // 無効なマップキー型 A m2 := make(map[A]int)
上記のコードでは、スライス型は比較できないため、m1 := make(map[[]int]int)
はコンパイルエラーを報告します。構造体A
にはスライスフィールドが含まれているため比較できず、m2 := make(map[A]int)
もコンパイルエラーを報告します。
結論
この記事では、Go言語の==
演算子の詳細な内容を包括的かつ詳細に紹介し、さまざまなデータ型での==
演算子の動作、特殊な型の比較ルール、および比較できない型がもたらす影響について説明しました。この記事の説明を通じて、大多数の開発者がGo言語の==
演算子をより正確かつ深く理解し、適用し、実際のプログラミングでの理解不足によって引き起こされるさまざまな問題を回避できることを願っています。
Leapcell:最高のサーバーレスWebホスティング
最後に、Goサービスをデプロイするのに最適なプラットフォームをお勧めします:Leapcell
🚀 お気に入りの言語で構築
JavaScript、Python、Go、またはRustで楽に開発します。
🌍 無制限のプロジェクトを無料でデプロイ
使用量に応じてのみ支払います。リクエストも料金もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティだけです。
🔹 Twitterでフォローしてください:@LeapcellHQ