DEV Community

Cover image for Deep Dive into Go's Equality Operator
Leapcell
Leapcell

Posted on

1 1 1 1 1

Deep Dive into Go's Equality Operator

Image description

Leapcell: The Best of Serverless Web Hosting

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:

  1. 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).
  2. Composite Types (Aggregate Types): Mainly including arrays and struct types.
  3. Reference Types: Including slices (slice), map, channel, and pointers.
  4. 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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Image description

🚀 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.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ

Top comments (0)

👋 Kindness is contagious

If you found this post helpful, please leave a ❤️ or a friendly comment below!

Okay