GuidesUnderstanding Mathematical Operators in Go

Understanding Mathematical Operators in Go

Using numbers and performing mathematical operations on them is a common practice in any programming language. Numerical data types in Go represent many types of numeric data, ranging from common numbers to more meaningful representations such as currency, screen pixel size, points, geographical locations, time, color codes, and so forth. Therefore to effectively deal with numbers, it is important to understand the basics of how numbers function in Go. This article shows how to work with two of the most common numeric data types: integer and float, with a quick detour covering the concept of numbers in computing.

Read: Getting Started with Go Programming

How Numbers Work in Programming

The base and index are the two important properties of a number. The index – or power (or exponent) – typically denotes how many times the number is to be multiplied. For example: (2 x 2 x 2 = 8) and (8 = 23 ), therefore (superscript) 3 in 23 is called the index.

We usually do not mention or write the base of a number because most of the time, we deal with only one type of number system called decimal numbers, hence the type is obvious. But as we deal with more than one type of number system, it is not apparently clear which number system we are talking about. In computing, four types of number systems are common: decimal, binary, octal, and hexadecimal. Meanwhile, we human beings are accustomed to only one type: decimal.

A decimal number – or floating point number – is represented by 0 – 9, 10 single-digit numbers. Other large digits are nothing but combinations of these 0-9 digits. Similarly, binary has two digits – 0 and 1. Octal has eight numbers, 0-7. Hexadecimal has 16 numbers. Since 10, 11, and so on are double-digit numbers, hexadecimal uses letters A to represent 10, B for 11, until we finally reach F for 15. The first 16 numbers are, therefore, from 0 to F, which in decimal means 0-15.

Now, typically the base of a number is written as subscript: (123)10 means a decimal number, (45)8 means an octal number, (1101)2 means a binary number, and finally (A920CD)16 means a hexadecimal number. Any numbers can be converted from one type to another, but that is beyond the scope of this section.

Two of the most commonly used numeric data types of values used in computing are integers and floats. Below we will quickly define what they mean from the concept of numbers in mathematics:

  • The set of all positive counting numbers {1, 2, 3, …} is known as natural numbers.
  • If we add zero to the set of natural numbers, {0, 1, 2, 3, …}, they are called whole numbers.
  • Integers are a set of all whole numbers plus all the negative natural numbers – {…, -2, -1, 0, 1, 2, 3, …}. An important characteristic of integers is that they are precise numbers and have no rounding off errors.

Another type of number is called floats or floating point numbers, which represent varied precision such as 3.14, 3.147, or -234.56, etc. This is also commonly called decimal numbers.

  • Mathematically, they are p/q numbers where q is not equal to 0 and called rational numbers.
  • As opposed to rational numbers, there are irrational numbers, which have endless non-repeating digits after the decimal point, such as the value of PI or sqrt(3) or sqrt(2).
  • The rational and irrational numbers together form the set of real numbers.

In computer programming, it is sufficient to have two data types: integer for all positive and negative precise numbers and floats to represent all positive and negative imprecise or fractional numbers or real numbers.

How Do Computers Store Data?

Computer memory uses bit patterns to store data. It is entirely up to the programmer to decide what each bit pattern means in the context of the program. It may be a number, a character, or any other object. This interpretation of a binary pattern is called data representation or encoding. Therefore, numbers are also a piece of data represented by a fixed number of bits.

An n-bit storage can represent 2n distinct entities. For example a 3-bit storage can store eight distinct binary patterns: 000, 001, 010, 011, 100, 101, 110, 111. This means we represent numbers 0 to 7 or characters ‘A’ to ‘H’ or any other eight distinct things.

An integer is typically represented as 8-bit, 16-bit, 32-bit, or 64-bit values. This choice imposes a constraint on the range of values that data types can represent. Besides bit-length, the representation scheme, such as signed or unsigned integers, has an impact on the range of values. For example, an 8-bit unsigned integer has a range of 0 to 255, while an 8-bit signed integer has a range of -128 to 127. Note that both signed and unsigned integers, in this case, represent 256 distinct numbers.

Floating point numbers, although represented by bit patterns like everything else, are a bit tricky. Let’s say we use 32 bits to represent a real number as well as an integer. An integer being equally spaced (if we put in a number line), the maximum value that can be stored is 232. However, there can be infinite floating-point numbers between two integers (hence the concept of precision is important for floats. Read IEE754 for more details).

A minor change in the precision value, such as from 2.34567 to 2.34566, may not be representable because the spacing between the represented value at the lower end of the number line towards 0 is very small, and it increases or decreases as we move between the positive or negative direction in the number line. In cases of integer and float, the 32 bits store is used to hold numbers, but it is just that the numbers are unequally spaced in floating-point, which allows its range to be more even with limited precision than that of integers represented by 32 bits. However, the true nature of real numbers cannot be replicated in digital computers. Floating-point representation is our best shot at mimicking this.

