Understanding Go Syntax and Language Features

Understanding Go Syntax and Language Features

A comprehensive introduction to syntax, variables, data types, and control structures in Golang.

In this article, I will be introducing you to the basic foundation of reading and writing GoLang. Go is one of the simplest programming languages, which is one of the many reasons many developers love the language so much (find out other reasons why Go is so popular here). Go programs are stored in a text file with a .go extension, e.g main.go.

Programming is just a sequence of written instructions for a computer, and large applications are built by the composition of tiny and simple other instructions. Understanding how to create and combine these tiny instructions is the foundation of programming. Let us start from the basics and build our way up.

Variables

A variable serves as a storage location on the computer, identified by a name for easy reference. and since it is a storage, it can hold a quantity of data or object of a particular type. For example, if we need to store the age of a person in memory, we can do so like this,

var age int = 20

In this case, age is the name of the variable, the type (we are coming to this) of data that the variable contains is int (which means integer) and 20 is the value that will be stored in the variable.

Or to store the value of a complex calculation (not really), we can do this, for example

var total int =  1 + 2 + 3 + 4 + 5

Declaring a value in GoLang often follows the following format,

var name type = expression

The var is a keyword in Golang to tell the compiler that we are creating a variable, the name is the reference to which we want our variable to be known and accessed, the type is the type of data the variable can hold, the = is the assignment operator, and the expression is the value to be stored in the variable. If the expression is not provided, the variable is created and initialized with the zero value of the type. A zero value simply means a default value.

For instance, if we declare our variable like so,

var age int

we did not specify any value to be stored in the variable, so Go creates the variable and sets its value to 0, which is the default value for number types.

Zero values for different types

  1. 0 for numbers

  2. false for booleans

  3. “” for strings (note, “” is an empty string)

  4. nil for interfaces and reference types (we are coming to this)

When creating multiple variables of the same type, you can efficiently do so on a single line, as demonstrated below:

var mean, median, mode, total, count int

As always, the var is the keyword for declaration, mean, median, mode, total, count are the variables to be created, and they are all of the same type as the type declared at the end of the line, which is int in this case. And remember, since we did not include any expression, they are all created with their zero values, so they all have the value of 0.

If perhaps we know the data ahead, we can create them in one line by providing the values separated by commas in the exact order in which the variables are created, like below,

var mean, median, mode, total, count = 20, 15, 30, 120, 10

// 20 is assigned to mean, 15 to median, and so on, in that order

If you want to create many variables of different types in one line, you can do so like this,

var name, age, sex, isPresent = "John", 12, "Male", true

// "John" is assigned to name, 12 to age, "Male" to sex, and true to isPresent

Multiple variables can also be created in the format below:

var (
        name      string
        age       int
        sex       string = "Male"
        isPresent bool = false
    )

And just like we have established, remember that if we don’t define an expression for the variable, it is created with the zero value. So in this case, name and age are created with zero values while sex and isPresent have the values “Male” and false respectfully.

Please note that expression can also be the output of a function (think of a function as a group of common code executing a particular task). Let’s say we have a function that computes the square of a number, we can store the result of this function when we calculate the square of 5 in a variable,

package main

import "fmt"

func main() {

    var squareOfFive int = Square(5)

    fmt.Println(squareOfFive)
}

// this is a function, we will discuss it in details in the next article
func Square(number int) int {
    return number * number
}

In some cases, especially if you want the variable to automatically be the type of the expression, you don’t need to explicitly specify the type, you can omit it, and Go will automatically infer the type of the variable from the type of value of the expression. So in this case of,

var squareOfFive int = Square(5)

we can simply omit the int and the variable will automatically assume the type of the returned data, like so:

// squareOfFive will automatically assume the type 
// of the data retured by the Square method, which is int in this case
var squareOfFive = Square(5)

This may be useful in some cases, but most times, you always want to declare the type of the variable, so your code can be simple, and readable.

There is an alternate way of creating a variable in GoLang, also known as a short variable declaration. This form of declaration is only permissible within a function and only used to declare local variables within the scope of the function. It takes the form:

// take not of the symbol
variableName := expression 

// the syntax is a semicolon followed by equals, :=

