What you should know about Go
This big section discusses important and essential Go features including variables, controlling program flow, iterations, getting user input, and Go concurrency. We begin by discussing variables, variable declaration, and variable usage.
Defining and using variables
Imagine that you want to perform basic mathematical calculations. In that case, you need to define variables to keep the input, intermediate computations, and results.
Go provides multiple ways to declare new variables to make the variable declaration process more natural and convenient. You can declare a new variable using the var
keyword, followed by the variable name, followed by the desired data type (we are going to cover data types in detail in Chapter 2, Basic Go Data Types). If you want, you can follow that declaration with =
and an initial value for your variable. If there is an initial value given, you can omit the data type and the compiler will infer it for you.
This brings us to a very important Go rule: if no initial value is given to a variable, the Go compiler will automatically initialize that variable to the zero value of its data type.
There is also the :=
notation, which can be used instead of a var declaration. :=
defines a new variable by inferring the data of the value that follows it. The official name for :=
is short assignment statement, and it is very frequently used in Go, especially for getting the return values from functions and for
loops with the range
keyword.
The short assignment statement can be used in place of a var
declaration with an implicit type. You rarely see the use of var
in Go; the var
keyword is mostly used for declaring global or local variables without an initial value. The reason for the former is that every statement that exists outside of the code of a function must begin with a keyword, such as func
or var
.
This means that the short assignment statement cannot be used outside of a function environment because it is not permitted there. Last, you might need to use var
when you want to be explicit about the data type. For example, when you want the type of a variable to be int8
or int32
instead of int
, which is the default.
Constants
There are values, such as the mathematical constant Pi, that cannot change. In that case, we can declare such values as constants using const
. Constants are declared just like variables but cannot change once they have been declared.
The supported data types for constants are character, string, Boolean, and all numeric data types. There’s more about Go data types in Chapter 2, Basic Go Data Types.
Global variables
Global variables are variables that are defined outside of a function implementation. Global variables can be accessed from anywhere in a package without the need to explicitly pass them to a function, and they can be changed unless they were defined as constants, using the const
keyword.
Although you can declare local variables using either var
or :=
, only const
(when the value of a variable is not going to change) and var
work for global variables.
Printing variables
Programs tend to display information, which means that they need to print data or send it somewhere for other software to store or process it. To print data on the screen, Go uses the functionality of the fmt
package. If you want Go to take care of the printing, then you might want to use the fmt.Println()
function. However, there are times when you want to have full control over how data is going to be printed. In such cases, you might want to use fmt.Printf()
.
fmt.Printf()
is similar to the C printf()
function and requires the use of control sequences that specify the data type of the variable that is going to be printed. Additionally, the fmt.Printf()
function allows you to format the generated output, which is particularly convenient for floating point values because it allows you to specify the digits that will be displayed in the output (%.2f
displays two digits after the decimal point of a floating point value). Lastly, the \n
character is used for printing a newline character and, therefore, creating a new line, as fmt.Printf()
does not automatically insert a newline—this is not the case with fmt.Println()
, which automatically inserts a newline, hence the ln
at the end of its name.
The following program illustrates how you can declare new variables, how to use them, and how to print them—type the following code into a plain text file named variables.go
:
package main
import (
"fmt"
"math"
)
var Global int = 1234
var AnotherGlobal = -5678
func main() {
var j int
i := Global + AnotherGlobal
fmt.Println("Initial j value:", j)
j = Global
// math.Abs() requires a float64 parameter
// so we type cast it appropriately
k := math.Abs(float64(AnotherGlobal))
fmt.Printf("Global=%d, i=%d, j=%d k=%.2f.\n", Global, i, j, k)
}
Personally, I prefer to make global variables stand out by either beginning them with an uppercase letter or using all capital letters. As you are going to learn in Chapter 6, Go Packages and Functions, the case of the first character of a variable name has a special meaning in Go and changes its visibility. So this works for the main
package only.
This above program contains the following:
- A global
int
variable namedGlobal
. - A second global variable named
AnotherGlobal
—Go automatically infers its data type from its value, which in this case is an integer. - A local variable named
j
of typeint
, which, as you will learn in the next chapter, is a special data type.j
does not have an initial value, which means that Go automatically assigns the zero value of its data type, which in this case is0
. - Another local variable named
i
—Go infers its data type from its value. As it is the sum of twoint
values, it is also anint
. - As
math.Abs()
requires afloat64
parameter, you cannot passAnotherGlobal
to it becauseAnotherGlobal
is anint
variable. Thefloat64()
type cast converts the value ofAnotherGlobal
tofloat64
. Note thatAnotherGlobal
continues to have theint
data type. - Lastly,
fmt.Printf()
formats and prints the output.
Running variables.go
produces the following output:
Initial j value: 0
Global=1234, i=-4444, j=1234 k=5678.00.
This example demonstrated another important Go rule that was also mentioned previously: Go does not allow implicit data conversions like C. As presented in variables.go
, the math.Abs()
function that expects (requires) a float64
value cannot work with an int
value, even if this particular conversion is straightforward and error-free. The Go compiler refuses to compile such statements. You should convert the int
value to a float64
explicitly using float64()
for things to work properly.
For conversions that are not straightforward (for example, string
to int
), there exist specialized functions that allow you to catch issues with the conversion, in the form of an error
variable that is returned by the function.
Controlling program flow
So far, we have seen Go variables, but how do we change the flow of a Go program based on the value of a variable or some other condition? Go supports the if/else
and switch
control structures. Both control structures can be found in most modern programming languages, so if you have already programmed in another programming language, you should already be familiar with both if
and switch
statements. if
statements use no parenthesis to embed the conditions that need to be examined because Go does not use parentheses in general. As expected, if
has support for else
and else if
statements.
To demonstrate the use of if
, let us use a very common pattern in Go that is used almost everywhere. This pattern says that if the value of an error variable as returned from a function is nil
, then everything is OK with the function execution. Otherwise, there is an error condition somewhere that needs special care. This pattern is usually implemented as follows:
err := anyFunctionCall()
if err != nil {
// Do something if there is an error
}
err
is the variable that holds the error value as returned from a function and !=
means that the value of the err variable is not equal to nil
. You will see similar code multiple times in Go programs.
Lines beginning with //
are single-line comments. If you put //
in the middle of a line, then everything after //
until the end of the line is considered a comment. This rule does not apply if //
is inside a string value.
The switch
statement has two different forms. In the first form, the switch
statement has an expression that is evaluated, whereas in the second form, the switch
statement has no expression to evaluate. In that case, expressions are evaluated in each case
statement, which increases the flexibility of switch
. The main benefit you get from switch
is that when used properly, it simplifies complex and hard-to-read if-else
blocks.
Both if
and switch
are illustrated in the following code, which is designed to process user input given as command line arguments—please type it and save it as control.go
. For learning purposes, we present the code of control.go
in pieces in order to explain it better:
package main
import (
"fmt"
"os"
"strconv"
)
This first part contains the expected preamble with the imported packages. The implementation of the main()
function starts next:
func main() {
if len(os.Args) != 2 {
fmt.Println("Please provide a command line argument")
return
}
argument := os.Args[1]
This part of the program makes sure that you have a single command line argument to process, which is accessed as os.Args[1]
, before continuing. We will cover this in more detail later, but you can refer to Figure 1.2 for more information about the os.Args
slice:
// With expression after switch
switch argument {
case "0":
fmt.Println("Zero!")
case "1":
fmt.Println("One!")
case "2", "3", "4":
fmt.Println("2 or 3 or 4")
fallthrough
default:
fmt.Println("Value:", argument)
}
Here, you see a switch block with four branches. The first three require exact string matches and the last one matches everything else. The order of the case statements is important because only the first match is executed. The fallthrough
keyword tells Go that after this branch is executed, it will continue with the next branch, which in this case is the default branch:
value, err := strconv.Atoi(argument)
if err != nil {
fmt.Println("Cannot convert to int:", argument)
return
}
As command line arguments are initialized as string values, we need to convert user input into an integer value using a separate call, which in this case is a call to strconv.Atoi()
. If the value of the err
variable is nil
, then the conversion was successful, and we can continue. Otherwise, an error message is printed onscreen and the program exits.
The following code shows the second form of switch
, where the condition is evaluated at each case branch:
// No expression after switch
switch {
case value == 0:
fmt.Println("Zero!")
case value > 0:
fmt.Println("Positive integer")
case value < 0:
fmt.Println("Negative integer")
default:
fmt.Println("This should not happen:", value)
}
}
This gives you more flexibility but requires more thinking when reading the code. In this case, the default branch should not be executed, mainly because any valid integer value would be caught by the other three branches. Nevertheless, the default branch is there, which is good practice because it can catch unexpected values.
Running control.go
generates the following output:
$ go run control.go 10
Value: 10
Positive integer
$ go run control.go 0
Zero!
Zero!
Each one of the two switch blocks in control.go
creates one line of output.
Iterating with for loops and range
This section is all about iterating in Go. Go supports for
loops as well as the range
keyword to iterate over all the elements of arrays, slices, and (as you will see in Chapter 3, Composite Data Types) maps, without knowing the size of the data structure.
An example of Go simplicity is the fact that Go provides support for the for
keyword only, instead of including direct support for while
loops. However, depending on how you write a for
loop, it can function as a while
loop or an infinite loop. Moreover, for
loops can implement the functionality of JavaScript’s forEach
function when combined with the range
keyword.
You need to put curly braces around a for
loop even if it contains just a single statement or no statements at all.
You can also create for
loops with variables and conditions. A for
loop can be exited with a break
keyword, and you can skip the current iteration with the continue
keyword.
The following program illustrates the use of for
on its own and with the range
keyword—type it and save it as forLoops.go
to execute it afterward:
package main
import "fmt"
func main() {
// Traditional for loop
for i := 0; i < 10; i++ {
fmt.Print(i*i, " ")
}
fmt.Println()
}
The previous code illustrates a traditional for
loop that uses a local variable named i
. This prints the squares of 0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, and 9
onscreen. The square of 10
is not computed and printed because it does not satisfy the 10 < 10
condition.
The following code is idiomatic Go and produces the same output as the previous for
loop:
i := 0
for ok := true; ok; ok = (i != 10) {
fmt.Print(i*i, " ")
i++
}
fmt.Println()
You might use it, but it is sometimes hard to read, especially for people who are new to Go. The following code shows how a for
loop can simulate a while
loop, which is not supported directly:
// For loop used as while loop
i = 0
for {
if i == 10 {
break
}
fmt.Print(i*i, " ")
i++
}
fmt.Println()
The break
keyword in the if
condition exits the loop early and acts as the loop exit condition. Without an exit condition that is going to be met at some point and the break
keyword, the for
loop is never going to finish.
Lastly, given a slice, which you can consider as a resizable array, named aSlice
, you iterate over all its elements with the help of range
, which returns two ordered values: the index of the current element in the slice and its value. If you want to ignore either of these return values, which is not the case here, you can use _
in the place of the value that you want to ignore. If you just need the index, you can leave out the second value from range
entirely without using _
:
// This is a slice but range also works with arrays
aSlice := []int{-1, 2, 1, -1, 2, -2}
for i, v := range aSlice {
fmt.Println("index:", i, "value: ", v)
}
If you run forLoops.go
, you get the following output:
$ go run forLoops.go
0 1 4 9 16 25 36 49 64 81
0 1 4 9 16 25 36 49 64 81
0 1 4 9 16 25 36 49 64 81
index: 0 value: -1
index: 1 value: 2
index: 2 value: 1
index: 3 value: -1
index: 4 value: 2
index: 5 value: -2
The previous output illustrates that the first three for
loops are equivalent and, therefore, produce the same output. The last six lines show the index and the value of each element found in aSlice
.
Now that we know about for
loops, let us see how to get user input.
Getting user input
Getting user input is an important part of the majority of programs. This section presents two ways of getting user input, which read from standard input and use the command line arguments of the program.
Reading from standard input
The fmt.Scanln()
function can help you read user input while the program is already running and store it to a string variable, which is passed as a pointer to fmt.Scanln()
. The fmt
package contains additional functions for reading user input from the console (os.Stdin
), files, or argument lists.
The fmt.Scanln()
function is rarely used to get user input. Usually, user input is read from command line arguments or external files. However, interactive command line applications need fmt.Scanln()
.
The following code illustrates reading from standard input—type it and save it as input.go
:
package main
import (
"fmt"
)
func main() {
// Get User Input
fmt.Printf("Please give me your name: ")
var name string
fmt.Scanln(&name)
fmt.Println("Your name is", name)
}
While waiting for user input, it is good to let the user know what kind of information they have to give, which is the purpose of the fmt.Printf()
call. The reason for not using fmt.Println()
instead is that fmt.Println()
automatically appends a newline character at the end of the output, which is not what we want here.
Executing input.go
generates the following kind of output and user interaction:
$ go run input.go
Please give me your name: Mihalis
Your name is Mihalis
Working with command line arguments
Although typing user input when needed might look like a nice idea, this is not usually how real software works. Usually, user input is given in the form of command line arguments to the executable file. By default, command line arguments in Go are stored in the os.Args
slice.
The standard Go library also offers the flag
package for parsing command line arguments, but there are better and more powerful alternatives.
The figure that follows shows the way command line arguments work in Go, which is the same as in the C programming language. It is important to know that the os.Args
slice is properly initialized by Go and is available to the program when referenced. The os.Args
slice contains string
values:
Figure 1.2: How the os.Args slice works
The first command line argument stored in the os.Args
slice is always the file path of the executable. If you use go run
, you will get a temporary name and path; otherwise, it will be the path of the executable as given by the user. The remaining command line arguments are what come after the name of the executable—the various command line arguments are automatically separated by space characters unless they are included in double or single quotes; this depends on the OS.
The use of os.Args
is illustrated in the code that follows, which is to find the minimum and the maximum numeric values of its input while ignoring invalid input, such as characters and strings. Type the code and save it as cla.go
:
package main
import (
"fmt"
"os"
"strconv"
)
As expected, cla.go
begins with its preamble. The fmt
package is used for printing output, whereas the os
package is required because os.Args
is a part of it. Lastly, the strconv
package contains functions for converting strings to numeric values. Next, we make sure that we have at least one command line argument:
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Need one or more arguments!")
return
}
Remember that the first element in os.Args
is always the path of the executable file, so os.Args
is never totally empty. Next, the program checks for errors in the same way we looked for them in previous examples. You will learn more about errors and error handling in Chapter 2, Basic Go Data Types:
var min, max float64
var initialized = 0
for i := 1; i < len(arguments); i++ {
n, err := strconv.ParseFloat(arguments[i], 64)
if err != nil {
continue
}
In this case, we use the error
variable returned by strconv.ParseFloat()
to make sure that the call to strconv.ParseFloat()
was successful and there is a valid numeric value to process. Otherwise, we should continue to the next command line argument.
The for
loop is used to iterate over all available command line arguments except the first one, which uses an index value of 0
. This is another popular technique for working with all command line arguments.
The following code is used to properly initialize the value of the min
and max
variables after the first valid command line argument is processed:
if initialized == 0 {
min = n
max = n
initialized = 1
continue
}
We are using initialized == 0
to test whether this is the first valid command line argument. If this is the case, we process the first command line argument and initialize the min
and max
variables to its value.
The next code checks whether the current value is our new minimum or maximum—this is where the logic of the program is implemented:
if n < min {
min = n
}
if n > max {
max = n
}
}
fmt.Println("Min:", min)
fmt.Println("Max:", max)
}
The last part of the program is about printing your findings, which are the minimum and maximum numeric values of all valid command line arguments. The output you get from cla.go
depends on its input:
$ go run cla.go a b 2 -1
Min: -1
Max: 2
In this case, a
and b
are invalid, and the only valid inputs are -1
and 2
, which are the minimum value and maximum value, respectively:
$ go run cla.go a 0 b -1.2 10.32
Min: -1.2
Max: 10.32
In this case, a
and b
are invalid input and, therefore, ignored:
$ go run cla.go
Need one or more arguments!
In the final case, as cla.go
has no input to process, it prints a help message. If you execute the program with no valid input values, for example, go run cla.go a b c
, then the values of both Min
and Max
are going to be zero.
The next subsection shows a technique for differentiating between different data types, using error variables.
Using error variables to differentiate between input types
Now, let me show you a technique that uses error variables to differentiate between various kinds of user input. For this technique to work, you should go from more specific cases to more generic ones. If we are talking about numeric values, you should first examine whether a string is a valid integer before examining whether the same string is a floating-point value, because every valid integer is also a valid floating-point value.
The first part of the program, which is saved as process.go
, is the following:
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Not enough arguments")
return
}
The previous code contains the preamble and the storing of the command line arguments in the arguments
variable.
The next part is where we start examining the validity of the input:
var total, nInts, nFloats int
invalid := make([]string, 0)
for _, k := range arguments[1:] {
// Is it an integer?
_, err := strconv.Atoi(k)
if err == nil {
total++
nInts++
continue
}
First, we create three variables for keeping a count of the total number of valid values examined, the total number of integer values found, and the total number of floating-point values found, respectively. The invalid
variable, which is a slice of strings, is used for keeping all non-numeric values.
Once again, we need to iterate over all the command line arguments except the first one, which has an index value of 0
, because this is the path of the executable file. We ignore the path of the executable, using arguments[1:]
instead of just arguments
—selecting a continuous part of a slice is discussed in the next chapter.
The call to strconv.Atoi()
determines whether we are processing a valid int
value or not. If so, we increase the total
and nInts
counters:
// Is it a float
_, err = strconv.ParseFloat(k, 64)
if err == nil {
total++
nFloats++
continue
}
Similarly, if the examined string represents a valid floating-point value, the call to strconv.ParseFloat()
is going to be successful, and the program will update the relevant counters. Lastly, if a value is not numeric, it is appended to the invalid
slice with a call to append()
:
// Then it is invalid
invalid = append(invalid, k)
}
The last part of the program is the following:
fmt.Println("#read:", total, "#ints:", nInts, "#floats:", nFloats)
if len(invalid) > total {
fmt.Println("Too much invalid input:", len(invalid))
for _, s := range invalid {
fmt.Println(s)
}
}
}
Presented here is extra code that warns you when your invalid input is more than the valid one (len(invalid) > total
). This is a common practice for keeping unexpected input in applications.
Running process.go
produces the following kind of output:
$ go run process.go 1 2 3
#read: 3 #ints: 3 #floats: 0
In this case, we process 1, 2, and 3, which are all valid integer values:
$ go run process.go 1 2.1 a
#read: 2 #ints: 1 #floats: 1
In this case, we have a valid integer, 1, a floating-point value, 2.1, and an invalid value, a:
$ go run process.go a 1 b
#read: 1 #ints: 1 #floats: 0
Too much invalid input: 2
a
b
If the invalid input is more than the valid one, then process.go
prints an extra error message.
The next subsection discusses the concurrency model of Go.
Understanding the Go concurrency model
This section is a quick introduction to the Go concurrency model. The Go concurrency model is implemented using goroutines and channels. A goroutine is the smallest executable Go entity. To create a new goroutine, you have to use the go
keyword followed by a predefined function or an anonymous function—both these methods are equivalent as far as Go is concerned.
The go
keyword works with functions or anonymous functions only.
A channel in Go is a mechanism that, among other things, allows goroutines to communicate and exchange data. If you are an amateur programmer or are hearing about goroutines and channels for the first time, do not panic. Goroutines and channels, as well as pipelines and sharing data among goroutines, will be explained in much more detail in Chapter 8, Go Concurrency.
Although it is easy to create goroutines, there are other difficulties when dealing with concurrent programming, including goroutine synchronization and sharing data between goroutines—this is a Go mechanism for avoiding side effects by using global state when running goroutines. As main()
runs as a goroutine as well, you do not want main()
to finish before the other goroutines of the program because once main()
exits, the entire program along with any goroutines that have not finished yet will terminate. Although goroutines cannot communicate directly with each other, they can share memory. The good thing is that there are various techniques for the main()
function to wait for goroutines to exchange data through channels or, less frequently in Go, use shared memory.
Type the following Go program, which synchronizes goroutines using time.Sleep()
calls (this is not the right way to synchronize goroutines—we will discuss the proper way to synchronize goroutines in Chapter 8, Go Concurrency), into your favorite editor, and save it as goRoutines.go
:
package main
import (
"fmt"
"time"
)
func myPrint(start, finish int) {
for i := start; i <= finish; i++ {
fmt.Print(i, " ")
}
fmt.Println()
time.Sleep(100 * time.Microsecond)
}
func main() {
for i := 0; i < 4; i++ {
go myPrint(i, 5)
}
time.Sleep(time.Second)
}
The preceding naively implemented example creates four goroutines and prints some values on the screen using the myPrint()
function—the go
keyword is used for creating the goroutines. Running goRoutines.go
generates the following output:
$ go run goRoutines.go
2 3 4 5
0 4 1 2 3 1 2 3 4 4 5
5
3 4 5
5
However, if you run it multiple times, you will most likely get a different output each time:
1 2 3 4 5
4 2 5 3 4 5
3 0 1 2 3 4 5
4 5
This happens because goroutines are initialized in a random order and start running in a random order. The Go scheduler is responsible for the execution of goroutines, just like the OS scheduler is responsible for the execution of the OS threads. Chapter 8, Go Concurrency, discusses Go concurrency in more detail and presents the solution to that randomness issue with the use of a sync.WaitGroup
variable—however, keep in mind that Go concurrency is everywhere, which is the main reason for including this section here. Therefore, as some error messages generated by the compiler discuss goroutines, you should not think that these goroutines were created by you.
The next section shows a practical example that involves developing a Go version of the which(1)
utility, which searches for an executable file in the PATH
environment value of the current user.