Learn all about Working with Strings in Go.

What are Integer Types in Go?

Two of the most commonly used numeric data types in Go are integers and floats. Go represents integers of four distinct sizes – 8, 16, 32, and 64 bits. Go integers:

  • Are represented as int8[-27,27-1], int16[-215, 215-1], int32[−231, 231-1] and int64[−263, 263-1] respectively. They are all signed integers.
  • Have corresponding unsigned versions: uint8[0, 28-1,], uint16[0, 216-1], uint32[0, 232-1] and uint64[0, 264-1].
  • If we do not specify the bit-size and simply write int or uint, then they can either mean 32 or 64 bits integers or unsigned-integers, respectively, depending upon the compiler.
  • There is another type called rune, which is an alias of int32, conventionally used to represent Unicode code points.
  • Similarly, the byte data type is an alias of uint8.
  • There is an unsigned integer type called uintptr whose bit value is unspecified. It is used to hold bits of pointer value and is typically used for low-level programming.
  • A variable of a type that cannot store a higher number is silently truncated.

The signed numbers are represented in 2’s complement form, where higher-order bits are reserved as sign-bit. Unsigned integers use a full range of bits.

What are Floating Point Types in Go?

There are two types of floating-point numbers represented as float32 and float64.

  • In the math package, there is constant called math.MaxFloat32 which denotes the largest float32 value that the variable of this type can hold.
  • Similarly, there is another constant called math.float64 for 64-bit size.
  • A float32 value provides approximate precision up to 6 decimal places.
  • A float64 value provides approximate precision up to 15 decimal places.

Operators and Order of Precedence in Go

According to the decreasing order of precedence, math, logic, and comparison related operators in Go can be arranged as follows:

  • Level 5: *(Mul), /(Div), %(Modulo), <<(Left-shift), >>(Right-shift), &(bitwise AND), &^(bitwise clear AND NOT)
  • Level 4: +(Add), -(Sub), |(bitwise OR), ^(bitwise XOR)
  • Level 3: ==(Equal to), !=(Not equal to), <(Less than), <=(Less equal), >(Greater than), >=(Greater equal)
  • Level 2: &&(logical AND)
  • Level 1: ||(Logical OR)

Operators at the same level have left-to-right associativity. Therefore, appropriate parenthesis must be used for clarity. Note that the %(Modulo) operator applies only to integers, and when used with negative integers such as -7%3, the sign of the remainder is the same as that of the sign of the dividend. The operators in levels (5 to 1) have decreasing order of precedence, while operators in the same level have the same order of precedence.

However, if an expression has multiple operators of the same precedence, the order of calculation is from left to right. Therefore, an expression (1+2-3+4) results in 4 and not -4. Be cautious while applying the familiar BODMAS or PEMDAS rule. There can be incorrect interpretations due to them. Of course, one can use parenthesis properly to alleviate the confusion.

Mathematical Operators in Go

The addition and subtraction operators in Go are similar to those in mathematics. Let’s run through a few quick examples of using mathematical operators in Go:

package main

import "fmt"

func main() {

	intX := 20
	intY := 50
	fmt.Println(intX + intY) //also, fmt.Println(intX - intY)
	intY = -10 //integers can be negative, using unary operator
	fmt.Println(intX + intY) //also, fmt.Println(intX - intY)

	floatX := 5.67
	floatY := 2.56
	//To properly format the output to 2 decimal place, we can use fmt.Printf
	fmt.Printf("%.2f", floatX+floatY) // fmt.Printf("%.2f", floatX+floatY)
	floatY = -1.24 //floats can be negative too
	fmt.Printf("\n%.2f", floatX+floatY) //fmt.Printf("%.2f", floatX+floatY)
}

The above code would result in the following output if run in your integrated development environment (IDE) or code editor:

70
10
8.23
4.43

Now, what happens if we try to calculate using mismatched data types as follows:

fmt.Printf("\n%.2f", intX+floatY) //error 
fmt.Printf("\n%d", intX+floatY) //error

This is not allowed in Go. However, we may cast the variable to its corresponding type and do the calculation without any error:

fmt.Printf("\n%.2f", float64(intX)+floatY) //casting in to float
fmt.Printf("\n%d", intX+int(floatY)) //casting float to int       

But note that typecasting a variable to another type may result in lost information. For example, casting a float to an int variable rounds it off to its nearest integer. The fractional values are truncated. So be cautious while typecasting one variable to another type.

Multiplication and Division Operators in Go

Multiplication and division operators in Go are also similar to their mathematical equivalents. Here is a quick example:

package main

import "fmt"

func main() {

	intX := 56
	intY := 12
	fmt.Println(intX * intY)
	fmt.Println(intX / intY)

	floatX := 5.67
	floatY := 2.56
	fmt.Printf("%.2f", floatX*floatY)
	fmt.Printf("\n%.2f", floatX/floatY)
}

Running this code in your IDE or code editor results in:

672
4
14.52
2.21

Note that the division operator / in Go performs floor division in the case of an integer, which means the fractional part of the division result (quotient) is rounded off to its nearest floor (integer) value. A division typically has two integer parts as its result: quotient and remainder. The division /, operator returns the quotient while the modulo %, operator returns the remainder after division. Check out the following example:

package main

import "fmt"

func main() {

	intX := 45
	intY := 7
	fmt.Printf("%d / %d = %d (Quotient)\n", intX, intY, (intX / intY))
	fmt.Printf("%d %% %d = %d (Remainder)\n", intX, intY, (intX % intY))
}

The output:

45 / 7 = 6 (Quotient)
45 % 7 = 3 (Remainder)

The Mod Function in Go

Can we do modulus with float data types? The answer is yes, but not with the modulus % operator. We have to use the Mod function from the math package. Here is a quick example:

package main

import (
	"fmt"
	"math"
)

func main() {
	floatX := 57.0
	floatY := 7.0
	fmt.Printf("%.2f Mod %.2f = %.2f\n", floatX, floatY, math.Mod(floatX, floatY))
}

The output would be:

57.00 Mod 7.00 = 1.00 

Apart from these, there are assignment operators = and shorthand operators written as x+=5, which essentially means x=x+5. The shorthand operator can be applied to all level 5 and level 4 operators such as x*=5(means, x=x*5), x&^=3(means, x=x&^3), x^=6(means, x=x^6), etc.

Learn about other Methods in Go.

Bitwise Operators in Go

The bitwise operators are used for bit calculation. Here is a quick example of how to use them:

package main

import (
	"fmt"
)

func main() {

	var x uint8 = 10<<2 | 9<<3
	var y uint8 = 16<<3 | 8<<2
	fmt.Printf("x = %08b\n", x)
	fmt.Printf("y = %08b\n", y)
	fmt.Printf("x&y = %08b\n", x&y) //AND
	fmt.Printf("x|y = %08b\n", x|y) //OR
	fmt.Printf("x^y = %08b\n", x^y) //XOR
	fmt.Printf("x&^y = %08b\n", x&^y) //AND NOT
	fmt.Printf("x<<2 = %08b\n", x<<2) //Left-shift fmt.Printf("x>>1 = %08b\n", x>>1) //Right-shift
}

The output:

x = 01101000
y = 10100000
x&y = 00100000
x|y = 11101000
x^y = 11001000
x&^y = 01001000
x<<2 = 10100000 x>>1 = 00110100

Working Big Numbers in Python

On some occasions, we may need to perform mathematical operations on numbers that exceed the limit of int64/uint64 float64 type. The Go standard library provides three unlimited accuracy numeric types for integers called big.Int for signed integers, big.Rat for rational numbers and big.Float for floating-point numbers.

The big.Rat (rational) only can store rational numbers. Irrational numbers such as the value of PI, e, or sqrt(3), etc., cannot be stored in big.Rat. Big numbers can be of any length (limited only by memory), and their calculation is a lot slower than the built-in primitive types. For mathematical calculations, we cannot use the usual operators. Instead, we have to use methods such as Add(), Mul(), and so forth. Here is a quick and very short example showing how to use Big numbers in Go:

package main

import (
	"fmt"
	"math/big"
)

func main() {

	bigInt1 := big.NewInt(int64(20)) //NewInt(x) returns an *Int set to the value of the int64 argument x
	bigInt2 := big.NewInt(int64(30))
	bigFloat1 := big.NewFloat(8.3983) 
		//NewFloat(x) returns a *Float initialized to the float64 argument x
	bigFloat2 := big.NewFloat(7.98327643)
	bigRat1 := big.NewRat(22, 7) 
			// NewRat(x, y) returns a *Rat set to the fraction 
			// x/y where x and y are int64 values
	bigRat2 := big.NewRat(67, 20)

	fmt.Println(bigInt1.Add(bigInt1, bigInt2))
	fmt.Println(bigFloat1.Mul(bigFloat1, bigFloat2))
	fmt.Println(bigRat1.Sub(bigRat1, bigRat2))
}

The output of this code if you type it in your code editor and run it would be:

50
67.04595044206901
-29/140

Final Thoughts on Mathematical Operators in Go

Here we have given a quick tour on the application of mathematical operators and numeric data types in Go, focusing on integer and float types only. Go provides a lot more in the standard library to deal with numbers and the mathematical part of its use. We often do not deal with numbers that exceed 64-bit space. Integers and floats suffice most of our needs in Go, unless we are performing some scientific calculations.

Scientific calculation sometimes requires big numbers that must be exact in their calculation. Go has a provision for that too in the standard library. The use of big numbers may be rare, but they have their uses. On most occasions, however, primitive data types should be made the basis of all calculations because they are a lot faster. Also, remember that for efficiency’s sake, integers are always better than floating-point numbers.

Latest Posts

Related Stories