Warum Sie Go’s Gleichheitsoperator falsch verwenden könnten
Wenhao Wang
Dev Intern · Leapcell

Eingehende Analyse der ==
Operation in Go
Überblick
In der Praxis der Go-Sprachenprogrammierung ist die ==
Gleichheitsoperation äußerst gebräuchlich. Bei der Kommunikation in Foren stellt man jedoch oft fest, dass viele Entwickler über die Ergebnisse der ==
Operation in Go-Sprache verwirrt sind. Tatsächlich gibt es bei der Bearbeitung der ==
Operation in Go-Sprache viele Details, die besondere Aufmerksamkeit erfordern. Obwohl diese detaillierten Probleme im täglichen Entwicklungsablauf seltener auftreten, können sie, sobald sie auftreten, zu schwerwiegenden Programmfehlern führen. Dieser Artikel wird systematisch und eingehend auf die relevanten Inhalte der ==
Operation in Go-Sprache eingehen und hofft, der Mehrheit der Entwickler eine starke Unterstützung zu bieten.
Typsystem
Die Datentypen in Go-Sprache lassen sich in die folgenden vier Kategorien einteilen:
- Grundtypen: Einschließlich Integer-Typen (wie
int
,uint
,int8
,uint8
,int16
,uint16
,int32
,uint32
,int64
,uint64
,byte
,rune
usw.), Gleitkommazahlen (float32
,float64
), komplexe Zahlentypen (complex64
,complex128
) und Strings (string
). - Zusammengesetzte Typen (Aggregattypen): Hauptsächlich Arrays und Struct-Typen.
- Referenztypen: Einschließlich Slices (
slice
),map
,channel
und Pointer. - Interface-Typen: Wie das
error
Interface.
Es sollte betont werden, dass die primäre Voraussetzung für die ==
Operation ist, dass die Typen der beiden Operanden genau gleich sein müssen. Wenn die Typen unterschiedlich sind, tritt ein Kompilierungsfehler auf.
Es ist erwähnenswert, dass:
- Go-Sprache ein strenges Typsystem hat und es keinen impliziten Typkonvertierungsmechanismus wie in C/C++ gibt. Obwohl dies beim Schreiben von Code etwas umständlich sein kann, kann es effektiv eine große Anzahl potenzieller Fehler später vermeiden.
- In Go-Sprache können neue Typen über das
type
Schlüsselwort definiert werden. Der neu definierte Typ unterscheidet sich vom zugrunde liegenden Typ und kann nicht direkt verglichen werden.
Um die Typen deutlicher darzustellen, geben die Variablendefinitionen im Beispielcode alle explizit die Typen an. Zum Beispiel:
package main import "fmt" func main() { var a int8 var b int16 // Kompilierungsfehler: ungültige Operation a == b (nicht übereinstimmende Typen int8 und int16) fmt.Println(a == b) }
In diesem Code wird, da die Typen von a
und b
unterschiedlich sind (int8
bzw. int16
), ein Kompilierungsfehler ausgelöst, wenn versucht wird, den ==
Vergleich durchzuführen.
Noch ein Beispiel:
package main import "fmt" func main() { type int8 myint8 var a int8 var b myint8 // Kompilierungsfehler: ungültige Operation a == b (nicht übereinstimmende Typen int8 und myint8) fmt.Println(a == b) }
Hier gehören myint8
, obwohl der zugrunde liegende Typ int8
ist, zu verschiedenen Typen, und ein direkter Vergleich führt ebenfalls zu einem Kompilierungsfehler.
Spezifische Verhaltensweisen der ==
Operation unter verschiedenen Typen
Grundtypen
Die Vergleichsoperation von Grundtypen ist relativ einfach und unkompliziert, wobei nur verglichen wird, ob die Werte gleich sind. Beispiele sind wie folgt:
var a uint32 = 10 var b uint32 = 20 var c uint32 = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true
Beim Umgang mit Gleitkommazahlenvergleichen ist jedoch besondere Aufmerksamkeit geboten:
var a float64 = 0.1 var b float64 = 0.2 var c float64 = 0.3 fmt.Println(a + b == c) // false
Dies liegt daran, dass im Computer einige Gleitkommazahlen nicht genau dargestellt werden können und es ein gewisser Fehler im Ergebnis von Gleitkommaoperationen auftritt. Durch die Ausgabe der Werte von a + b
bzw. c
kann der Unterschied deutlich erkannt werden:
fmt.Println(a + b) fmt.Println(c) // 0.30000000000000004 // 0.3
Dieses Problem ist nicht einzigartig für die Go-Sprache. Jede Programmiersprache, die dem IEEE 754-Standard folgt, kann bei der Arbeit mit Gleitkommazahlen auf ähnliche Situationen stoßen. Daher sollten in der Programmierung direkte Gleitkommazahlenvergleiche so weit wie möglich vermieden werden. Wenn ein Vergleich wirklich erforderlich ist, kann der absolute Wert der Differenz zwischen den beiden Gleitkommazahlen berechnet werden. Wenn dieser Wert kleiner als ein festgelegter extrem kleiner Wert (wie 1e - 9
) ist, können sie als gleich betrachtet werden.
Zusammengesetzte Typen
Die zusammengesetzten Typen (d. h. Aggregattypen) in Go-Sprache sind nur Arrays und Strukturen. Für zusammengesetzte Typen vergleicht die ==
Operation Element für Element/Feld für Feld.
Es ist zu beachten, dass die Länge eines Arrays Teil seines Typs ist. Zwei Arrays mit unterschiedlicher Länge gehören zu unterschiedlichen Typen und können nicht direkt verglichen werden.
Für Arrays werden die Werte jedes Elements der Reihe nach verglichen. Je nach den verschiedenen Elementtypen (die Grundtypen, zusammengesetzte Typen, Referenztypen oder Interface-Typen sein können) wird der Vergleich nach den entsprechenden Typvergleichsregeln beurteilt. Nur wenn alle Elemente gleich sind, werden die beiden Arrays als gleich betrachtet.
Für Strukturen werden auch die Werte jedes Feldes der Reihe nach verglichen. Entsprechend den vier Haupttypenkategorien, zu denen die Feldtypen gehören, sind die spezifischen Typvergleichsregeln zu befolgen. Nur wenn alle Felder gleich sind, sind die beiden Strukturen gleich.
Beispiele sind wie folgt:
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)
Referenztypen
Referenztypen verweisen indirekt auf die Daten, auf die sie verweisen, und die Variablen speichern die Adressen der Daten. Daher bestimmt der ==
Vergleich von Referenztypen tatsächlich, ob die beiden Variablen auf dasselbe Datenelement verweisen, anstatt den tatsächlichen Dateninhalt zu vergleichen, auf den sie verweisen.
Beispiele sind wie folgt:
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)
In diesem Beispiel sind, obwohl die von aa
und bb
verwiesenen Strukturwerte gleich sind (siehe die Vergleichsregeln für zusammengesetzte Typen oben), sie auf verschiedene Strukturinstanzen verweisen, sodass aa == bb
false
ist; während aa
und cc
auf dieselbe Struktur verweisen, sodass aa == cc
true
ist.
Nehmen wir channel
als Beispiel:
ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch3 := ch1 fmt.Println(ch1 == ch2) fmt.Println(ch1 == ch3)
Obwohl ch1
und ch2
denselben Typ haben, verweisen sie auf verschiedene channel
Instanzen, sodass ch1 == ch2
false
ist; ch1
und ch3
verweisen auf denselben channel
, sodass ch1 == ch3
true
ist.
In Bezug auf Referenztypen gibt es zwei besondere Bestimmungen:
- Slices dürfen nicht direkt verglichen werden. Slices können nur mit dem Wert
nil
verglichen werden. - Maps dürfen nicht direkt verglichen werden. Maps können nur mit dem Wert
nil
verglichen werden.
Der Grund, warum Slices nicht direkt verglichen werden dürfen, ist wie folgt: Als Referenztyp können Slices indirekt auf sich selbst verweisen. Zum Beispiel:
a := []interface{}{ 1, 2.0 } a[1] = a fmt.Println(a) // !!! // runtime: goroutine stack exceeds 1000000000 - byte limit // fatal error: stack overflow
Der obige Code weist a
a[1]
zu, was zu einem rekursiven Verweis führt, der beim Ausführen der fmt.Println(a)
Anweisung einen Stack Overflow-Fehler verursacht. Wenn die Referenzadressen von Slices direkt verglichen werden, ist dies einerseits sehr unterschiedlich von der Vergleichsmethode von Arrays und es ist wahrscheinlich, dass Entwickler verwirrt werden; Andererseits sind die Länge und Kapazität von Slices Teil ihrer Typen, und es ist schwierig, eine einheitliche Vergleichsregel für Slices mit unterschiedlichen Längen und Kapazitäten festzulegen. Wenn die Elemente innerhalb der Slices wie Arrays verglichen werden, liegt das Problem der zirkulären Verweise vor. Obwohl dieses Problem auf Sprachebene gelöst werden kann, ist das Go-Sprachentwicklungsteam der Ansicht, dass es sich nicht lohnt, zu viel Aufwand in dieses Problem zu investieren. Aus den oben genannten Gründen legt die Go-Sprache eindeutig fest, dass Slice-Typen nicht direkt verglichen werden können, und die Verwendung von ==
, um Slices zu vergleichen, führt direkt zu einem Kompilierungsfehler. Zum Beispiel:
var a []int var b []int // ungültige Operation: a == b (Slice kann nur mit nil verglichen werden) fmt.Println(a == b)
Die Fehlermeldung gibt deutlich an, dass Slices nur mit dem Wert nil
verglichen werden können.
Da für den Typ map
sein Werttyp ein unvergleichlicher Typ (z. B. ein Slice) sein kann, kann der Typ map
auch nicht direkt verglichen werden.
Interface-Typen
Interface-Typen spielen in Go-Sprache eine wichtige Rolle. Der Wert eines Interface-Typs, d. h. ein Interface-Wert, besteht aus zwei Teilen: dem spezifischen Typ (d. h. dem Typ des im Interface gespeicherten Werts) und einem Wert dieses Typs. In Bezug auf Referenzen werden sie als dynamischer Typ bzw. dynamischer Wert bezeichnet. Der Vergleich von Interface-Werten umfasst den Vergleich dieser beiden Teile. Nur wenn die dynamischen Typen genau gleich sind und die dynamischen Werte gleich sind (die dynamischen Werte werden mit ==
verglichen), sind die beiden Interface-Werte gleich.
Beispiele sind wie folgt:
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
In diesem Beispiel sind die dynamischen Typen von a
und b
gleich (beide sind int
), und die dynamischen Werte sind ebenfalls gleich (beide sind 1
, was zum Vergleich von Grundtypen gehört), sodass a == b
true
ist; die dynamischen Typen von a
und c
sind gleich, die dynamischen Werte sind jedoch nicht gleich (1
bzw. 2
), sodass a == c
false
ist; die dynamischen Typen von a
und d
sind unterschiedlich (a
ist int
und d
ist float64
), sodass a == d
false
ist.
Betrachten wir die Situation, in der eine Struktur als Interface-Wert verwendet wird:
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
Die dynamischen Typen von aa
und bb
sind gleich (beide sind A
), und die dynamischen Werte sind ebenfalls gleich (gemäss den Vergleichsregeln für Strukturen in zusammengesetzten Typen oben), sodass aa == bb
true
ist; die dynamischen Typen von aa
und cc
sind gleich, die dynamischen Werte sind jedoch unterschiedlich, sodass aa == cc
false
ist; die dynamischen Typen von dd
und ee
sind gleich (beide sind *A
), und die dynamischen Werte verwenden die Vergleichsregeln für Zeiger(Referenz)-Typen. Da sie nicht auf dieselbe Adresse verweisen, ist dd == ee
false
.
Es ist zu beachten, dass, wenn der dynamische Wert eines Interface nicht vergleichbar ist, das erzwungene Vergleichen einen Panic
verursacht. Zum Beispiel:
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)
Hier sind die dynamischen Werte von a
und b
vom Slice-Typ, und der Slice-Typ ist unvergleichbar, sodass die Ausführung von a == b
einen Panic
auslöst.
Darüber hinaus erfordert der Vergleich von Interface-Werten nicht, dass die Interface-Typen (beachten Sie, dass es sich nicht um die dynamischen Typen handelt) genau gleich sind. Solange ein Interface in ein anderes Interface konvertiert werden kann, kann ein Vergleich durchgeführt werden. Zum Beispiel:
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // ungültige Operation: r == w (nicht übereinstimmende Typen io.Reader und io.Writer) fmt.Println(r == w)
Der Typ von r
ist das io.Reader
Interface, und der Typ von rc
ist das io.ReadCloser
Interface. Wenn wir uns den Quellcode ansehen, ist die Definition von io.ReadCloser
wie folgt:
type ReadCloser interface { Reader Closer }
Da io.ReadCloser
in io.Reader
konvertiert werden kann, können r
und rc
verglichen werden; während io.Writer
nicht in io.Reader
konvertiert werden kann, tritt ein Kompilierungsfehler auf.
Mit type
definierte Typen
Für neue Typen, die auf der Grundlage vorhandener Typen über das Schlüsselwort type
definiert werden, wird der Vergleich gemäss ihren zugrunde liegenden Typen durchgeführt. Zum Beispiel:
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)
Hier wird der Typ myint
gemäss dem zugrunde liegenden Typ int
verglichen, und der Typ arr4
wird gemäss dem zugrunde liegenden Typ [4]int
verglichen.
Unvergleichbarkeit und ihre Auswirkungen
Wie oben erwähnt, ist der Slice-Typ in Go-Sprache unvergleichbar. Die Auswirkung davon ist, dass alle Typen, die Slices enthalten, ebenfalls unvergleichbar sind. Insbesondere umfassen diese:
- Array-Elemente sind vom Slice-Typ.
- Strukturen enthalten Felder vom Slice-Typ.
- Zeiger verweisen auf Slice-Typen.
Unvergleichbarkeit ist transitiv. Wenn eine Struktur unvergleichbar ist, weil sie ein Slice-Feld enthält, dann ist ein Array mit ihr als Element unvergleichbar, und eine Struktur mit ihr als Feldtyp ist ebenfalls unvergleichbar.
Die Beziehung zwischen map
und unvergleichlichen Typen
Da die Schlüssel-Wert-Paare in einer map
die ==
Operation zur Gleichbeitsbeurteilung verwenden, können alle unvergleichlichen Typen nicht als Schlüssel einer map
verwendet werden. Zum Beispiel:
// ungültiger Map-Schlüsseltyp []int m1 := make(map[[]int]int) type A struct { a []int b string } // ungültiger Map-Schlüsseltyp A m2 := make(map[A]int)
Im obigen Code wird, da der Slice-Typ unvergleichbar ist, m1 := make(map[[]int]int)
einen Kompilierungsfehler melden; die Struktur A
ist unvergleichbar, weil sie ein Slice-Feld enthält, was auch dazu führt, dass m2 := make(map[A]int)
einen Kompilierungsfehler meldet.
Schlussfolgerung
Dieser Artikel hat umfassend und eingehend die detaillierten Details der ==
Operation in Go-Sprache vorgestellt und das Verhalten der ==
Operation unter verschiedenen Datentypen, die Vergleichsregeln für spezielle Typen und die Auswirkungen von unvergleichlichen Typen behandelt. Es wird gehofft, dass durch die Ausarbeitung dieses Artikels die Mehrheit der Entwickler die ==
Operation in Go-Sprache genauer und umfassender verstehen und anwenden und verschiedene Probleme vermeiden kann, die durch unzureichendes Verständnis in der tatsächlichen Programmierung verursacht werden.
Leapcell: Das Beste aus Serverlosem Webhosting
Abschliessend möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Go-Diensten eignet: Leapcell
🚀 Entwickeln Sie mit Ihrer Lieblingssprache
Entwickeln Sie mühelos in JavaScript, Python, Go oder Rust.
🌍 Stellen Sie unbegrenzt Projekte kostenlos bereit
Bezahlen Sie nur das, was Sie verbrauchen – keine Anforderungen, keine Gebühren.
⚡ Pay-as-You-Go, keine versteckten Kosten
Keine Leerlaufgebühren, nur nahtlose Skalierbarkeit.
📖 Entdecken Sie unsere Dokumentation
🔹 Folgen Sie uns auf Twitter: @LeapcellHQ