Search icon CANCEL
Subscription
0
Cart icon
Cart
Close icon
You have no products in your basket yet
Save more on your purchases!
Savings automatically calculated. No voucher code required
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Mastering Go - Fourth Edition
Mastering Go - Fourth Edition

Mastering Go: Leverage Go's expertise for advanced utilities, empowering you to develop professional software, Fourth Edition

By Mihalis Tsoukalos
€32.99 €22.99
Book Mar 2024 736 pages 4th Edition
eBook
€32.99 €22.99
Print
€41.99
Subscription
€14.99 Monthly
eBook
€32.99 €22.99
Print
€41.99
Subscription
€14.99 Monthly

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon AI Assistant (beta) to help accelerate your learning
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Table of content icon View table of contents Preview book icon Preview Book

Mastering Go - Fourth Edition

A Quick Introduction to Go

Despite its name, this chapter is more than just a quick introduction to Go, as it is also going to be the foundation for the rest of the book. The basics of Go, some design decisions, and the philosophy of Go are explained in this chapter so that you can get the big picture before learning the details of Go. Among other things, we present the advantages and disadvantages of Go so that you know when to use Go and when to consider other alternatives.

In the sections that follow, we cover a number of concepts and utilities in order to build a solid foundation of Go, before building a simplified version of the which(1) utility, which is a UNIX utility that locates program files by searching the directories of the PATH environment variable. Additionally, we explain how to write information in log files, as this can help you store error messages and warnings while you are developing software in Go.

At the end of the chapter, we develop a basic command line utility that computes basic statistical properties. It is that command line utility that we are going to improve and expand in the remaining book chapters as we learn more advanced Go features.

The contents of this chapter are:

  • Introducing Go
  • When to use Go
  • Hello World!
  • Running Go code
  • What you should know about Go
  • Developing the which(1) utility in Go
  • Logging information
  • Developing a statistics application

Introducing Go

Go is an open-source systems programming language, initially developed as an internal Google project that went public back in 2009. The spiritual fathers of Go are Robert Griesemer, Ken Thomson, and Rob Pike.

Although the official name of the language is Go, it is sometimes (wrongly) referred to as Golang. The official reason for this is that https://go.org/ was not available for registration and golang.org was chosen instead—however, nowadays, the official Go website is https://go.dev/. Keep in mind that when you are querying a search engine for Go-related information, the word Go is usually interpreted as a verb; therefore, you should search for golang instead. Additionally, the official Twitter hashtag for Go is #golang.

Let us now discuss the history of Go and what that means for someone who wants to learn Go.

The history of Go

As mentioned earlier, Go started as an internal Google project that went public back in 2009. Griesemer, Thomson, and Pike designed Go as a language for professional programmers who want to build reliable, robust, and efficient software that is easy to manage. They designed Go with simplicity in mind, even if simplicity meant that Go was not going to be a programming language for everyone or everything.

The figure that follows shows the programming languages that directly or indirectly influenced Go. As an example, Go syntax looks like C, whereas the package concept was inspired by Modula-2.

A group of white squares with black text  Description automatically generated with low confidence

Figure 1.1: The programming languages that influenced Go

The deliverable was a programming language with tools and a standard library. What you get with Go, apart from its syntax and tools, is a rich standard library and a type system that tries to save you from easy mistakes, such as implicit type conversions, unused variables, and unused packages. The Go compiler catches most of these easy mistakes and refuses to compile until you do something about them. Additionally, the Go compiler can find difficult-to-catch mistakes such as race conditions.

If you are going to install Go for the first time, you can start by visiting https://go.dev/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.

As Go is a portable programming language, almost all presented code is going to work fine on any modern Microsoft Windows, Linux, or macOS machine without any changes. The only Go code that might need some small or big adjustments is the code that deals with the operating system. Most of that code is covered in Chapter 7, Telling a UNIX System What to Do.

The advantages of Go

Go comes with some important advantages for developers, starting with the fact that it was designed and is currently maintained by real programmers. Go is also easy to learn, especially if you are already familiar with programming languages such as C, Python, or Java. On top of that, due to its simplified and elegant syntax, Go code is pleasant to the eye, which is great, especially when you are programming applications for a living and you have to look at code on a daily basis. Go code is also easy to read, which means that you can make changes to existing Go code easily, and offers support for Unicode out of the box. Lastly, Go has reserved only 25 keywords, which makes it much easier to remember the language. Can you do that with C++?

Go also comes with concurrency capabilities, using a simple concurrency model that is implemented using goroutines and channels. Go manages OS threads for you and has a powerful runtime that allows you to spawn lightweight units of work (goroutines) that communicate with each other using channels.

