Why You Might Be Using Go’s Equality Operator Wrong
Grace Collins
Solutions Engineer · Leapcell

In-depth Analysis of the ==
Operation in Go Language
Overview
In the practice of Go language programming, the ==
equality operation is extremely common. However, when communicating on forums, it is often found that many developers are confused about the results of the ==
operation in Go language. In fact, when Go language deals with the ==
operation, there are many details that need special attention. Although these detailed issues may be encountered less frequently in daily development, once encountered, they may lead to serious program errors. This article will systematically and in-depth elaborate on the relevant content of the ==
operation in Go language, hoping to provide strong assistance to the majority of developers.
Type System
The data types in Go language can be divided into the following four categories:
- Basic Types: Including integer types (such as
int
,uint
,int8
,uint8
,int16
,uint16
,int32
,uint32
,int64
,uint64
,byte
,rune
, etc.), floating-point numbers (float32
,float64
), complex number types (complex64
,complex128
), and strings (string
). - Composite Types (Aggregate Types): Mainly including arrays and struct types.
- Reference Types: Including slices (
slice
),map
,channel
, and pointers. - Interface Types: Such as the
error
interface.
It should be emphasized that the primary prerequisite for the ==
operation is that the types of the two operands must be exactly the same. If the types are different, a compilation error will occur.
It is worth noting that:
- Go language has a strict type system and there is no implicit type conversion mechanism like in C/C++ language. Although this may be a bit cumbersome when writing code, it can effectively avoid a large number of potential errors later.
- In Go language, new types can be defined through the
type
keyword. The newly defined type is different from the underlying type and cannot be directly compared.
To more clearly display the types, the variable definitions in the sample code all explicitly specify the types. For example:
package main import "fmt" func main() { var a int8 var b int16 // Compilation error: invalid operation a == b (mismatched types int8 and int16) fmt.Println(a == b) }
In this code, since the types of a
and b
are different (int8
and int16
respectively), a compilation error will be triggered when trying to perform the ==
comparison.
Another example:
package main import "fmt" func main() { type int8 myint8 var a int8 var b myint8 // Compilation error: invalid operation a == b (mismatched types int8 and myint8) fmt.Println(a == b) }
Here, although the underlying type of myint8
is int8
, they belong to different types, and a direct comparison will also lead to a compilation error.
Specific Behaviors of the ==
Operation under Different Types
Basic Types
The comparison operation of basic types is relatively simple and straightforward, just comparing whether the values are equal. Examples are as follows:
var a uint32 = 10 var b uint32 = 20 var c uint32 = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true
However, when dealing with floating-point number comparisons, special attention should be paid:
var a float64 = 0.1 var b float64 = 0.2 var c float64 = 0.3 fmt.Println(a + b == c) // false
This is because in the computer, some floating-point numbers cannot be accurately represented, and there will be a certain error in the result of floating-point operations. By outputting the values of a + b
and c
respectively, the difference can be clearly seen:
fmt.Println(a + b) fmt.Println(c) // 0.30000000000000004 // 0.3
This problem is not unique to the Go language. Any programming language that follows the IEEE 754 standard may face similar situations when dealing with floating-point numbers. Therefore, in programming, direct floating-point number comparisons should be avoided as much as possible. If a comparison is really necessary, the absolute value of the difference between the two floating-point numbers can be calculated. When this value is less than a set extremely small value (such as 1e - 9
), they can be considered equal.
Composite Types
The composite types (i.e., aggregate types) in Go language are only arrays and structs. For composite types, the ==
operation compares element by element/field by field.
It should be noted that the length of an array is part of its type. Two arrays with different lengths belong to different types and cannot be directly compared.
For arrays, the values of each element will be compared in turn. According to the different types of elements (which may be basic types, composite types, reference types, or interface types), the comparison is judged according to the corresponding type comparison rules. Only when all elements are equal are the two arrays considered equal.
For structs, the values of each field are also compared in turn. According to the four major type categories to which the field types belong, follow the specific type comparison rules. Only when all fields are equal are the two structs equal.
Examples are as follows:
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)
Reference Types
Reference types point to the data they reference indirectly, and the variables store the addresses of the data. Therefore, the ==
comparison of reference types actually determines whether the two variables point to the same piece of data, rather than comparing the actual data content they point to.
Examples are as follows:
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 this example, although the struct values pointed to by aa
and bb
are equal (refer to the comparison rules of composite types above), they point to different struct instances, so aa == bb
is false
; while aa
and cc
point to the same struct, so aa == cc
is true
.
Take channel
as an example:
ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch3 := ch1 fmt.Println(ch1 == ch2) fmt.Println(ch1 == ch3)
Although ch1
and ch2
have the same type, they point to different channel
instances, so ch1 == ch2
is false
; ch1
and ch3
point to the same channel
, so ch1 == ch3
is true
.
Regarding reference types, there are two special regulations:
- Slices are not allowed to be directly compared. Slices can only be compared with the
nil
value. - Maps are not allowed to be directly compared. Maps can only be compared with the
nil
value.
The reason why slices are not allowed to be directly compared is as follows: As a reference type, slices may indirectly point to themselves. For example:
a := []interface{}{ 1, 2.0 } a[1] = a fmt.Println(a) // !!! // runtime: goroutine stack exceeds 1000000000 - byte limit // fatal error: stack overflow
The above code assigns a
to a[1]
, resulting in a recursive reference, which causes a stack overflow error when executing the fmt.Println(a)
statement. If the reference addresses of slices are directly compared, on the one hand, it is very different from the comparison method of arrays and is likely to confuse developers; on the other hand, the length and capacity of slices are part of their types, and it is difficult to determine a unified comparison rule for slices with different lengths and capacities. If the elements inside the slices are compared like arrays, the problem of circular references will be faced. Although this problem can be solved at the language level, the Go language development team believes that it is not worth investing too much effort in this. For the above reasons, Go language clearly stipulates that slice types cannot be directly compared, and using ==
to compare slices will directly lead to a compilation error. For example:
var a []int var b []int // invalid operation: a == b (slice can only be compared to nil) fmt.Println(a == b)
The error message clearly indicates that slices can only be compared with the nil
value.
For the map
type, since its value type may be an uncomparable type (such as a slice), the map
type cannot be directly compared either.
Interface Types
Interface types play an important role in Go language. The value of an interface type, that is, an interface value, consists of two parts: the specific type (that is, the type of the value stored in the interface) and a value of that type. In reference terms, they are called the dynamic type and the dynamic value respectively. The comparison of interface values involves the comparison of these two parts. Only when the dynamic types are exactly the same and the dynamic values are equal (the dynamic values are compared using ==
) are the two interface values equal.
Examples are as follows:
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 this example, the dynamic types of a
and b
are the same (both are int
), and the dynamic values are also the same (both are 1
, which belongs to the comparison of basic types), so a == b
is true
; the dynamic types of a
and c
are the same, but the dynamic values are not equal (1
and 2
respectively), so a == c
is false
; the dynamic types of a
and d
are different (a
is int
and d
is float64
), so a == d
is false
.
Let's look at the situation where a struct is used as an interface value:
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
The dynamic types of aa
and bb
are the same (both are A
), and the dynamic values are also the same (according to the comparison rules of structs in composite types above), so aa == bb
is true
; the dynamic types of aa
and cc
are the same, but the dynamic values are different, so aa == cc
is false
; the dynamic types of dd
and ee
are the same (both are *A
), and the dynamic values use the comparison rules of pointer (reference) types. Since they do not point to the same address, dd == ee
is false
.
It should be noted that if the dynamic value of an interface is uncomparable, forcibly comparing it will cause a panic
. For example:
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)
Here, the dynamic values of a
and b
are of slice type, and the slice type is uncomparable, so executing a == b
will trigger a panic
.
In addition, the comparison of interface values does not require that the interface types (note that it is not the dynamic types) are exactly the same. As long as one interface can be converted to another interface, a comparison can be made. For example:
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // invalid operation: r == w (mismatched types io.Reader and io.Writer) fmt.Println(r == w)
The type of r
is the io.Reader
interface, and the type of rc
is the io.ReadCloser
interface. Looking at the source code, the definition of io.ReadCloser
is as follows:
type ReadCloser interface { Reader Closer }
Since io.ReadCloser
can be converted to io.Reader
, r
and rc
can be compared; while io.Writer
cannot be converted to io.Reader
, so a compilation error will occur.
Types Defined with type
For new types defined based on existing types through the type
keyword, the comparison will be carried out according to their underlying types. For example:
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)
Here, the myint
type is compared according to the underlying type int
, and the arr4
type is compared according to the underlying type [4]int
.
Uncomparability and Its Impact
As mentioned above, the slice type in Go language is uncomparable. The impact of this is that all types that contain slices are also uncomparable. Specifically, these include:
- Array elements are of slice type.
- Structs contain fields of slice type.
- Pointers point to slice types.
Uncomparability is transitive. If a struct is uncomparable because it contains a slice field, then an array with it as an element is uncomparable, and a struct with it as a field type is also uncomparable.
The Relationship between map
and Uncomparable Types
Since the key-value pairs in a map
use the ==
operation for equality judgment, all uncomparable types cannot be used as keys of a map
. For example:
// invalid map key type []int m1 := make(map[[]int]int) type A struct { a []int b string } // invalid map key type A m2 := make(map[A]int)
In the above code, since the slice type is uncomparable, m1 := make(map[[]int]int)
will report a compilation error; the struct A
is uncomparable because it contains a slice field, which also causes m2 := make(map[A]int)
to report a compilation error.
Conclusion
This article comprehensively and in-depth introduced the detailed details of the ==
operation in Go language, covering the behavior of the ==
operation under different data types, the comparison rules of special types, and the impact brought by uncomparable types. It is hoped that through the elaboration of this article, it can help the majority of developers to more accurately and deeply understand and apply the ==
operation in Go language, and avoid various problems caused by insufficient understanding of it in actual programming.
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend a platform that is most suitable for deploying Go services: Leapcell
🚀 Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
🌍 Deploy Unlimited Projects for Free
Only pay for what you use—no requests, no charges.
⚡ Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
🔹 Follow us on Twitter: @LeapcellHQ