This method of variable declaration is prevalent and the syntax := is often confusing for people who are not familiar with Golang syntax. Let’s see it in an example, suppose we want to write a function that returns the average of 5 numbers, we can decide that we will compute the total of the 5 numbers and put it in a variable, then return the division of that variable by the count:

func AverageofFiveNumbers(a,b,c,d,e int) int {
    total := a + b + c + d + e
    return total / 5
}

// you cannot do this outside of a function, it will throw an error
total:= 20

This function is purely for demonstration and you will probably never be writing simple code like this in production. As you can see, we omitted the var keyword when we declared the total variable, other rules of variables apply to this too, as it is simply a variable too, but just created with another syntax (and remember that you can only use this := syntax in a function).

Variables are mutable and reassignable!

After we create a variable, we can overwrite the data later in the code to hold new information, as long as the new information is of the same type as the old value. For instance, after calculating the squareOfFive in the code below, we can overwrite it later in the code, to hold the wrong data.

package main

import "fmt"

func main() {

    var squareOfFive = Square(5)

    fmt.Println(squareOfFive) // 25 is printed

    squareOfFive = 10

    fmt.Println(squareOfFive) // 10 is printed
}

func Square(number int) int {
    return number * number
}

If you run the code, you will see that the value squareOfFive is overwritten with the wrong data. For this reason, variables are used to hold data that can be manipulated in the program (hence the name, it is not fixed).

Constants

We have already established that variables can be overwritten and changed in the code, whether intentionally or even by mistake, which is a very good thing, because most times, we make things work by manipulating the variables and changing their values at runtime. However, there are times when we need data storage to be fixed and immutable, i.e. it should be constant.

For example, if we are running a mathematical calculation that involves Pi , and some other mathematical constants, we do not want our code to be able to change their values, whether willingly or by mistake. In that case, we use, well, CONSTANTS.

Constants are data storage marked as immutable and are created at the program compile time. They can only be numbers, characters, strings, or booleans. And because they are constants, the expressions that define them must also be constant and can be evaluated by the compiler.

To create a constant, use the keyword const , e,g:

const speedOfLight = 299792458 // m/s

It follows the same format as when creating a variable, just replace the var keyword with const. If we try to mutate the value of the constant speedOfLight later in our code, we will get an error and our code will not compile.

If we need to create multiple constants, we can do this:

const speedOfLight, speedOfSound = 299792458, 343

// or 

const (
    speedOfLight = 299792458
    speedOfSound = 343
)

But remember that constants cannot be mutated, so we cannot have zero values or use any expression that implies that. Every value of the constant must be known at compile time.

There is also an iota keyword that is used to enumerate constants. For example, if we need to declare the days of the week with an index, starting with Sunday at index 0, we can use iota to make things easier like so:

const (
        SUNDAY int = iota
        MONDAY
        TUESDAY
        WEDNESDAY
        THURSDAY
        FRIDAY
        SATURDAY
)

In this code, the iota assigned the value 0 to Sunday, then increments to 1 for Monday, 2 for Tuesday, and so on. This can be very useful if we just need to quickly enumerate and increment some constants by a value.

Data Types

We have established that data type is the type of value that is stored in a variable. It is how we know about the data we have and how we can interact with the data.

In Golang, there are four categories of data types:

  1. Primary types: Primary types are the data types that are pre-defined and built into the language and are the fundamental building blocks of the language. Examples are integers, strings, and booleans.

  2. Aggregate types: Aggregate types, as the name implies, are data types that combine simple primary data types to form complex ones. Examples are arrays and structs.

  3. Reference types: Reference types are data types that are used for dynamic management and accessibility of data locations and addresses in memory. Examples include pointers, slices, maps, and channels.

  4. Interface types: Interface types are used to express generalization or abstraction about the behaviors of other types.

Let us dive into the primary data types in Golang, we will look at the other advanced types later in future articles.

Integers

Numbers are very important in programming and all languages provide ways to work with different types of numbers for different use cases. In Go, there are several sizes of integers, floating point numbers, and complex numbers to work with.

Go provides both signed and unsigned integer arithmetic. Signed integers simply mean the type can represent both positive and negative numbers, while unsigned integers mean that the type can only represent non-negative numbers. Signed integers are represented by int in Go while unsigned integers are represented by uint. int is the most used integer type and so it is often the recommended choice for integers.