Although Go comes with a rich standard library, there are really handy Go packages, such as cobra and viper, that allow Go to develop complex command line utilities such as docker and hugo. This is greatly supported by the fact that executable binaries are statically linked, which means that once they are generated, they do not depend on any shared libraries and include all required information. In practice, this means that you can transfer an existing executable file to a different machine with the same architecture and be sure that it is going to run without any issues.

Due to its simplicity, Go code is predictable and does not have strange side effects, and although Go supports pointers, it does not support pointer arithmetic like C, unless you use the unsafe package, which can be the root of many bugs and security holes. Although Go is not an object-oriented programming language, Go interfaces are very versatile and allow you to mimic some of the capabilities of object-oriented languages, such as polymorphism, encapsulation, and composition. However, Go offers no support for classes and inheritance. Chapter 5, Reflection and Interfaces, provides more information on the subject.

Additionally, the latest Go versions offer support for generics, which simplifies your code when working with multiple data types. You can learn more about Go generics in Chapter 4, Go Generics. Finally, Go is a garbage-collected language, which means that no manual memory management is needed.

When to use Go

Although Go is a general-purpose programming language, it is primarily used for writing system tools, command line utilities, web services, and software that works over networks and the internet. You can use Go for teaching programming, and it is a good candidate as your first programming language because of its lack of verbosity and clear ideas and principles.

Go can help you develop the following kinds of applications:

  • Professional web services
  • Networking tools and servers such as Kubernetes and Istio
  • Backend systems
  • Robust UNIX and Windows system tools
  • Servers that work with APIs and clients that interact by exchanging data in myriad formats, including JSON, XML, and CSV
  • WebSocket servers and clients
  • gRPC (Remote Procedure Call) servers and clients
  • Complex command line utilities with multiple commands, sub-commands, and command line parameters, such as docker and hugo
  • Applications that exchange data in the JSON format
  • Applications that process data from relational databases, NoSQL databases, or other popular data storage systems
  • Compilers and interpreters for your own programming languages
  • Database systems such as CockroachDB and key/value stores such as etcd

Although Go is a very practical and competent programming language, it is not perfect:

  • This is a personal preference rather than an actual technical shortcoming: Go has no direct and full support for object-oriented programming, which is a popular programming paradigm.
  • Although goroutines are lightweight, they are not as powerful as OS threads. Depending on the application you are trying to implement, there might exist some rare cases where goroutines will not be appropriate for the job. The Apache web server creates UNIX processes with fork(2) to serve its clients—Go does not support the functionality of fork(2). However, in most cases, designing your application with goroutines and channels in mind will solve your problems.
  • Although garbage collection is fast enough most of the time, and for almost all kinds of applications, there are times when you need to handle memory allocation manually, such as when developing an operating system or working with large chunks of memory and want to avoid fragmentation—Go cannot do that. In practice, this means that Go will not allow you to perform any memory management manually.
  • Go does not offer the full functionality of a functional programming language.
  • Go is not good at developing systems with high availability guarantees. In such cases, use Erlang or Elixir instead.

There are many things that Go does better than other programming languages, including the following:

  • The Go compiler catches a large set of silly errors that might end up being bugs. This includes imported Go packages and variables that are not being used in the code.
  • Go uses fewer parentheses than C, C++, or Java, and no semicolons, which makes Go source code more human-readable and less error-prone.
  • Go comes with a rich and reliable standard library that keeps improving.
  • Go has support for concurrency out of the box through goroutines and channels.
  • Goroutines are lightweight. You can easily run thousands of goroutines on any modern machine without any performance issues.
  • Unlike C, Go considers functions as first-class citizens.
  • Go code is backward compatible, which means that newer versions of the Go compiler accept programs that were created using a previous version of the language without any modifications. This compatibility guarantee is limited to major versions of Go. For example, there is no guarantee that a Go 1.x program will compile with Go 2.x.

The next subsection describes my personal Go journey.

My personal Go journey

In this subsection, I am going to tell you my personal story of how I ended up learning and using Go. I am a UNIX person, which means that I like UNIX and prefer to use it whenever possible. I also love C, and I used to like C++; I wrote a command line FTP client in C++ for my M.Sc. project. Nowadays, C++ is just a huge programming language that is difficult to learn. Although C continues to be a decent programming language, it requires lots of code to perform simple tasks and suffers from difficult-to-find and correct bugs, due to manual memory management and extremely flexible conversion between different data types without any warnings or error messages.

As a result, I used to use Perl to write simple command line utilities. However, Perl is far from perfect for writing serious command line tools and services, as it is a scripting programming language and is not intended for web development.

When I first heard about Go, that it was developed by Google, and that both Rob Pike and Ken Thomson were involved in its development, I instantly became interested in Go.

Since then, I have used Go to create web services, servers, and clients that communicate with RabbitMQ, MySQL, and PostgreSQL, create simple command line utilities, implement algorithms for time series data mining, create utilities that generate synthetic data, etc.

