Mastering Go - Second Edition

5 (5 reviews total)
By Mihalis Tsoukalos
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Go and the Operating System

About this book

Often referred to (incorrectly) as Golang, Go is the high-performance systems language of the future. Mastering Go, Second Edition helps you become a productive expert Go programmer, building and improving on the groundbreaking first edition.

Mastering Go, Second Edition shows how to put Go to work on real production systems. For programmers who already know the Go language basics, this book provides examples, patterns, and clear explanations to help you deeply understand Go’s capabilities and apply them in your programming work.

The book covers the nuances of Go, with in-depth guides on types and structures, packages, concurrency, network programming, compiler design, optimization, and more. Each chapter ends with exercises and resources to fully embed your new knowledge.

This second edition includes a completely new chapter on machine learning in Go, guiding you from the foundation statistics techniques through simple regression and clustering to classification, neural networks, and anomaly detection. Other chapters are expanded to cover using Go with Docker and Kubernetes, Git, WebAssembly, JSON, and more.

If you take the Go programming language seriously, the second edition of this book is an essential guide on expert techniques.

Publication date:
August 2019
Publisher
Packt
Pages
798
ISBN
9781838559335

 

Go and the Operating System

This chapter is an introduction to various Go topics that beginners will find very useful. More experienced Go developers can also use this chapter as a refresher course on the fundamentals of Go. As it happens with most practical subjects, the best way to understand something is to experiment with it. In this case, experimenting means writing Go code on your own, making your own mistakes, and learning from them! Just don't let error messages and bugs discourage you.

In this chapter, you will learn about:

  • The history and the future of the Go programming language
  • The advantages of Go
  • Compiling Go code
  • Executing Go code
  • Downloading and using external Go packages
  • UNIX standard input, output, and error
  • Printing data on the screen
  • Getting user input
  • Printing data to standard error
  • Working with log files
  • Using Docker to compile and execute a Go source file
  • Error handling in Go
 

The history of Go

Go is a modern, generic-purpose, open source programming language that was officially announced at the end of 2009. Go began as an internal Google project, which means that it was started as an experiment, and has since been inspired by many other programming languages, including C, Pascal, Alef, and Oberon. Go's spiritual fathers are the professional programmers Robert Griesemer, Ken Thomson, and Rob Pike.

They designed Go as a language for professional programmers who want to build reliable, robust, and efficient software. Apart from its syntax and its standard functions, Go comes with a pretty rich standard library.

At the time of writing, the latest stable Go version is version 1.13. However, even if your version number is higher, the contents of the book will still be relevant.

If you are going to install Go for the first time, you can start by visiting https://golang.org/dl/. However, there is a big chance that your UNIX variant has a ready-to-install package for the Go programming language, so you might want to get Go by using your favorite package manager.

 

Where is Go going?

The Go community is already discussing the next major version of Go, which is going to be called Go 2, but there is nothing definitive at the moment.

The intention of the current Go 1 team is to make Go 2 more community driven. Although this is a good idea in general, it is always dangerous when lots of people try to make important decisions about a programming language that was initially designed and developed as an internal project by a small group of great people.

Some of the big changes that are being considered for Go 2 are generics, package versioning, and improved error handling. All these new features are under discussion at the moment and you should not be worried about them, but it is worthwhile to have an idea of the direction that Go is going in.

 

The advantages of Go

Go has many advantages, and some of them are unique to Go, while others are shared with other programming languages.

The list of the most significant Go advantages and features includes the following:

  • Go is a modern programming language that is easy to read, easy to understand, and was made by experienced developers.
  • Go wants happy developers because happy developers write better code!
  • The Go compiler prints practical warning and error messages that help you to solve the actual problem. Putting it simply, the Go compiler is there to help you, not to make your life miserable by printing pointless output!
  • Go code is portable, especially among UNIX machines.
  • Go has support for procedural, concurrent, and distributed programming.
  • Go supports garbage collection, so you do not have to deal with memory allocation and deallocation.
  • Go does not have a preprocessor and does high-speed compilation. As a consequence, Go can also be used as a scripting language.
  • Go can build web applications and provides a simple web server for testing purposes.
  • The standard Go library offers many packages that simplify the work of the developer. Additionally, the functions found in the standard Go library are tested and debugged in advance by the people who develop Go, which means that, most of the time, they come without bugs.
  • Go uses static linking by default, which means that the binary files produced can be easily transferred to other machines with the same OS. As a consequence, once a Go program is compiled successfully and an executable file is generated, you do not need to worry about libraries, dependencies, and different library versions anymore.
  • You will not need a graphical user interface (GUI) for developing, debugging, and testing Go applications, as Go can be used from the command-line, which I think many UNIX people prefer.
  • Go supports Unicode, which means that you do not need any extra code for printing characters from multiple human languages.
  • Go keeps concepts orthogonal because a few orthogonal features work better than many overlapping ones.

Is Go perfect?

There is no such thing as the perfect programming language, and Go is not an exception to this rule. However, some programming languages are better at some areas of programming or we like them more than other programming languages. Personally, I do not like Java, and while I used to like C++, I do not like it anymore. C++ has become too complex as a programming language, whereas, in my opinion, Java, code does not look good.

Some of the disadvantages of Go are:

  • Go does not have direct support for object-oriented programming, which can be a problem for programmers who are used to writing code in an object-oriented manner. Nevertheless, you can use composition in Go to mimic inheritance.
  • For some people, Go will never replace C.
  • C is still faster than any other programming language for systems programming and this is mainly because UNIX is written in C.

Nevertheless, Go is a pretty decent programming language that will not disappoint you if you find the time to learn it and program in it.

The godoc utility

The Go distribution comes with a plethora of tools that can make your life as a programmer easier. One of these tools is the godoc utility, which allows you to see the documentation of existing Go functions and packages without needing an internet connection.

The godoc utility can be executed either as a normal command-line application that displays its output on a terminal, or as a command-line application that starts a web server. In the latter case, you will need a web browser to look at the Go documentation.

If you type godoc without any command-line parameters, you will get a list of the command-line options supported by godoc.

The first way is similar to using the man(1) command, but for Go functions and packages. So, in order to find information about the Printf() function of the fmt package, you should execute the following command:

$ go doc fmt.Printf
  

Similarly, you can find information about the entire fmt package by running the following command:

$ go doc fmt
  

The second way requires executing godoc with the -http parameter:

$ godoc -http=:8001
  

The numeric value in the preceding command, which in this case is 8001, is the port number the HTTP server will listen to. You can choose any port number that is available provided that you have the right privileges. However, note that port numbers 0-1023 are restricted and can only be used by the root user, so it is better to avoid choosing one of those and pick something else, provided that it is not already in use by a different process.

You can omit the equal sign in the presented command and put a space character in its place. So, the following command is completely equivalent to the previous one:

$ godoc -http :8001
  

After that, you should point your web browser to the http://localhost:8001/pkg/ URL in order to get the list of available Go packages and browse their documentation.

 

Compiling Go code

In this section, you will learn how to compile Go code. The good news is that you can compile your Go code from the command line without the need for a graphical application. Furthermore, Go does not care about the name of the source file of an autonomous program as long as the package name is main and there is a single main() function in it. This is because the main() function is where the program execution begins. As a result, you cannot have multiple main() functions in the files of a single project.

We will start our first Go program compilation with a program named aSourceFile.go that contains the following Go code:

package main 
  
import ( 
    "fmt" 
) 
 
func main() { 
    fmt.Println("This is a sample Go program!") 
} 

Notice that the Go community prefers to name the Go source file source_file.go instead of aSourceFile.go. Whatever you choose, be consistent.

In order to compile aSourceFile.go and create a statically linked executable file, you will need to execute the following command:

$ go build aSourceFile.go
  

After that, you will have a new executable file named aSourceFile that you will need to execute:

$ file aSourceFile
aSourceFile: Mach-O 64-bit executable x86_64
$ ls -l aSourceFile
-rwxr-xr-x  1 mtsouk  staff  2007576 Jan 10 21:10 aSourceFile
$ ./aSourceFile
This is a sample Go program!
  

The main reason why the file size of aSourceFile is that big is because it is statically linked, which means that it does not require any external libraries to run.

 

Executing Go code

There is another way to execute your Go code that does not create any permanent executable files – it just generates some intermediate files that are automatically deleted afterward.

The way presented allows you to use Go as if it is a scripting programming language like Python, Ruby, or Perl.

So, in order to run aSourceFile.go without creating an executable file, you will need to execute the following command:

$ go run aSourceFile.go
This is a sample Go program!
  

As you can see, the output of the preceding command is exactly the same as before.

Please note that with go run, the Go compiler still needs to create an executable file. It is because you do not see it, it is automatically executed, and it is automatically deleted after the program has finished that you might think that there is no need for an executable file.

This book mainly uses go run to execute the example code; primarily because it is simpler than running go build and then executing the executable file. Additionally, go run does not leave any files on your hard disk after the program has finished its execution.

 

Two Go rules

Go has strict coding rules that are there to help you avoid silly errors and bugs in your code, as well as to make your code easier to read for the Go community. This section will present two such Go rules that you need to know.

As mentioned earlier, please remember that the Go compiler is there to help and not make your life miserable. As a result, the main purpose of the Go compiler is to compile and increase the quality of your Go code.

You either use a Go package or you do not include it

Go has strict rules about package usage. Therefore, you cannot just include any package you might think that you will need and then not use it afterward.

Look at the following naive program, which is saved as packageNotUsed.go:

package main 
 
import ( 
    "fmt" 
    "os" 
) 
 
func main() { 
    fmt.Println("Hello there!") 
} 
In this book, you are going to see lots of error messages, error situations, and warnings. I believe that looking at code that fails to compile is also useful and sometimes even more valuable than just looking at Go code that compiles without any errors. The Go compiler usually displays useful error messages and warnings that will most likely help you to resolve an erroneous situation, so do not underestimate Go error messages and warnings.

If you execute packageNotUsed.go, you will get the following error message from Go and the program will not get executed:

$ go run packageNotUsed.go
# command-line-arguments
./packageNotUsed.go:5:2: imported and not used: "os"
  

If you remove the os package from the import list of the program, packageNotUsed.go will compile just fine; try it on your own.

Although this is not the perfect time to start talking about breaking Go rules, there is a way to bypass this restriction. This is showcased in the following Go code that is saved in the packageNotUsedUnderscore.go file:

package main 
 
import ( 
    "fmt" 
    _ "os" 
) 
 
func main() { 
    fmt.Println("Hello there!") 
} 

So, using an underscore character in front of a package name in the import list will not create an error message in the compilation process even if that package will not be used in the program:

$ go run packageNotUsedUnderscore.go
Hello there!
  
The reason that Go is allowing you to bypass this rule will become more evident in Chapter 6, What You Might Not Know About Go Packages and Go Functions.

There is only one way to format curly braces

Look at the following Go program named curly.go:

package main 
 
import ( 
    "fmt" 
) 
 
func main() 
{ 
    fmt.Println("Go has strict rules for curly braces!") 
} 

Although it looks just fine, if you try to execute it, you will be fairly disappointed, because you will get the following syntax error message and the code will not compile and therefore run:

$ go run curly.go
# command-line-arguments
./curly.go:7:6: missing function body for "main"
./curly.go:8:1: syntax error: unexpected semicolon or newline before {
  

The official explanation for this error message is that Go requires the use of semicolons as statement terminators in many contexts, and the compiler automatically inserts the required semicolons when it thinks that they are necessary. Therefore, putting the opening curly brace ({) in its own line will make the Go compiler insert a semicolon at the end of the previous line (func main()), which is the cause of the error message.

 

Downloading Go packages

Although the standard Go library is very rich, there are times that you will need to download external Go packages in order to use their functionality. This section will teach you how to download an external Go package and where it will be placed on your UNIX machine.

Have in mind that although Go modules, which is a new Go feature that is still under development, might introduce changes to the way you work with external Go code, the process of downloading a single Go package into your computer will remain the same.
You will learn a lot more about Go packages and Go modules in Chapter 6, What You Might Not Know About Go Packages and Go Functions.

Look at the following naive Go program that is saved as getPackage.go:

package main 
 
import ( 
    "fmt" 
    "github.com/mactsouk/go/simpleGitHub" 
) 
 
func main() { 
    fmt.Println(simpleGitHub.AddTwo(5, 6)) 
} 

This program uses an external package because one of the import commands uses an internet address. In this case, the external package is called simpleGitHub and is located at github.com/mactsouk/go/simpleGitHub.

If you try to execute getPackage.go right away, you will be disappointed:

$ go run getPackage.go
getPackage.go:5:2: cannot find package "github.com/mactsouk/go/simpleGitHub" in any of:
    /usr/local/Cellar/go/1.9.1/libexec/src/github.com/mactsouk/go/
simpleGitHub (from $GOROOT)
/Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub (from $GOPATH)

So, you will need to get the missing package on your computer. In order to download it, you will need to execute the following command:

$ go get -v github.com/mactsouk/go/simpleGitHub
github.com/mactsouk/go (download)
github.com/mactsouk/go/simpleGitHub
  

After that, you can find the downloaded files at the following directory:

$ ls -l ~/go/src/github.com/mactsouk/go/simpleGitHub/
total 8
-rw-r--r--  1 mtsouk  staff  66 Oct 17 21:47 simpleGitHub.go
  

However, the go get command also compiles the package. The relevant files can be found at the following place:

$ ls -l ~/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a
-rw-r--r--  1 mtsouk  staff  1050 Oct 17 21:47 /Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a
  

You are now ready to execute getPackage.go without any problems:

$ go run getPackage.go
11
  

You can delete the intermediate files of a downloaded Go package as follows:

$ go clean -i -v -x github.com/mactsouk/go/simpleGitHub
cd /Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub
rm -f simpleGitHub.test simpleGitHub.test.exe
rm -f /Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/
simpleGitHub.a

Similarly, you can delete an entire Go package you have downloaded locally using the rm(1) UNIX command to delete its Go source after using go clean:

$ go clean -i -v -x github.com/mactsouk/go/simpleGitHub
$ rm -rf ~/go/src/github.com/mactsouk/go/simpleGitHub
  

After executing the former commands, you will need to download the Go package again.

 

UNIX stdin, stdout, and stderr

Every UNIX OS has three files open all the time for its processes. Remember that UNIX considers everything, even a printer or your mouse, a file.

UNIX uses file descriptors, which are positive integer values, as an internal representation for accessing all of its open files, which is much prettier than using long paths.

So, by default, all UNIX systems support three special and standard filenames: /dev/stdin, /dev/stdout, and /dev/stderr, which can also be accessed using file descriptors 0, 1, and 2, respectively. These three file descriptors are also called standard input, standard output, and standard error, respectively. Additionally, file descriptor 0 can be accessed as /dev/fd/0 on a macOS machine and as both /dev/fd/0 and /dev/pts/0 on a Debian Linux machine.

Go uses os.Stdin for accessing standard input, os.Stdout for accessing standard output, and os.Stderr for accessing standard error. Although you can still use /dev/stdin, /dev/stdout, and /dev/stderr or the related file descriptor values for accessing the same devices, it is better, safer, and more portable to stick with os.Stdin, os.Stdout, and os.Stderr offered by Go.

 

About printing output

As with UNIX and C, Go offers a variety of ways for printing your output on the screen. All the printing functions of this section require the use of the fmt Go standard package and are illustrated in the printing.go program, which will be presented in two parts.

The simplest way to print something in Go is by using the fmt.Println() and the fmt.Printf() functions. The fmt.Printf() function has many similarities with the C printf(3) function. You can also use the fmt.Print() function instead of fmt.Println(). The main difference between fmt.Print() and fmt.Println() is that the latter automatically adds a newline character each time you call it.

On the other hand, the biggest difference between fmt.Println() and fmt.Printf() is that the latter requires a format specifier for each thing that you want to print, just like the C printf(3) function, which means that you have better control of what you are doing, but you have to write more code. Go calls these format specifiers verbs. You can find more information about verbs at https://golang.org/pkg/fmt/.

If you have to perform any formatting before printing something or have to arrange multiple variables, then using fmt.Printf() might be a better choice. However, if you only have to print a single variable, then you might need to choose either fmt.Print() or fmt.Println(), depending on whether you need a newline character or not.

The first part of printing.go contains the following Go code:

package main 
 
import ( 
    "fmt" 
) 
 
func main() { 
    v1 := "123" 
    v2 := 123 
    v3 := "Have a nice day\n" 
    v4 := "abc" 

In this part, you see the import of the fmt package and the definition of four Go variables. The \n used in v3 is the line break character. However, if you just want to insert a line break in your output, you can call fmt.Println() without any arguments, instead of using something like fmt.Print("\n").

The second part is as follows:

    fmt.Print(v1, v2, v3, v4) 
    fmt.Println() 
    fmt.Println(v1, v2, v3, v4) 
    fmt.Print(v1, " ", v2, " ", v3, " ", v4, "\n") 
    fmt.Printf("%s%d %s %s\n", v1, v2, v3, v4) 
} 

In this part, you print the four variables using fmt.Println(), fmt.Print(), and fmt.Printf() in order to better understand how they differ.

If you execute printing.go, you will get the following output:

$ go run printing.go
123123Have a nice day
abc
123 123 Have a nice day
abc
123 123 Have a nice day
abc
123123 Have a nice day
abc
  

As you can see from the preceding output, the fmt.Println() function also adds a space character between its parameters, which is not the case with fmt.Print().

As a result, a statement such as  fmt.Println(v1, v2) is equivalent to fmt.Print(v1, " ", v2, "\n").

Apart from fmt.Println(), fmt.Print(), and fmt.Printf(), which are the simplest functions that can be used for generating output on the screen, there is also the S family of functions that includes fmt.Sprintln(), fmt.Sprint(), and fmt.Sprintf(). These functions are used to create strings based on a given format.

Finally, there is the F family of functions, which includes fmt.Fprintln(), fmt.Fprint(), and fmt.Fprintf(). They are used for writing to files using an io.Writer.

You will learn more about the io.Writer and io.Reader interfaces in Chapter 8, Telling a UNIX System What to Do.

The next section will teach you how to print your data using standard output, which is pretty common in the UNIX world.

 

Using standard output

Standard output is more or less equivalent to printing on the screen. However, using standard output might require the use of functions that do not belong to the fmt package, which is why it is presented in its own section.

The relevant technique will be illustrated in stdOUT.go and will be offered in three parts. The first part of the program is as follows:

package main 
 
import ( 
    "io" 
    "os" 
) 

So, stdOUT.go will use the io package instead of the fmt package. The os package is used for reading the command-line arguments of the program and for accessing os.Stdout.

The second portion of stdOUT.go contains the following Go code:

func main() { 
    myString := "" 
    arguments := os.Args 
    if len(arguments) == 1 { 
        myString = "Please give me one argument!" 
    } else { 
        myString = arguments[1] 
    } 

The myString variable holds the text that will be printed on the screen, which is either the first command-line argument of the program or, if the program was executed without any command-line arguments, a hardcoded text message.

The third part of the program is as follows:

    io.WriteString(os.Stdout, myString) 
    io.WriteString(os.Stdout, "\n") 
} 

In this case, the io.WriteString() function works in the same way as the fmt.Print() function; however, it takes only two parameters. The first parameter is the file you want to write to, which, in this case, is os.Stdout, and the second parameter is a string variable.

Strictly speaking, the type of the first parameter of the io.WriteString() function should be io.Writer, which requires a slice of bytes as the second parameter. However, in this case, a string variable does the job just fine. You will learn more about slices in Chapter 3, Working with Basic Go Data Types.

Executing stdOUT.go will produce the following output:

$ go run stdOUT.go
Please give me one argument!
$ go run stdOUT.go 123 12
123
  

The preceding output verifies that the io.WriteString() function sends the contents of its second parameter onto the screen when its first parameter is os.Stdout.

 

Getting user input

There are three main ways to get user input: firstly, by reading the command-line arguments of a program; secondly, by asking the user for input; or thirdly, by reading external files. This section will present the first two ways. Should you wish to learn how to read an external file, you should visit Chapter 8, Telling a UNIX System What to Do.

About := and =

Before continuing, it will be very useful to talk about the use of := and how it differs from =. The official name for := is the short assignment statement. The short assignment statement can be used in place of a var declaration with an implicit type.

You will rarely see the use of var in Go; the var keyword is mostly used for declaring global variables in Go programs, as well as for declaring 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 because it is not available there.

The := operator works as follows:

m := 123 

The result of the preceding statement is a new integer variable named m with a value of 123.

However, if you try to use := on an already declared variable, the compilation will fail with the following error message, which makes perfect sense:

$ go run test.go
# command-line-arguments
./test.go:5:4: no new variables on left side of :=
  

So, you might now ask, what will happen if you are expecting two or more values from a function and you want to use an existing variable for one of them. Should you use := or =? The answer is simple: you should use :=, as in the following code example:

i, k := 3, 4 
j, k := 1, 2 

As the j variable is used for the first time in the second statement, you use := even though k has already been defined in the first statement.

Although it may seem boring to talk about such insignificant things, knowing them will save you from various types of errors in the long run!

Reading from standard input

The reading of data from the standard input will be illustrated in stdIN.go, which you will see in two parts. The first part is as follows:

package main 
 
import ( 
    "bufio" 
    "fmt" 
    "os" 
) 

In the preceding code, you can see the use of the bufio package for the first time in this book.

You will learn more about the bufio package in Chapter 8, Telling a UNIX System What to Do.

Although the bufio package is mostly used for file input and output, you will keep seeing the os package all the time in this book because it contains many handy functions; its most common functionality is that it provides a way to access the command-line arguments of a Go program (os.Args).

The official description of the os package tells us that it offers functions that perform OS operations. This includes functions for creating, deleting, and renaming files and directories, as well as functions for learning the UNIX permissions and other characteristics of files and directories. The main advantage of the os package is that it is platform independent. Put simply, its functions will work on both UNIX and Microsoft Windows machines.

The second part of stdIN.go contains the following Go code:

func main() {
var f *os.File f = os.Stdin defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { fmt.Println(">", scanner.Text()) } }

First, there is a call to bufio.NewScanner() using standard input (os.Stdin) as its parameter. This call returns a bufio.Scanner variable, which is used with the Scan() function for reading from os.Stdin line by line. Each line that is read is printed on the screen before getting the next one. Please note that each line that is printed by the program begins with the > character.

The execution of stdIN.go will produce the following kind of output:

$ go run stdIN.go
This is number 21
> This is number 21
This is Mihalis
> This is Mihalis
Hello Go!
> Hello Go!
Press Control + D on a new line to end this program!
> Press Control + D on a new line to end this program!
  

According to the UNIX way, you can tell a program to stop reading data from standard input by pressing Ctrl + D.

The Go code of stdIN.go and stdOUT.go will be very useful when we talk about UNIX pipes in Chapter 8, Telling a UNIX System What to Do, so do not underestimate their simplicity.

Working with command-line arguments

The technique of this section will be illustrated using the Go code of cla.go, which will be presented in three parts. The program will find the minimum and the maximum of its command-line arguments.

The first part of the program is as follows:

package main 
 
import ( 
    "fmt" 
    "os" 
    "strconv" 
) 

What is important here is realizing that getting the command-line arguments requires the use of the os package. Additionally, you need another package, named strconv, in order to be able to convert a command-line argument, which is given as a string, into an arithmetical data type.

The second part of the program is the following:

func main() { 
    if len(os.Args) == 1 { 
        fmt.Println("Please give one or more floats.") 
        os.Exit(1) 
    } 
 
    arguments := os.Args 
    min, _ := strconv.ParseFloat(arguments[1], 64) 
    max, _ := strconv.ParseFloat(arguments[1], 64) 

Here, cla.go checks whether you have any command-line arguments by checking the length of os.Args. This is because the program needs at least one command-line argument to operate. Please note that os.Args is a Go slice with string values. The first element in the slice is the name of the executable program. Therefore, in order to initialize the min and max variables, you will need to use the second element of the string type os.Args slice that has an index value of 1.

There is an important point here: the fact that you are expecting one or more floats does not necessarily mean that the user will give you valid floats, either by accident or on purpose. However, as we have not talked about error handling in Go so far, cla.go assumes that all command-line arguments are in the right format and therefore will be acceptable. As a result, cla.go ignores the error value returned by the strconv.ParseFloat() function using the following statement:

n, _ := strconv.ParseFloat(arguments[i], 64) 

The preceding statement tells Go that you only want to get the first value returned by strconv.ParseFloat() and that you are not interested in the second value, which in this case is an error variable, by assigning it to the underscore character. The underscore character, which is called blank identifier, is the Go way of discarding a value. If a Go function returns multiple values, you can use the blank identifier multiple times.

Ignoring all or some of the return values of a Go function, especially the error values, is a very dangerous technique that should not be used in production code!

The third part comes with the following Go code:

    for i := 2; i < len(arguments); i++ { 
        n, _ := strconv.ParseFloat(arguments[i], 64) 
 
        if n < min { 
            min = n 
        } 
        if n > max { 
            max = n 
        } 
    } 
 
    fmt.Println("Min:", min) 
    fmt.Println("Max:", max) 
} 

Here, you use a for loop that will help you to visit all the elements of the os.Args slice, which was previously assigned to the arguments variable.

Executing cla.go will create the following kind of output:

$ go run cla.go -10 0 1
Min: -10
Max: 1
$ go run cla.go -10
Min: -10
Max: -10
  

As you might expect, the program does not behave well when it receives erroneous input; the worst thing of all is that it does not generate any warnings to inform the user that there was an error (or several) while processing the command-line arguments of the program:

$ go run cla.go a b c 10
Min: 0
Max: 10
  
 

About error output

This section will present a technique for sending data to UNIX standard error, which is the UNIX way of differentiating between actual values and error output.

The Go code for illustrating the use of standard error in Go is included in stdERR.go and will be presented in two parts. As writing to standard error requires the use of the file descriptor related to standard error, the Go code of stdERR.go will be based on the Go code of stdOUT.go.

The first part of the program is as follows:

package main 
 
import ( 
    "io" 
    "os" 
) 
func main() { 
    myString := "" 
    arguments := os.Args 
    if len(arguments) == 1 { 
        myString = "Please give me one argument!" 
    } else { 
        myString = arguments[1] 
    } 

So far, stdERR.go is almost identical to stdOUT.go.

The second portion of stdERR.go is the following:

    io.WriteString(os.Stdout, "This is Standard output\n") 
    io.WriteString(os.Stderr, myString) 
    io.WriteString(os.Stderr, "\n") 
} 

You call io.WriteString() two times to write to standard error (os.Stderr) and one more time to write to standard output (os.Stdout).

Executing stdERR.go will create the following output:

$ go run stdERR.go
This is Standard output
Please give me one argument!
  

The preceding output cannot help you to differentiate between data written to standard output and data written to standard error, which can be very useful sometimes. However, if you are using the bash(1) shell, there is a trick you can use in order to distinguish between standard output data and standard error data. Almost all UNIX shells offer this functionality in their own way.

When using bash(1), you can redirect the standard error output to a file as follows:

$ go run stdERR.go 2>/tmp/stdError
This is Standard output
$ cat /tmp/stdError
Please give me one argument!
  
The number after the name of a UNIX program or system call refers to the section of the manual its page belongs to. Although most of the names can be found only once in the manual pages, which means that putting the section number is not required, there are names that can be located in multiple sections because they have multiple meanings, such as crontab(1) and crontab(5). Therefore, if you try to retrieve the manual page of a name with multiple meanings without stating its section number, you will get the entry that has the smallest section number.

Similarly, you can discard error output by redirecting it to the /dev/null device, which is like telling UNIX to completely ignore it:

$ go run stdERR.go 2>/dev/null
This is Standard output
  

In the two examples, we redirected the file descriptor of standard error into a file and /dev/null, respectively. If you want to save both standard output and standard error to the same file, you can redirect the file descriptor of standard error (2) to the file descriptor of standard output (1). The following command shows the technique, which is pretty common in UNIX systems:

$ go run stdERR.go >/tmp/output 2>&1
$ cat /tmp/output
This is Standard output
Please give me one argument!
  

Finally, you can send both standard output and standard error to /dev/null as follows:

$ go run stdERR.go >/dev/null 2>&1
  
 

Writing to log files

The log package allows you to send log messages to the system logging service of your UNIX machine, whereas the syslog Go package, which is part of the log package, allows you to define the logging level and the logging facility your Go program will use.

Usually, most system log files of a UNIX OS can be found under the /var/log directory. However, the log files of many popular services, such as Apache and Nginx, can be found elsewhere, depending on their configuration.

Generally speaking, using a log file to write some information is considered a better practice than writing the same output on the screen for two reasons: firstly, because the output does not get lost as it is stored on a file, and secondly, because you can search and process log files using UNIX tools, such as grep(1), awk(1), and sed(1), which cannot be done when messages are printed on a terminal window.

The log package offers many functions for sending output to the syslog server of a UNIX machine. The list of functions includes log.Printf(), log.Print(), log.Println(), log.Fatalf(), log.Fatalln(), log.Panic(), log.Panicln(), and log.Panicf().

Please note that logging functions can be extremely handy for debugging your programs, especially server processes written in Go, so you should not underestimate their power.

Logging levels

The logging level is a value that specifies the severity of the log entry. There are various logging levels, including debug, info, notice, warning, err, crit, alert, and emerg, in reverse order of severity.

Logging facilities

A logging facility is like a category used for logging information. The value of the logging facility part can be one of auth, authpriv, cron, daemon, kern, lpr, mail, mark, news, syslog, user, UUCP, local0, local1, local2, local3, local4, local5, local6, or local7 and is defined inside /etc/syslog.conf, /etc/rsyslog.conf or another appropriate file depending on the server process used for system logging on your UNIX machine.

This means that if a logging facility is not defined and therefore handled, the log messages you send to it might get ignored and therefore lost.

Log servers

All UNIX machines have a separate server process that is responsible for receiving logging data and writing it to log files. There are various log servers that work on UNIX machines. However, only two of them are used on most UNIX variants: syslogd(8) and rsyslogd(8).

On macOS machines, the name of the process is syslogd(8). On the other hand, most Linux machines use rsyslogd(8), which is an improved and more reliable version of syslogd(8), which was the original UNIX system utility for message logging.

However, despite the UNIX variant you are using or the name of the server process used for logging, logging works the same way on every UNIX machine and therefore does not affect the Go code that you will write.

The configuration file of rsyslogd(8) is usually named rsyslog.conf and is located in /etc. The contents of a rsyslog.conf configuration file, without the lines with comments and lines starting with $, might look like the following:

$ grep -v '^#' /etc/rsyslog.conf | grep -v '^$' | grep -v '^\$'
auth,authpriv.*               /var/log/auth.log
*.*;auth,authpriv.none        -/var/log/syslog
daemon.*                      -/var/log/daemon.log
kern.*                        -/var/log/kern.log
lpr.*                         -/var/log/lpr.log
mail.*                        -/var/log/mail.log
user.*                        -/var/log/user.log
mail.info                     -/var/log/mail.info
mail.warn                     -/var/log/mail.warn
mail.err                      /var/log/mail.err
news.crit                     /var/log/news/news.crit
news.err                      /var/log/news/news.err
news.notice                   -/var/log/news/news.notice
*.=debug;\
    auth,authpriv.none;\
    news.none;mail.none       -/var/log/debug
*.=info;*.=notice;*.=warn;\
    auth,authpriv.none;\
    cron,daemon.none;\
    mail,news.none            -/var/log/messages
*.emerg                :omusrmsg:*
daemon.*;mail.*;\
    news.err;\
    *.=debug;*.=info;\
    *.=notice;*.=warn    |/dev/xconsole
local7.* /var/log/cisco.log

So, in order to send your logging information to /var/log/cisco.log, you will need to use the local7 logging facility. The star character after the name of the facility tells the logging server to catch every logging level that goes to the local7 logging facility and write it to /var/log/cisco.log.

The syslogd(8) server has a pretty similar configuration file that is usually /etc/syslog.conf. On macOS High Sierra, the /etc/syslog.conf file is almost empty and has been replaced by /etc/asl.conf. Nevertheless, the logic behind the configuration of /etc/syslog.conf, /etc/rsyslog.conf, and /etc/asl.conf is the same.

A Go program that sends information to log files

The Go code of logFiles.go will explain the use of the log and log/syslog packages to write to the system log files.

Please note that the log/syslog package is not implemented on the Microsoft Windows version of Go.

The first part of logFiles.go is as follows:

package main 
 
import ( 
    "fmt" 
    "log" 
    "log/syslog" 
    "os" 
    "path/filepath" 
) 
 
func main() { 
    programName := filepath.Base(os.Args[0]) 
    sysLog, err := syslog.New(syslog.LOG_INFO|syslog.LOG_LOCAL7, 
programName)

The first parameter to the syslog.New() function is the priority, which is a combination of the logging facility and the logging level. Therefore, a priority of LOG_NOTICE | LOG_MAIL, which is mentioned as an example, will send notice logging level messages to the MAIL logging facility.

As a result, the preceding code sets the default logging to the local7 logging facility using the info logging level. The second parameter to the syslog.New() function is the name of the process that will appear on the logs as the sender of the message. Generally speaking, it is considered a good practice to use the real name of the executable in order to be able to easily find the information you want in the log files at another time.

The second part of the program contains the following Go code:

    if err != nil { 
        log.Fatal(err) 
    } else { 
        log.SetOutput(sysLog) 
    } 
    log.Println("LOG_INFO + LOG_LOCAL7: Logging in Go!") 

After the call to syslog.New(), you will have to check the error variable that is returned from it so that you can make sure that everything is fine. If everything is OK, which means that the value of the error variable is equal to nil, you call the log.SetOutput() function, which sets the output destination of the default logger, which, in this case, is the logger you created earlier on (sysLog). Then, you can use log.Println() to send information to the log server.

The third part of logFiles.go comes with the following code:

    sysLog, err = syslog.New(syslog.LOG_MAIL, "Some program!") 
    if err != nil { 
        log.Fatal(err) 
    } else { 
        log.SetOutput(sysLog) 
    } 
 
    log.Println("LOG_MAIL: Logging in Go!") 
    fmt.Println("Will you see this?") 
} 

The last part shows that you can change the logging configuration in your programs as many times as you want and that you can still use fmt.Println() for printing output on the screen.

The execution of logFiles.go will create the following output on the screen of a Debian Linux machine:

$ go run logFiles.go
    
Broadcast message from [email protected] (Tue 2017-10-17 20:06:08 EEST):
    
logFiles[23688]: Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL: Logging in Go!
    
    
Message from [email protected] at Oct 17 20:06:08 ...
Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL: Logging in Go!
Will you see this?
  

Executing the same Go code on a macOS High Sierra machine generated the following output:

$ go run logFiles.go
Will you see this?
  

Please bear in mind that most UNIX machines store logging information in more than one log file, which is also the case with the Debian Linux machine used in this section. As a result, logFiles.go sends its output to multiple log files, which can be verified by the output of the following shell commands:

$ grep LOG_MAIL /var/log/mail.log
Oct 17 20:06:08 mail Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL: Logging in Go!
$ grep LOG_LOCAL7 /var/log/cisco.log
Oct 17 20:06:08 mail logFiles[23688]: 2017/10/17 20:06:08 LOG_INFO + LOG_LOCAL7: Logging in Go!
$ grep LOG_ /var/log/syslog
Oct 17 20:06:08 mail logFiles[23688]: 2017/10/17 20:06:08 LOG_INFO + LOG_LOCAL7: Logging in Go!
Oct 17 20:06:08 mail Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL: Logging in Go!
  

The preceding output shows that the message of the log.Println("LOG_INFO + LOG_LOCAL7: Logging in Go!") statement was written on both /var/log/cisco.log and /var/log/syslog, whereas the message of the log.Println("LOG_MAIL: Logging in Go!") statement was written on both /var/log/syslog and /var/log/mail.log.

The important thing to remember from this section is that if the logging server of a UNIX machine is not configured to catch all logging facilities, some of the log entries you send to it might get discarded without any warnings.

About log.Fatal()

In this section, you will see the log.Fatal() function in action. The log.Fatal() function is used when something really bad has happened and you just want to exit your program as fast as possible after reporting the bad situation.

The use of log.Fatal() is illustrated in the logFatal.go program, which contains the following Go code:

package main 
 
import ( 
    "fmt" 
    "log" 
    "log/syslog" 
) 
 
func main() { 
    sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!") 
    if err != nil { 
        log.Fatal(err) 
    } else { 
        log.SetOutput(sysLog) 
    } 
 
    log.Fatal(sysLog) 
    fmt.Println("Will you see this?") 
} 

Executing log.Fatal() will create the following output:

$ go run logFatal.go
exit status 1
  

As you can easily understand, the use of log.Fatal() terminates a Go program at the point where log.Fatal() was called, which is the reason that you did not see the output from the fmt.Println("Will you see this?") statement.

However, because of the parameters of the syslog.New() call, a log entry has been added to the log file that is related to mail, which is /var/log/mail.log:

$ grep "Some program" /var/log/mail.log
Jan 10 21:29:34 iMac Some program![7123]: 2019/01/10 21:29:34 &{17 Some program! iMac.local   {0 0} 0xc00000c220}
  

About log.Panic()

There are situations where a program will fail for good and you want to have as much information about the failure as possible.

In such difficult times, you might consider using log.Panic(), which is the logging function that is illustrated in this section using the Go code of logPanic.go.

The Go code of logPanic.go is as follows:

package main 
 
import ( 
    "fmt" 
    "log" 
    "log/syslog" 
) 
 
func main() { 
    sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!") 
    if err != nil { 
        log.Fatal(err) 
    } else { 
        log.SetOutput(sysLog) 
    } 
 
    log.Panic(sysLog) 
    fmt.Println("Will you see this?") 
} 

Executing logPanic.go on macOS Mojave will produce the following output:

$ go run logPanic.go
panic: &{17 Some program! iMac.local   {0 0} 0xc0000b21e0}
    
goroutine 1 [running]:
log.Panic(0xc00004ef68, 0x1, 0x1)
    /usr/local/Cellar/go/1.11.4/libexec/src/log/log.go:326 +0xc0
main.main()
    /Users/mtsouk/Desktop/mGo2nd/Mastering-Go-Second-Edition/ch01/logPanic.go:17 +0xd6
exit status 2
  

Running the same program on a Debian Linux with Go version 1.3.3 will generate the following output:

$ go run logPanic.go
panic: &{17 Some program! mail   {0 0} 0xc2080400e0}
    
goroutine 16 [running]:
runtime.panic(0x4ec360, 0xc208000320)
    /usr/lib/go/src/pkg/runtime/panic.c:279 +0xf5
log.Panic(0xc208055f20, 0x1, 0x1)
    /usr/lib/go/src/pkg/log/log.go:307 +0xb6
main.main()
    /home/mtsouk/Desktop/masterGo/ch/ch1/code/logPanic.go:17 +0x169
    
goroutine 17 [runnable]:
runtime.MHeap_Scavenger()
    /usr/lib/go/src/pkg/runtime/mheap.c:507
runtime.goexit()
    /usr/lib/go/src/pkg/runtime/proc.c:1445
    
goroutine 18 [runnable]:
bgsweep()
    /usr/lib/go/src/pkg/runtime/mgc0.c:1976
runtime.goexit()
    /usr/lib/go/src/pkg/runtime/proc.c:1445
    
goroutine 19 [runnable]:
runfinq()
    /usr/lib/go/src/pkg/runtime/mgc0.c:2606
runtime.goexit()
    /usr/lib/go/src/pkg/runtime/proc.c:1445
exit status 2
  

So, the output of log.Panic() includes additional low-level information that will hopefully help you to resolve difficult situations that happened in your Go code.

Analogous to the log.Fatal() function, the use of the log.Panic() function will add an entry to the proper log file and will immediately terminate the Go program.

Writing to a custom log file

Sometimes, you just need to write your logging data in a file of your choice. This can happen for many reasons, including writing debugging data, which sometimes can be too much, without messing with the system log files, keeping your own logging data separate from system logs in order to transfer it or store it in a database, and storing your data using a different format. This subsection will teach you how to write to a custom log file.

The name of the Go utility will be customLog.go, and the log file used will be /tmp/mGo.log.

The Go code of customLog.go will be presented in three parts. The first part is as follows:

package main 
 
import ( 
    "fmt" 
    "log" 
    "os" 
) 
 
var LOGFILE = "/tmp/mGo.log" 

The path of the log file is hardcoded into customLog.go using a global variable named LOGFILE. For the purposes of this chapter, that log file resides inside the /tmp directory, which is not the usual place for storing data because usually, the /tmp directory is emptied after each system reboot. However, at this point, this will save you from having to execute customLog.go with root privileges and from putting unnecessary files into system directories. If you ever decide to use the code of customLog.go in a real application, you should change that path into something more rational.

The second part of customLog.go is as follows:

func main() { 
    f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 
 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
    defer f.Close() 

Here, you create a new log file using os.OpenFile() using the desired UNIX file permissions (0644).

The last part of customLog.go is the following:

    iLog := log.New(f, "customLogLineNumber ", log.LstdFlags) 
 
    iLog.SetFlags(log.LstdFlags) 
    iLog.Println("Hello there!") 
    iLog.Println("Another log entry!") 
} 

If you look at the documentation page of the log package, which, among other places, can be found at https://golang.org/pkg/log/, you will see that the SetFlags function allows you to set the output flags (options) for the current logger. The default values as defined by LstdFlags are Ldate and Ltime, which means that you will get the current date and the time in each log entry you write in your log file.

Executing customLog.go will generate no visible output. However, after executing it twice, the contents of /tmp/mGo.log will be as follows:

$ go run customLog.go
$ cat /tmp/mGo.log
customLog 2019/01/10 18:16:09 Hello there!
customLog 2019/01/10 18:16:09 Another log entry!
$ go run customLog.go
$ cat /tmp/mGo.log
customLog 2019/01/10 18:16:09 Hello there!
customLog 2019/01/10 18:16:09 Another log entry!
customLog 2019/01/10 18:16:17 Hello there!
customLog 2019/01/10 18:16:17 Another log entry!
  

Printing line numbers in log entries

In this section, you are going to learn how to print the line number of the source file that executed the statement that wrote the log entry to a log file using the Go code of customLogLineNumber.go. This will be presented in two parts. The first part is as follows:

package main 
 
import ( 
    "fmt" 
    "log" 
    "os" 
) 
 
var LOGFILE = "/tmp/mGo.log" 
 
func main() { 
    f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 
 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
    defer f.Close() 

So far, there is nothing special when compared to the code of customLog.go.

The remaining Go code of customLogLineNumber.go is as follows:

    iLog := log.New(f, "customLogLineNumber ", log.LstdFlags) 
    iLog.SetFlags(log.LstdFlags | log.Lshortfile) 
    iLog.Println("Hello there!") 
    iLog.Println("Another log entry!") 
} 

All the magic happens with the iLog.SetFlags(log.LstdFlags | log.Lshortfile) statement, which, apart from log.LstdFlags, also includes log.Lshortfile. The latter flag adds the full filename as well as the line number of the Go statement that printed the log entry in the log entry itself.

Executing customLogLineNumber.go will generate no visible output. However, after two executions of customLogLineNumber.go, the contents of the /tmp/mGo.log log file will be similar to the following:

$ go run customLogLineNumber.go
$ cat /tmp/mGo.log
customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:26: Hello there!
customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:27: Another log entry!
$ go run customLogLineNumber.go
$ cat /tmp/mGo.log
customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:26: Hello there!
customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:27: Another log entry!
customLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:26: Hello there!
customLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:27: Another log entry!
  

As you can see, using long names for your command-line utilities makes your log files difficult to read.

In Chapter 2, Understanding Go Internals, you will learn how to use the defer keyword for printing the log messages of a Go function more elegantly.

 

Error handling in Go

Errors and error handling are two very important Go topics. Go likes error messages so much that it has a separate data type for errors, named error. This also means that you can easily create your own error messages if you find that what Go gives you is not adequate.

You will most likely need to create and handle your own errors when you are developing your own Go packages.

Please note that having an error condition is one thing, but deciding how to react to an error condition is a totally different thing. Putting it simply, not all error conditions are created equal, which means that some error conditions might require that you immediately stop the execution of a program, whereas other error situations might require printing a warning message for the user to see while continuing the execution of the program. It is up to the developer to use common sense and decide what to do with each error value the program might get.

Errors in Go are not like exceptions or errors in other programming languages; they are normal Go objects that get returned from functions or methods just like any other value.

The error data type

There are many scenarios where you might end up having to deal with a new error case while you are developing your own Go application. The error data type is here to help you to define your own errors.

This subsection will teach you how to create your own error variables. As you will see, in order to create a new error variable, you will need to call the New() function of the errors standard Go package.

The example Go code to illustrate this process can be found in newError.go and will be presented in two parts. The first part of the program is as follows:

package main 
 
import ( 
    "errors" 
    "fmt" 
) 
 
func returnError(a, b int) error { 
    if a == b { 
        err := errors.New("Error in returnError() function!") 
        return err 
    } else { 
        return nil 
    } 
} 

There are many interesting things happening here. First of all, you can see the definition of a Go function other than main() for the first time in this book. The name of this new naive function is returnError(). Additionally, you can see the errors.New() function in action, which takes a string value as its parameter. Lastly, if a function should return an error variable but there is not an error to report, it returns nil instead.

You will learn more about the various types of Go functions in Chapter 6, What You Might Not Know About Go Packages and Go Functions.

The second part of newError.go is the following:

func main() { 
    err := returnError(1, 2) 
    if err == nil { 
        fmt.Println("returnError() ended normally!") 
    } else { 
        fmt.Println(err) 
    } 
 
    err = returnError(10, 10) 
    if err == nil { 
        fmt.Println("returnError() ended normally!") 
    } else { 
        fmt.Println(err) 
    } 
 
    if err.Error() == "Error in returnError() function!" { 
        fmt.Println("!!") 
    } 
} 

As the code illustrates, most of the time, you need to check whether an error variable is equal to nil and then act accordingly. What is also presented here is the use of the errors.Error() function, which allows you to convert an error variable into a string variable. This function lets you compare an error variable with a string variable.

It is considered good practice to send your error messages to the logging service of your UNIX machine, especially when a Go program is a server or some other critical application. However, the code of this book will not follow this principle everywhere in order to avoid filling your log files with unnecessary data.

Executing newError.go will produce the following output:

$ go run newError.go
returnError() ended normally!
Error in returnError() function!
!!
  

If you try to compare an error variable with a string variable without first converting the error variable to a string variable, the Go compiler will create the following error message:

# command-line-arguments
./newError.go:33:9: invalid operation: err == "Error in returnError() function!" (mismatched types error and string)
  

Error handling

Error handling in a very important feature of Go because almost all Go functions return an error message or nil, which is the Go way of saying whether there was an error condition while executing a function. You will most likely get tired of seeing the following Go code not only in this book but also in every other Go program you can find on the internet:

if err != nil { 
    fmt.Println(err) 
    os.Exit(10) 
} 
Please do not confuse error handling with printing to error output because they are two totally different things. The former has to do with Go code that handles error conditions, whereas the latter has to do with writing something to the standard error file descriptor.

The preceding code prints the generated error message on the screen and exits your program using os.Exit(). Please note that you can also exit your program by calling the return keyword inside the main() function. Generally speaking, calling os.Exit() from a function other than main() is considered a bad practice. Functions other than main() tend to return the error message before exiting, which is handled by the calling function.

Should you wish to send the error message to the logging service instead of the screen, you should use the following variation of the preceding Go code:

if err != nil { 
    log.Println(err) 
    os.Exit(10) 
} 

Lastly, there is another variation of the preceding code that is used when something really bad has happened and you want to terminate the program:

if err != nil { 
    panic(err) 
    os.Exit(10) 
} 

Panic is a built-in Go function that stops the execution of a program and starts panicking! If you find yourself using panic too often, you might want to reconsider your Go implementation. People tend to avoid panic situations in favor of errors wherever possible.

As you will see in the next chapter, Go also offers the recover function, which might be able to save you from some bad situations. For now, you will need to wait for Chapter 2, Understanding Go Internals, to learn more about the power of the panic and recover function duo.

It is now time to see a Go program that not only handles error messages generated by standard Go functions, but also defines its own error message. The name of the program is errors.go and it will be presented to you in five parts. As you will see, the errors.go utility tries to improve the functionality of the cla.go program you saw earlier in this chapter by examining whether its command-line arguments are acceptable floats.

The first part of the program is as follows:

package main 
 
import ( 
    "errors" 
    "fmt" 
    "os" 
    "strconv" 
) 

This part of errors.go contains the expected import statements.

The second portion of errors.go comes with the following Go code:

func main() { 
    if len(os.Args) == 1 { 
        fmt.Println("Please give one or more floats.") 
        os.Exit(1) 
    } 
 
    arguments := os.Args 
    var err error = errors.New("An error") 
    k := 1 
    var n float64 

Here, you create a new error variable named err in order to initialize it with your own value.

The third part of the program is as follows:

    for err != nil { 
        if k >= len(arguments) { 
        fmt.Println("None of the arguments is a float!") 
        return 
        } 
        n, err = strconv.ParseFloat(arguments[k], 64) 
        k++ 
    } 
 
    min, max := n, n 

This is the trickiest part of the program because if the first command-line argument is not a proper float, you will need to check the next one and keep checking until you find a suitable command-line argument. If none of the command-line arguments are in the correct format, errors.go will terminate and print a message on the screen. All this checking happens by examining the error value that is returned by strconv.ParseFloat(). All this code is just for the accurate initialization of the min and max variables.

The fourth portion of the program comes with the following Go code:

    for i := 2; i < len(arguments); i++ { 
        n, err := strconv.ParseFloat(arguments[i], 64) 
        if err == nil { 
        if n < min { 
            min = n 
        } 
        if n > max { 
            max = n 
        } 
        } 
    } 

Here, you just process all the right command-line arguments in order to find the minimum and maximum floats among them.

Finally, the last code portion of the program deals with just printing out the current values of the min and max variables:

    fmt.Println("Min:", min) 
    fmt.Println("Max:", max) 
} 

As you can see from the Go code of errors.go, most of its code is about error handling rather than about the actual functionality of the program. Unfortunately, this is the case with most modern software developed in Go, as well as most other programming languages.

If you execute errors.go, you will get the following kind of output:

$ go run errors.go a b c
None of the arguments is a float!
$ go run errors.go b c 1 2 3 c -1 100 -200 a
Min: -200
Max: 100
  
 

Using Docker

In the last section of this chapter, you will learn how to use a Docker image in order to compile and execute your Go code inside the Docker image.

As you might already know, everything in Docker begins with a Docker image; you can either build your own Docker image from scratch or begin with an existing one. For the purposes of this section, the base Docker image will be downloaded from Docker Hub and we will continue with building the Go version of the Hello World! program inside that Docker image.

The contents of the Dockerfile that will be used are as follows:

FROM golang:alpine 
 
RUN mkdir /files 
COPY hw.go /files 
WORKDIR /files 
 
RUN go build -o /files/hw hw.go 
ENTRYPOINT ["/files/hw"] 

The first line defines the Docker image that will be used. The remaining three commands create a new directory in the Docker image, copy a file (hw.go) from the current user directory into the Docker image, and change the current working directory of the Docker image, respectively. The last two commands create a binary executable from the Go source file and specify the path of the binary file that will be executed when you run that Docker image.

So, how do you use that Dockerfile? Provided that a file named hw.go exists in the current working directory, you can build a new Docker image as follows:

$ docker build -t go_hw:v1 .
Sending build context to Docker daemon  2.237MB
Step 1/6 : FROM golang:alpine
alpine: Pulling from library/golang
cd784148e348: Pull complete
7e273b0dfc44: Pull complete
952c3806fd1a: Pull complete
ee1f873f86f9: Pull complete
7172cd197d12: Pull complete
Digest: sha256:198cb8c94b9ee6941ce6d58f29aadb855f64600918ce602cdeacb018ad77d647
Status: Downloaded newer image for golang:alpine
 ---> f56365ec0638
Step 2/6 : RUN mkdir /files
 ---> Running in 18fa7784d82c
Removing intermediate container 18fa7784d82c
 ---> 9360e95d7cb4
Step 3/6 : COPY hw.go /files
 ---> 680517bc4aa3
Step 4/6 : WORKDIR /files
 ---> Running in f3f678fcc38d
Removing intermediate container f3f678fcc38d
 ---> 640117aea82f
Step 5/6 : RUN go build -o /files/hw hw.go
 ---> Running in 271cae1fa7f9
Removing intermediate container 271cae1fa7f9
 ---> dc7852b6aeeb
Step 6/6 : ENTRYPOINT ["/files/hw"]
 ---> Running in cdadf286f025
Removing intermediate container cdadf286f025
 ---> 9bec016712c4
Successfully built 9bec016712c4
Successfully tagged go_hw:v1
  

The name of the newly created Docker image is go_hw:v1.

If the golang:alpine Docker image is already present on your computer, the output of the preceding command will be as follows:

$ docker build -t go_hw:v1 .
Sending build context to Docker daemon  2.237MB
Step 1/6 : FROM golang:alpine
 ---> f56365ec0638
Step 2/6 : RUN mkdir /files
 ---> Running in 982e6883bb13
Removing intermediate container 982e6883bb13
 ---> 0632577d852c
Step 3/6 : COPY hw.go /files
 ---> 68a0feb2e7dc
Step 4/6 : WORKDIR /files
 ---> Running in d7d4d0c846c2
Removing intermediate container d7d4d0c846c2
 ---> 6597a7cb3882
Step 5/6 : RUN go build -o /files/hw hw.go
 ---> Running in 324400d532e0
Removing intermediate container 324400d532e0
 ---> 5496dd3d09d1
Step 6/6 : ENTRYPOINT ["/files/hw"]
 ---> Running in bbd24840d6d4
    Removing intermediate container bbd24840d6d4
 ---> 5a0d2473aa96
    Successfully built 5a0d2473aa96
    Successfully tagged go_hw:v1
  

You can verify that the go_hw:v1 Docker image exists on your machine as follows:

$ docker images
REPOSITORY    TAG       IMAGE ID      CREATED              SIZE
go_hw         v1        9bec016712c4  About a minute ago   312MB
golang        alpine    f56365ec0638  11 days ago          310MB
  

The contents of the hw.go file are as follows:

package main 
 
import ( 
    "fmt" 
) 
 
func main() { 
    fmt.Println("Hello World!") 
} 

You can use a Docker image that is on your local computer as follows:

$ docker run go_hw:v1
Hello World!
  

There are other more complex ways to execute a Docker image, but for such a naive Docker image, this is the simplest way to use it.

If you want, you can store (push) a Docker image at a Docker registry on the internet in order to be able to retrieve it (pull) from there afterward.

Docker Hub can be such a place, provided that you have a Docker Hub account, which is easy to create and free. So, after creating a Docker Hub account, you should execute the following commands on your UNIX machine and push that image to Docker Hub:

$ docker login
Authenticating with existing credentials...
Login Succeeded
$ docker tag go_hw:v1 "mactsouk/go_hw:v1"
$ docker push "mactsouk/go_hw:v1"
The push refers to repository [docker.io/mactsouk/go_hw]
bdb6946938e3: Pushed
99e21c42e35d: Pushed
0257968d27b2: Pushed
e121936484eb: Pushed
61b145086eb8: Pushed
789935042c6f: Pushed
b14874cfef59: Pushed
7bff100f35cb: Pushed
v1: digest: 
sha256:c179d5d48a51b74b0883e582d53bf861c6884743eb51d9b77855949b5d91dd
e1 size: 1988

The first command is needed to log in to Docker Hub and should be executed only once. The docker tag command is needed for specifying the name that a local image will have on Docker Hub and should be executed before the docker push command. The last command sends the desired Docker image to Docker Hub, hence the rich output it generates. If you make your Docker image public, anyone will be able to pull it and use it.

You can delete one or more Docker images from your local UNIX machine in many ways. One of them is by using the IMAGE ID of a Docker image:

$ docker rmi 5a0d2473aa96 f56365ec0638
Untagged: go_hw:v1
Deleted: 
sha256:5a0d2473aa96bcdafbef92751a0e1c1bf146848966c8c971f462eb1eb242d2
a6
Deleted:
sha256:5496dd3d09d13c63bf7a9ac52b90bb812690cdfd33cfc3340509f9bfe6215c
48
Deleted:
sha256:598c4e474b123eccb84f41620d2568665b88a8f176a21342030917576b9d82
a8
Deleted:
sha256:6597a7cb3882b73855d12111787bd956a9ec3abb11d9915d32f2bba4d0e92e
c6
Deleted:
sha256:68a0feb2e7dc5a139eaa7ca04e54c20e34b7d06df30bcd4934ad6511361f2c
b8
Deleted:
sha256:c04452ea9f45d85a999bdc54b55ca75b6b196320c021d777ec1f766d115aa5
14
Deleted:
sha256:0632577d852c4f9b66c0eff2481ba06c49437e447761d655073eb034fa0ac3
33
Deleted:
sha256:52efd0fa2950c8f3c3e2e44fbc4eb076c92c0f85fff46a07e060f5974c1007
a9
Untagged: golang:alpine Untagged:
[email protected]:198cb8c94b9ee6941ce6d58f29aadb855f64600918ce602cdeacb01
8ad77d647
Deleted:
sha256:f56365ec0638b16b752af4bf17e6098f2fda027f8a71886d6849342266cc3a
b7
Deleted:
sha256:d6a4b196ed79e7ff124b547431f77e92dce9650037e76da294b3b3aded709b
dd
Deleted:
sha256:f509ec77b9b2390c745afd76cd8dd86977c86e9ff377d5663b42b664357c35
22
Deleted:
sha256:1ee98fa99e925362ef980e651c5a685ad04cef41dd80df9be59f158cf9e529
51
Deleted:
sha256:78c8e55f8cb4c661582af874153f88c2587a034ee32d21cb57ac1fef51c610
9e
Deleted:
sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7
b8
Docker is a huge and really important topic that we will revisit in several chapters of this book.
 

Exercises and links

 

Summary

This chapter talked about many interesting Go topics, including compiling Go code, working with standard input, standard output, and standard error in Go, processing command-line arguments, printing on the screen, and using the logging service of a UNIX system, as well as error handling and some general information about Go. You should consider all these topics as foundational information about Go.

The next chapter is all about the internals of Go, which includes talking about garbage collection, the Go compiler, calling C code from Go, the defer keyword, the Go assembler, and WebAssembly, as well as panic and recover.

About the Author

  • Mihalis Tsoukalos

    Mihalis Tsoukalos is an accomplished author. His previous books, Go Systems Programming and Mastering Go, have become a must-read for the Unix and Linux systems professionals. When not writing books, he spends his working life as a Unix administrator, programmer, DBA, and mathematician who enjoys writing technical articles and learning new technologies. His research interests include programming languages, visualization, and databases. He holds a BSc in Mathematics from the University of Patras and an MSc in IT from University College London, UK. He has written various technical articles for Sys Admin, MacTech, C/C++ Users Journal, Linux Journal, Linux User and Developer, Linux Format, and Linux Voice.

    Browse publications by this author

Latest Reviews

(5 reviews total)
excellent! good structure
The book is not for complete beginners in programming. It contains many useful examples of using Go and its standard library and can be used as a reference. It is a good introduction to Go for programmers experienced in other languages.
.

Recommended For You

The Go Workshop

Cut through the noise and get real results with a step-by-step approach to learning Go programming

By Delio D'Anna and 5 more
Hands-On Docker for Microservices with Python

A step-by-step guide to building microservices using Python and Docker, along with managing and orchestrating them with Kubernetes

By Jaime Buelta
The Python Workshop

Cut through the noise and get real results with a step-by-step approach to learning Python 3.X programming

By Andrew Bird and 4 more
Mastering PostgreSQL 12 - Third Edition

Master PostgreSQL 12 features such as advanced indexing, high availability, monitoring, and much more to efficiently manage and maintain your database

By Hans-Jürgen Schönig