Sizes of integers

  1. int8 - 8 bits

  2. int16 - 16 bits

  3. int32 - 32 bits

  4. int64 - 64 bits

  5. uint8 - 8 bits

  6. uint16 - 16 bits

  7. uint32 - 32 bits

  8. uint64 - 64 bits

The bits specify the range of the storage capacity of each type, the higher the size of the bit, the more number the type can hold. There is also a type rune which is often used in place of int32.

Many operations can be performed on integers, some of them are addition +, subtraction -, multiplication * , division / , remainder %, and comparison operations (compares the values of left and right and returns a boolean) e.g equals ==, not equal !=, less than <, greater than > and so on. There are also operations like +=, -=, *= which means performing this operation with the expression, and then assigning the result as the new data. For instance:

var total = 0

total += 10

// which is same as
total = total + 10

total -= 10

// which is same as
total = total - 10

total *= 10

// which is samea s
total = total * 10

Floating-Point Numbers

Floating points numbers are simply decimal numbers, and Go provides two data types to work with them - float32 and float64. The difference between these two types is also the data capacity. While the float32 provides approximately six decimal digits of precision, the float64 provides 15 digits, so it is often used for most floating number applications.

const pi = 3.141592653589793
const discount = .3 // notice that the 0 is omitted, this is permissible in Go.

You can also write floating point numbers with scientific notation,

const r = 3.456e11

Strings

A string is an immutable sequence of bytes, and is often human-readable text.

var name = "John" // John is a string

The length of a string can be determined by the len(string) function. Strings are just a list of individual characters and are indexed from left to right, and the count starts from zero. E.g

var name = "John"

John
0123

// we can get the first character by passing the index in a square bracket

j := name[0]
o := name[1]
h := name[2]
n := name[3]

However, when indexing a string, take note not to let the index go out of the bound of the string length as that can cause an indexing error.

Booleans

Booleans, represented as bool in Go can only be true or false. They are often used to represent possibilities of two logical states, e.g light is on or off (true for on, and false, for off.)

var isLightOn bool = false

isLightOn = true

They are used in conditions for control flows and are the outputs of comparison operations such as ==, <=, >=, !=. The ! syntax is used to negate the values of Booleans, it often reads as not. For instance,

var isLightOn bool = false

isLightOn = !isLightOn // note exclamation

This code means that the value of isLightOn should be the opposite of whatever the current value is, so if it is false, it becomes true, and if it is true, it becomes false.

Control Structures: Loops and Conditional Statements.

Control structures are used to analyze the state of the program and decide the direction of execution based on the given state. They are the basic decision-making process in programming.

Control statements mostly allow you to do two things:

  1. Execute some statements while skipping others

  2. Repeat one or more statements while some conditions are met.

Loops

There is only one way to loop in Go, and that is with the for loop. A loop is an iteration/rerun of a block of code until a certain condition is met. The condition is specified by the programmer and the program will not proceed until the condition of the loop becomes false, and if the condition is never met (i.e never false), the loop will run till infinity and eventually crash the computer (don’t worry, most computers can handle this elegantly). A for loop usually has this format, notice how the components are separated by semicolons, it is very important.

for initialization; condition; post {
    // do this
}

there are three basic parts of the for loop:

  • The initialization: this is called before the first iteration, so it is often used to do some pre-calculations, run a short code, or for variable declaration before the loop runs. Often used for short variable declarations, and any variable declared there is only available in the scope of the loop i.e in the block of code contained between the { and }.

  • the condition: this is run before every iteration, so it is where the comparison needed to check if the loop should run is done. If the value of this condition is false, then the loop stops running, and the program proceeds to the code following the for loop,

  • the post: this part of the loop is executed at the end of every iteration, so it is where the necessary variables are incremented and updated ahead of the next iteration.

Let us write a simple for loop that counts from 0 to 9;

// remember to import the fmt package

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

This code will run 10 times. It declares i to be zero at the initialization (which is run only once and before the for loop begins), then it checks the condition i < 10 , and since i is 0 at this point, the condition is true, so the code runs, and 0 is printed to the console. Immediately after that, the post is run and i++ executed. ++ is a syntax meaning increment the current value by one,