Soon, we are going to move on to actually learn some Go, using Hello World! as the first example, but before that, we will present the go doc command, which allows you to find information about the Go standard library, its packages, and their functions, as well as the godoc utility.

If you have not already installed Go, this is the right time to do so. To do that, visit https://go.dev/dl/ or use your favorite package manager.

The go doc and godoc utilities

The Go distribution comes with a plethora of tools that can make your life as a programmer easier. Two of these tools are the go doc subcommand and godoc utility, which allow you to see the documentation of existing Go functions and packages without needing an internet connection. However, if you prefer viewing the Go documentation online, you can visit https://pkg.go.dev/.

The go doc command can be executed as a normal command line application that displays its output on a terminal, and it is similar to the UNIX man(1) command, but for Go functions and packages only. So, in order to find out information about the Printf() function of the fmt package, you should execute the following command:

$ go doc fmt.Printf

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

$ go doc fmt

As godoc is not installed by default, you might need to install it by running go install golang.org/x/tools/cmd/godoc@latest. The godoc binary is going to be installed in ~/go/bin, and you can execute it as ~/go/bin/godoc unless ~/go/bin is in your PATH environment variable.

The godoc command line application starts a local web server. So you need a web browser to look at the Go documentation.

Running godoc requires executing godoc with the -http parameter:

$ ~/go/bin/godoc -http=:8001

The numeric value in the preceding command, which in this case is 8001, is the port number that the HTTP server will listen to. As we have omitted the IP address, godoc is going to listen to all network interfaces.

You can choose any port number that is available if you have the right privileges. However, note that port numbers 01023 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, if it is not already in use by a different process. Port number 8001 is usually free and is frequently used for local HTTP servers.

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

$ ~/go/bin/godoc -http :8001

After that, you should point your web browser to http://localhost:8001/ in order to get the list of available Go packages and browse their documentation. If no -http parameter is provided, godoc listens to port 6060.

If you are using Go for the first time, you will find the Go documentation very handy for learning the parameters and the return values of the functions you want to use — as you progress in your Go journey, you will use the Go documentation to learn the gory details of the functions and variables that you want to use.

The next section presents the first Go program of the book and explains the basic concepts of Go.

Hello World!

The following is the Go version of the Hello World! program. Please type it and save it as hw.go:

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

If you are eager to execute hw.go, type go run hw.go in the same directory where you save it. The file can also be found in the ch01 directory of the GitHub repository of the book.

Each Go source code begins with a package declaration. In this case, the name of the package is main, which has a special meaning in Go—autonomous Go programs should use the main package. The import keyword allows you to include functionality from existing packages. In our case, we only need some of the functionality of the fmt package that belongs to the standard Go library, implementing formatted input and output with functions that are analogous to C’s printf() and scanf(). The next important thing if you are creating an executable application is a main() function. Go considers this the entry point to the application, and it begins the execution of an application with the code found in the main() function of the main package.

hw.go is a Go program that runs on its own. Two characteristics make hw.go a source file that can generate an executable binary: the name of the package, which should be main, and the presence of the main() function—we discuss Go functions in more detail in the next subsection, but we will learn even more about functions and methods, which are functions attached to specific data types, in Chapter 6, Go Packages and Functions.

Introducing functions

Each Go function definition begins with the func keyword, followed by its name, signature, and implementation. Apart from the main() function, which has a special purpose, you can name the rest of your functions anything you want—there is a global Go rule that also applies to function and variable names and is valid for all packages except main: everything that begins with a lowercase letter is considered private and is accessible in the current package only. We will learn more about that rule in Chapter 6, Go Packages and Functions. The only exception to this rule is package names, which can begin with either lowercase or uppercase letters. Having said that, I am not aware of a Go package that begins with an uppercase letter!

You might now ask how functions are organized and delivered. Well, the answer is in packages—the next subsection sheds some light on that.

Introducing packages

Go programs are organized in packages—even the smallest Go program should be delivered as a package. The package keyword helps you define the name of a new package, which can be anything you want, with just one exception: if you are creating an executable application and not just a package that will be shared by other applications or packages, you should name your package main. You will learn more about developing Go packages in Chapter 6, Go Packages and Functions.

Packages can be used by other packages. In fact, reusing existing packages is a good practice that saves you from having to write lots of code or implement existing functionality from scratch.

The import keyword is used for importing other Go packages into your Go programs to use some or all of their functionality. A Go package can either be a part of the rich Standard Go library or come from an external source. Packages of the standard Go library are imported by name, for example, import "os" to use the os package, whereas external packages like github.com/spf13/cobra are imported using their full URLs: import "github.com/spf13/cobra".

Running Go code

You now need to know how to execute hw.go or any other Go application. As will be explained in the two subsections that follow, there are two ways to execute Go code: as a compiled language, using go build, or by mimicking a scripting language, using go run. So let us find out more about these two ways of running Go code.

Compiling Go code

To compile Go code and create a binary executable file, we need to use the go build command. What go build does is create an executable file for us to distribute and execute manually. This means that when using go build, an extra step is required to run the executable file.

The generated executable is automatically named after the source code filename without the .go file extension. Therefore, because of the hw.go source filename, the executable will be called hw. If this is not what you want, go build supports the -o option, which allows you to change the filename and the path of the generated executable file. As an example, if you want to name the executable file a helloWorld, you should execute go build -o helloWorld hw.go instead. If no source files are provided, go build looks for a main package in the current directory.

After that, you need to execute the generated executable binary file on your own. In our case, this means executing either hw or helloWorld. This is shown in the following output:

$ go build hw.go
$ ./hw
Hello World!

Now that we know how to compile Go code, let us continue using Go as if it were a scripting language.

Using Go like a scripting language

The go run command builds the named Go package, which in this case is the main package implemented in a single file, creates a temporary executable file, executes that file, and deletes it once it is done—to our eyes, this looks like using a scripting language while the Go compiler still creates a binary executable. In our case, we can do the following:

$ go run hw.go
Hello World!

Using go run is a better choice when testing code. However, if you want to create and distribute an executable binary, then go build is the way to go.

Important formatting and coding rules

You should know that Go comes with some strict formatting and coding rules that help a developer avoid rookie mistakes and bugs—once you learn these few rules and Go idiosyncrasies as well as the implications they have on your code, you will be free to concentrate on the actual functionality of your code. Additionally, the Go compiler is here to help you follow these rules with its expressive error messages and warnings. Last, Go offers standard tooling (gofmt) that can format your code for you, so you never have to think about it.

The following is a list of important Go rules that will help you while reading this chapter:

  • Go code is delivered in packages, and you are free to use the functionality found in existing packages. There is a Go rule that says that if you import a package, you should use it in some way (call a function or use a datatype), or the compiler is going to complain. There exist exceptions to this rule that mainly have to do with packages that initialize connections with database and TCP/IP servers, but they are not important for now. Packages are covered in Chapter 6, Go Packages and Functions.
  • You either use a variable or you do not declare it at all. This rule helps you avoid errors such as misspelling an existing variable or function name.
  • There is only one way to format curly braces in Go.
  • Coding blocks in Go are embedded in curly braces, even if they contain just a single statement or no statements at all.
  • Go functions can return multiple values.
  • You cannot automatically convert between different data types, even if they are of the same kind. As an example, you cannot implicitly convert an integer to a floating point.

Go has more rules, but the preceding ones are the most important and will keep you going for most of the book. You are going to see all these rules in action in this chapter as well as in other chapters. For now, let’s consider the only way to format curly braces in Go because this rule applies everywhere.

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 disappointed because the code will not compile and, therefore, you will get the following syntax error message:

$ go run curly.go
# command-line-arguments
./curly.go:7:6: missing function body
./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 implicitly 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 main cause of the error message. The correct way to write the previous code is the following:

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

After learning about this global rule, let us continue by presenting some important characteristics of Go.

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 named Global.
  • 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 type int, 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 is 0.
  • Another local variable named i—Go infers its data type from its value. As it is the sum of two int values, it is also an int.
  • As math.Abs() requires a float64 parameter, you cannot pass AnotherGlobal to it because AnotherGlobal is an int variable. The float64() type cast converts the value of AnotherGlobal to float64. Note that AnotherGlobal continues to have the int 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:

A picture containing text, screenshot, font, black  Description automatically generated

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.

Developing the which(1) utility in Go

Go can work with your operating system through a set of packages. A good way of learning a new programming language is by trying to implement simple versions of traditional UNIX utilities—in general, the only efficient way to learn a programming language is by writing lots of code in that language. In this section, you will see a Go version of the which(1) utility, which will help you understand the way Go interacts with the underlying OS and reads environment variables.

The presented code, which will implement the functionality of which(1), can be divided into three logical parts. The first part is about reading the input argument, which is the name of the executable file that the utility will be searching for. The second part is about reading the value stored in the PATH environment variable, splitting it, and iterating over the directories of the PATH variable. The third part is about looking for the desired binary file in these directories and determining whether it can be found or not, whether it is a regular file, and whether it is an executable file. If the desired executable file is found, the program terminates with the help of the return statement. Otherwise, it will terminate after the for loop ends and the main() function exits.

The presented source file is called which.go and is located under the ch01 directory of the GitHub repository of the book. Now, let us see the code, beginning with the logical preamble that usually includes the package name, the import statements, and other definitions with a global scope:

package main
import (
    "fmt"
    "os"
    "path/filepath"
)