i++

// is the same as 

i = i + 1

and since i is zero, ++ increments i to 1. This completes the cycle for the first iteration. On the second iteration, the initialization is skipped, and the condition is checked again, this time, we know i is 1, so the condition is true again since 1 is < 10. The loop is run and 1 printed to the console, after which the post part is run to increment i to 2 and complete the cycle of the second iteration. This is repeated until i is incremented up to 10 the condition fails and the loop is closed and the program continues to the next line after the loop, which is why you don’t see the 10 printed in the console.

There is no while or do while that you may be familiar with in other languages in Go. You only have the for. To achieve a while loop, you can simply drop the initialization and post part of the for loop, as those are optional, and only declare a condition:

package main

import "fmt"

func main() {
    number := 1000
    noOfTimesDivisibleBy14 := 0

    for number >= 14 { // this works like a while loop
        number -= 14
        noOfTimesDivisibleBy14++
    }

    fmt.Printf("No of times divisible is %v, remainder is %v", noOfTimesDivisibleBy14, number)

}

If you omit the condition, you can create an infinite loop that runs forever;

for {
    // this is an infinite loop. 
}

To break out of a loop, you can use the break keyword to stop the iteration and break away from the loop irrespective of the condition, then the program continues to the next line after the loop body.

count := 0 

for {
    fmt.Println(count)
    if count >= 1000 {
        break // stop the loop and continue
    }
}
// the program continues running here

Note that you can also use a return keyword to close the loop irrespective of the conditions, but instead of continuing to the next line after the loop like in the case of break above, the program returns completely from that scope.

Conditional Statements

if

The if statement is used to check if a block of code should be executed or not. it usually follows the following expression:

if initialization; condtion {
    // do something conditionally
}

The initialization, as we saw in the for loop is where we can make some variable declarations before the condition is checked, and if the condition is true the code in the block {} will run and any variable created in the initialization will be available in the scope of that block. If the condition is false, the block is skipped and the program proceeds to the next line after the if condition.

There is also an else part of the if, though it is optional. The else block will run if the condition of the if statement is false.

if condtion {
    // do this, only if the condition is true
} else if second condition {
    // or do this, if first condition is false and second condition is true
} else {
    // if any of the previous conditions are not true, then do this
}

You can chain as many else if as you want (although it is probably not advisable to do so) or you can use the next conditional statement, the switch. Not that if you use a return inside the if statement, the program returns from the current scope.

Switch

The switch is simply a more compact and easier way to write a chain of if and else statements. It runs a condition against preconditions called cases and executes the first case it encounters that satisfies the condition of the execution.

Switch statements follow the following format:

switch initialization; comparator {
    case condition1: 
            // run this if comparator == condition1
    case condition2:
        // run this if comparator != condition1 and comparator == condition2
    default:
        // run this if the comparator matches none of the cases
}

// note that you can have as many cases as required

As always, the initialization is the part that allows us to create variables or run short codes before the switch is executed, and it is also optional. The comporator is the thing we want to check with cases from top to bottom to find a match. if no match is found with any of the cases, the default block is executed.

For example, let us write a code that switches between the color of a car:

switch carColor {
    case "red":
            fmt.Println("The car is red")
    case "blue":
            fmt.Println("The car is blue")
    case "green":
            fmt.Println("The car is green")
    default:
            fmt.Println("I don't know the color of the car")
}

The comparator is also optional, if it is not provided, it defaults to true and the program goes through the cases from top to bottom until it finds the first case whose condition is true. For example:

switch {
    case true == false:
            fmt.Println("The is false")
    case false == true:
            fmt.Println("The is false")
    case false != true:
            fmt.Println("The is true")
    default:
            fmt.Println("the default")
}

// in this case, since the comparator is not provided, it defaults to true and
// the program goes through the cases until it executes the third case whose 
// condition is true.

Congratulations! You made it here. That is just the basic syntax of Golang. In the next article, we will dive deeply into advanced data types such as arrays, maps, functions, pointers, and structs.

You can connect with me on LinkedIn here, on X, and my email is here.

Keep GOing

References:

Effective Go: https://go.dev/doc/effective_go

The Go Programming Language: Alan A.A Donovan, Brian W. Kernighan