The fmt package is used for printing onscreen, the os package is for interacting with the underlying operating system, and the path/filepath package is used for working with the contents of the PATH variable that is read as a long string, depending on the number of directories it contains.

The second logical part of the utility is the following:

func main() {
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Please provide an argument!")
        return
    }
    file := arguments[1]
    path := os.Getenv("PATH")
    pathSplit := filepath.SplitList(path)
    for _, directory := range pathSplit {

First, we read the command line arguments of the program (os.Args) and save the first command line argument into the file variable. Then, we get the contents of the PATH environment variable and split it using filepath.SplitList(), which offers a portable way of separating a list of paths. Lastly, we iterate over all the directories of the PATH variable using a for loop with range, as filepath.SplitList() returns a slice.

The rest of the utility contains the following code:

        fullPath := filepath.Join(directory, file)
        // Does it exist?
        fileInfo, err := os.Stat(fullPath)
        if err != nil {
            continue
        }
        mode := fileInfo.Mode()
        // Is it a regular file?
        if !mode.IsRegular() {
            continue
        }
        // Is it executable?
        if mode&0111 != 0 {
            fmt.Println(fullPath)
            return
        }
    }
}

We construct the full path that we examine using filepath.Join(), which is used for concatenating the different parts of a path using an OS-specific separator—this makes filepath.Join() work on all supported operating systems. In this part, we also get some lower-level information about the file—keep in mind that UNIX considers everything as a file, which means that we want to make sure that we are dealing with a regular file that is also executable.

Executing which.go generates the following kind of output:

$ go run which.go which
/usr/bin/which
$ go run which.go doesNotExist

The last command could not find the doesNotExist executable—according to the UNIX philosophy and the way UNIX pipes work, utilities generate no output onscreen if they have nothing to say.

Although it is useful to print error messages onscreen, there are times that you need to keep all error messages together and be able to search for them later when it is convenient for you. In this case, you need to use one or more log files.

The next section discusses logging in Go.

Logging information

All UNIX systems have their own log files for writing logging information that comes from running servers and programs. Usually, most system log files of a UNIX system 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.

Logging and storing logging information in log files is a practical way of examining data and information from your software asynchronously, either locally, at a central log server, or using server software such as Elasticsearch, Beats, and Grafana Loki.

Generally speaking, using a log file to write some information used to be considered a better practice than writing the same output on 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 in a terminal window. However, writing to log files is not always the best approach, mainly because many services run as Docker images, which have their own log files that get lost when the Docker image stops.

As we usually run our services via systemd, programs should log to stdout so that systemd can put logging data in the journal. https://12factor.net/logs offers more information about app logs. Additionally, in cloud-native applications, we are encouraged to simply log to stderr and let the container system redirect the stderr stream to the desired destination.

The UNIX logging service has support for two properties named logging level and logging facility. 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. The log package of the standard Go library does not support working with logging levels. The 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 correctly, it will not be handled; therefore, the log messages you send to it might get ignored and, therefore, lost.

The log package sends log messages to standard error. Part of the log package is the log/syslog package, which allows you to send log messages to the syslog server of your machine. Although by default log writes to standard error, the use of log.SetOutput() modifies that behavior. The list of functions for sending logging data includes log.Printf(), log.Print(), log.Println(), log.Fatalf(), log.Fatalln(), log.Panic(), log.Panicln(), and log.Panicf().

Logging is for application code, not library code. If you are developing libraries, do not put logging in them.

In order to write to system logs, you need to call the syslog.New() function with the appropriate parameters. Writing to the main system log file is as easy as calling syslog.New() with the syslog.LOG_SYSLOG option. After that, you need to tell your Go program that all logging information goes to the new logger—this is implemented with a call to the log.SetOutput() function. The process is illustrated in the following code—type it into your favorite plain text editor and save it as systemLog.go:

package main
import (
    "log"
    "log/syslog"
)
func main() {
    sysLog, err := syslog.New(syslog.LOG_SYSLOG, "systemLog.go")
    if err != nil {
        log.Println(err)
        return
    } else {
        log.SetOutput(sysLog)
        log.Print("Everything is fine!")
    }
}

After the call to log.SetOutput(), all logging information goes to the syslog logger variable which sends it to syslog.LOG_SYSLOG. Custom text for the log entries coming from that program is specified as the second parameter to the syslog.New() call.

Usually, we want to store logging data in user-defined files because they group relevant information, which makes them easier to process and inspect.

Running systemLog.go generates no output. However, if you execute journalctl -xe on a Linux machine, you can see entries like the following:

Jun 08 20:46:05 thinkpad systemLog.go[4412]: 2023/06/08 20:46:05 Everything is fine!
Jun 08 20:46:51 thinkpad systemLog.go[4822]: 2023/06/08 20:46:51 Everything is fine!

The output on your own operating system might be slightly different, but the general idea is the same.

Bad things happen all the time, even to good people and good software. So the next subsection covers the Go way of dealing with bad situations.

log.Fatal() and log.Panic()

The log.Fatal() function is used when something erroneous has happened and you just want to exit your program as soon as possible after reporting that bad situation. The call to log.Fatal() terminates a Go program at the point where log.Fatal() was called after printing an error message. In most cases, this custom error message can be Not enough arguments, Cannot access file, or similar. Additionally, it returns a non-zero exit code, which in UNIX indicates an error.

There are situations where a program is about to fail for good and you want to have as much information about the failure as possible—log.Panic() implies that something really unexpected and unknown, such as not being able to find a file that was previously accessed or not having enough disk space, has happened. Analogous to the log.Fatal() function, log.Panic() prints a custom message and immediately terminates the Go program.

Keep in mind that log.Panic() is equivalent to a call to log.Print(), followed by a call to panic(). This is a built-in function that stops the execution of the current function and begins panicking. After that, it returns to the caller function. Conversely, log.Fatal() calls log.Print() and then os.Exit(1), which is an immediate way of terminating the current program. Both log.Fatal() and log.Panic() are illustrated in the logs.go file, which contains the following Go code:

package main
import (
    "log"
    "os"
)
func main() {
    if len(os.Args) != 1 {
        log.Fatal("Fatal: Hello World!")
    }
    log.Panic("Panic: Hello World!")
}

If you call logs.go without any command line arguments, it calls log.Panic(). Otherwise, it calls log.Fatal(). This is illustrated in the following output from an Arch Linux system:

$ go run logs.go
2023/06/08 20:48:42 Panic: Hello World!
panic: Panic: Hello World!
goroutine 1 [running]:
log.Panic({0xc000104f60?, 0x0?, 0x0?})
    /usr/lib/go/src/log/log.go:384 +0x65
main.main()
    /home/mtsouk/code/mGo4th/ch01/logs.go:12 +0x85
exit status 2
$ go run logs.go 1
2023/06/08 20:48:59 Fatal: Hello World!
exit status 1

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

Please keep in mind that both of these functions terminate the program abruptly, which may not be what the user wants. As a result, they are not the best way to end a program. However, they can be handy for reporting really bad error conditions or unexpected situations. Two such examples are when a program is unable to save its data or when a configuration file is not found.

The next subsection is about writing to custom log files.

Writing to a custom log file

Most of the time, and especially on applications and services that are deployed to production, you need to write your logging data in a log file of your choice. This can be for many reasons, including writing debugging data without messing with the system log files, or keeping your own logging data separate from system logs to transfer it or store it in a database or software, like Elasticsearch. This subsection teaches you how to write to a custom log file that is usually application-specific.

Writing to files and file input and output are both covered in Chapter 7, Telling a UNIX System What to Do—however, saving information to files is very handy when troubleshooting and debugging Go code, which is why this is covered in the first chapter.

The path of the log file (mGo.log) that is used is stored on a variable named LOGFILE—this is created using the os.TempDir() function, which returns the default directory used on the current OS for temporary files, in order to prevent your file system from getting full in case something goes wrong.

Additionally, at this point, this will save you from having to execute customLog.go with root privileges and putting unnecessary files into precious system directories.

Type the following code and save it as customLog.go:

package main
import (
    "fmt"
    "log"
    "os"
    "path"
)
func main() {
    LOGFILE := path.Join(os.TempDir(), "mGo.log")
    fmt.Println(LOGFILE)
    f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// The call to os.OpenFile() creates the log file for writing, 
// if it does not already exist, or opens it for writing 
// by appending new data at the end of it (os.O_APPEND)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

The defer keyword tells Go to execute the statement just before the current function returns. This means that f.Close() is going to be executed just before main() returns. We will go into more detail on defer in Chapter 6, Go Packages and Functions:

    iLog := log.New(f, "iLog ", log.LstdFlags)
    iLog.Println("Hello there!")
    iLog.Println("Mastering Go 4th edition!")
}

The last three statements create a new log file based on an opened file (f) and write two messages to it, using Println().

If you ever decide to use the code of customLog.go in a real application, you should change the path stored in LOGFILE to something that makes more sense.

Running customLog.go on an Arch Linux machine prints the file path of the log file:

$ go run customLog.go
/tmp/mGo.log

Depending on your operating system, your output might vary. However, what is important is what has been written in the custom log file:

$ cat /tmp/mGo.log
iLog 2023/11/27 22:15:10 Hello there!
iLog 2023/11/27 22:15:10 Mastering Go 4th edition!

The next subsection shows how to print line numbers in log entries.

Printing line numbers in log entries

In this subsection, you will learn how to print the filename as well as the line number in the source file where the statement that wrote a log entry is located.

The desired functionality is implemented with the use of log.Lshortfile in the parameters of log.New() or SetFlags(). The log.Lshortfile flag adds the filename as well as the line number of the Go statement that printed the log entry in the log entry itself. If you use log.Llongfile instead of log.Lshortfile, then you get the full path of the Go source file—usually, this is not necessary, especially when you have a really long path.

Type the following code and save it as customLogLineNumber.go:

package main
import (
    "fmt"
    "log"
    "os"
    "path"
)
func main() {
    LOGFILE := path.Join(os.TempDir(), "mGo.log")
    fmt.Println(LOGFILE)
    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()
    LstdFlags := log.Ldate | log.Lshortfile
    iLog := log.New(f, "LNum ", LstdFlags)
    iLog.Println("Mastering Go, 4th edition!")
    iLog.SetFlags(log.Lshortfile | log.LstdFlags)
    iLog.Println("Another log entry!")
}

In case you are wondering, you are allowed to change the format of the log entries during program execution—this means that when there is a reason, you can print more analytical information in the log entries. This is implemented with multiple calls to iLog.SetFlags().

Running customLogLineNumber.go generates the following output:

$ go run customLogLineNumber.go
/var/folders/sk/ltk8cnw50lzdtr2hxcj5sv2m0000gn/T/mGo.log

It also writes the following entries in the file path that is specified by the value of the LOGFILE global variable:

$ cat /var/folders/sk/ltk8cnw50lzdtr2hxcj5sv2m0000gn/T/mGo.log
LNum 2023/06/08 customLogLineNumber.go:25: Mastering Go, 4th edition!
LNum 2023/06/08 20:58:09 customLogLineNumber.go:28: Another log entry!

The first error message is from source code line 25, whereas the second one is from source code line 28.

You will most likely get a different output on your own machine, which is the expected behavior.

Writing to multiple log files

This subsection shows a technique for writing to multiple log files—this is illustrated in multipleLogs.go, which can be found in the GitHub repository of the book under directory ch01 and comes with the following code:

package main
import (
    "fmt"
    "io"
    "log"
    "os"
)
func main() {
    flag := os.O_APPEND | os.O_CREATE | os.O_WRONLY
    file, err := os.OpenFile("myLog.log", flag, 0644)
    if err != nil {
        fmt.Println(err)
        os.Exit(0)
    }
    defer file.Close()
    w := io.MultiWriter(file, os.Stderr)
    logger := log.New(w, "myApp: ", log.LstdFlags)
    logger.Printf("BOOK %d", os.Getpid())
}

The io.MultiWriter() function is what allows us to write to multiple destinations, which in this case are a file named myLog.log and standard error.

The results of running multipleLogs.go can be seen in the myLog.log file, which is going to be created in the current working directory, and to standard error, which is usually presented on screen:

$ go run multipleLogs.go
myApp: 2023/06/24 21:02:55 BOOK 71457

The contents of myLog.log are the same as before:

$ at myLog.log
myApp: 2023/06/24 21:02:55 BOOK 71457

In the next section, we are going to write the first version of the statistics application.

Developing a statistics application

In this section, we are going to develop a basic statistics application stored in stats.go. The statistical application is going to be improved and enriched with new features throughout this book.

The first part of stats.go is the following:

package main
import (
    "fmt"
    "math"
    "os"
    "strconv"
)
func main() {
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Need one or more arguments!")
        return
    }

In this first part of the application, the necessary Go packages are imported before the main() function makes sure that we have at least a single command line parameter to work with, using len(arguments) == 1.

The second part of stats.go is the following:

    var min, max float64
    var initialized = 0
    nValues := 0
    var sum float64
    for i := 1; i < len(arguments); i++ {
        n, err := strconv.ParseFloat(arguments[i], 64)
        if err != nil {
            continue
        }
        nValues = nValues + 1
        sum = sum + n
        if initialized == 0 {
            min = n
            max = n
            initialized = 1
            continue
        }
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    fmt.Println("Number of values:", nValues)
    fmt.Println("Min:", min)
    fmt.Println("Max:", max)

In the previous code excerpt, we process all valid inputs to count the number of valid values and find the minimum and the maximum values among them.

The last part of stats.go is the following:

    // Mean value
    if nValues == 0 {
        return
    }
meanValue := sum / float64(nValues)
    fmt.Printf("Mean value: %.5f\n", meanValue)
    // Standard deviation
    var squared float64
for i := 1; i < len(arguments); i++ {
        n, err := strconv.ParseFloat(arguments[i], 64)
        if err != nil {
            continue
        }
        squared = squared + math.Pow((n-meanValue), 2)
    }
    standardDeviation := math.Sqrt(squared / float64(nValues))
    fmt.Printf("Standard deviation: %.5f\n", standardDeviation)
}

In the previous code excerpt, we find the mean value because this cannot be computed without processing all values first. After that, we process each valid value to compute the standard deviation because the mean value is required in order to compute the standard deviation.

Running stats.go generates the following kind of output:

$ go run stats.go 1 2 3
Number of values: 3
Min: 1
Max: 3
Mean value: 2.00000
Standard deviation: 0.81650

Summary

At the beginning of this chapter, we discussed the advantages, disadvantages, philosophy, and history of Go. Then, the basics of Go were presented, which include variables, iterations, and flow control as well as how to log data.

After that, we learned about logging, implemented which(1), and created a basic statistics application.

The next chapter is all about the basic Go data types.

Exercises

Test out what you have learned by trying to complete the following exercises:

  • Read the documentation of the fmt package using go doc.
  • In UNIX, an exit code of 0 means success, whereas a non-zero exit code usually means failure. Try to modify which.go to do so with the help of os.Exit().
  • The current version of which(1) stops after finding the first occurrence of the desired executable. Make the necessary code changes to which.go in order to find all possible occurrences of the desired executable.

Additional resources

Leave a review!

Enjoying this book? Help readers like you by leaving an Amazon review. Scan the QR code below to get a free eBook of your choice.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Fully updated with coverage of web services, TCP/IP, REST APIs, Go Generics, and Fuzzy Testing
  • Apply your new knowledge to real-world exercises, building high-performance servers and robust command-line utilities, to deepen your learning
  • Gain clarity on what makes Go different, understand its nuances and features for smoother Go development

Description

Mastering Go, now in its fourth edition, remains the go-to resource for real-world Go development. This comprehensive guide delves into advanced Go concepts, including RESTful servers, and Go memory management. This edition brings new chapters on Go Generics and fuzzy Testing, and an enriched exploration of efficiency and performance. As you work your way through the chapters, you will gain confidence and a deep understanding of advanced Go topics, including concurrency and the operation of the Garbage Collector, using Go with Docker, writing powerful command-line utilities, working with JavaScript Object Notation (JSON) data, and interacting with databases. You will be engaged in real-world exercises, build network servers, and develop robust command-line utilities. With in-depth chapters on RESTful services, the WebSocket protocol, and Go internals, you are going to master Go's nuances, optimization, and observability. You will also elevate your skills in efficiency, performance, and advanced testing. With the help of Mastering Go, you will become an expert Go programmer by building Go systems and implementing advanced Go techniques in your projects.

What you will learn

Learn Go data types, error handling, constants, pointers, and array and slice manipulations through practical exercises Create generic functions, define data types, explore constraints, and grasp interfaces and reflections Grasp advanced concepts like packages, modules, functions, and database interaction Create concurrent RESTful servers, and build TCP/IP clients and servers Learn testing, profiling, and efficient coding for high-performance applications Develop an SQLite package, explore Docker integration, and embrace workspaces

Product Details

Country selected

Publication date : Mar 29, 2024
Length 736 pages
Edition : 4th Edition
Language : English
ISBN-13 : 9781805127147
Vendor :
Google
Category :
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon AI Assistant (beta) to help accelerate your learning
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want

Product Details


Publication date : Mar 29, 2024
Length 736 pages
Edition : 4th Edition
Language : English
ISBN-13 : 9781805127147
Vendor :
Google
Category :
Languages :
Tools :

Table of Contents

19 Chapters
Preface Chevron down icon Chevron up icon
1. A Quick Introduction to Go Chevron down icon Chevron up icon
2. Basic Go Data Types Chevron down icon Chevron up icon
3. Composite Data Types Chevron down icon Chevron up icon
4. Go Generics Chevron down icon Chevron up icon
5. Reflection and Interfaces Chevron down icon Chevron up icon
6. Go Packages and Functions Chevron down icon Chevron up icon
7. Telling a UNIX System What to Do Chevron down icon Chevron up icon
8. Go Concurrency Chevron down icon Chevron up icon
9. Building Web Services Chevron down icon Chevron up icon
10. Working with TCP/IP and WebSocket Chevron down icon Chevron up icon
11. Working with REST APIs Chevron down icon Chevron up icon
12. Code Testing and Profiling Chevron down icon Chevron up icon
13. Fuzz Testing and Observability Chevron down icon Chevron up icon
14. Efficiency and Performance Chevron down icon Chevron up icon
15. Changes in Recent Go Versions Chevron down icon Chevron up icon
16. Other Books You May Enjoy Chevron down icon Chevron up icon
17. Index Chevron down icon Chevron up icon
Appendix: The Go Garbage Collector Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Empty star icon Empty star icon Empty star icon Empty star icon Empty star icon 0
(0 Ratings)
5 star 0%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
Top Reviews
No reviews found
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.