1
GO FUNDAMENTALS

Image

This chapter will guide you through the process of setting up your Go development environment and introduce you to the language’s syntax. People have written entire books on the fundamental mechanics of the language; this chapter covers the most basic concepts you’ll need in order to work through the code examples in the following chapters. We’ll cover everything from primitive data types to implementing concurrency. For readers who are already well versed in the language, you’ll find much of this chapter to be a review.

Setting Up a Development Environment

To get started with Go, you’ll need a functional development environment. In this section, we’ll walk you through the steps to download Go and set up your workspace and environment variables. We’ll discuss various options for your integrated development environment and some of the standard tooling that comes with Go.

Downloading and Installing Go

Start by downloading the Go binary release most appropriate to your operating system and architecture from https://golang.org/dl/. Binaries exist for Windows, Linux, and macOS. If you’re using a system that doesn’t have an available precompiled binary, you can download the Go source code from that link.

Execute the binary and follow the prompts, which will be minimal, in order to install the entire set of Go core packages. Packages, called libraries in most other languages, contain useful code you can use in your Go programs.

Setting GOROOT to Define the Go Binary Location

Next, the operating system needs to know how to find the Go installation. In most instances, if you’ve installed Go in the default path, such as /usr/local/go on a *Nix/BSD-based system, you don’t have to take any action here. However, in the event that you’ve chosen to install Go in a nonstandard path or are installing Go on Windows, you’ll need to tell the operating system where to find the Go binary.

You can do this from your command line by setting the reserved GOROOT environment variable to the location of your binary. Setting environment variables is operating-system specific. On Linux or macOS, you can add this to your ~/.profile:

set GOROOT=/path/to/go

On Windows, you can add this environment variable through the System (Control Panel), by clicking the Environment Variables button.

Setting GOPATH to Determine the Location of Your Go Workspace

Unlike setting your GOROOT, which is necessary in only certain installation scenarios, you must always define an environment variable named GOPATH to instruct the Go toolset where your source code, third-party libraries, and compiled programs will exist. This can be any location of your choosing. Once you’ve chosen or created this base workspace directory, create the following three subdirectories within: bin, pkg, and src (more on these directories shortly). Then, set an environment variable named GOPATH that points to your base workspace directory. For example, if you want to place your projects in a directory called gocode located within your home directory on Linux, you set GOPATH to the following:

GOPATH=$HOME/gocode

The bin directory will contain your compiled and installed Go executable binaries. Binaries that are built and installed will be automatically placed into this location. The pkg directory stores various package objects, including third-party Go dependencies that your code might rely on. For example, perhaps you want to use another developer’s code that more elegantly handles HTTP routing. The pkg directory will contain the binary artifacts necessary to consume their implementation in your code. Finally, the src directory will contain all the evil source code you’ll write.

The location of your workspace is arbitrary, but the directories within must match this naming convention and structure. The compilation, build, and package management commands you’ll learn about later in this chapter all rely on this common directory structure. Without this important setup, Go projects won’t compile or be able to locate any of their necessary dependencies!

After configuring the necessary GOROOT and GOPATH environment variables, confirm that they’re properly set. You can do this on Linux and Windows via the set command. Also, check that your system can locate the binary and that you’ve installed the expected Go version with the go version command:

$ go version
go version go1.11.5 linux/amd64

This command should return the version of the binary you installed.

Choosing an Integrated Development Environment

Next, you’ll probably want to select an integrated development environment (IDE) in which to write your code. Although an IDE isn’t required, many have features that help reduce errors in your code, add version-control shortcuts, aid in package management, and more. As Go is still a fairly young language, there may not be as many mature IDEs as for other languages.

Fortunately, advancements over the last few years leave you with several, full-featured options. We’ll review some of them in this chapter. For a more complete list of IDE or editor options, check out the Go wiki page at https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins/. This book is IDE/editor agnostic, meaning we won’t force you into any one solution.

Vim Editor

The Vim text editor, available in many operating-system distributions, provides a versatile, extensible, and completely open source development environment. One appealing feature of Vim is that it lets users run everything from their terminal without fancy GUIs getting in the way.

Vim contains a vast ecosystem of plug-ins through which you can customize themes, add version control, define snippets, add layout and code-navigation features, include autocomplete, perform syntax highlighting and linting, and much, much more. Vim’s most common plug-in management systems include Vundle and Pathogen.

To use Vim for Go, install the vim-go plug-in (https://github.com/fatih/vim-go/) shown in Figure 1-1.

Image

Figure 1-1: The vim-go plug-in

Of course, to use Vim for Go development, you’ll have to become comfortable with Vim. Further, customizing your development environment with all the features you desire might be a frustrating process. If you use Vim, which is free, you’ll likely need to sacrifice some of the conveniences of commercial IDEs.

GitHub Atom

GitHub’s IDE, called Atom (https://atom.io/), is a hackable text editor with a large offering of community-driven packages. Unlike Vim, Atom provides a dedicated IDE application rather than an in-terminal solution, as shown in Figure 1-2.

Image

Figure 1-2: Atom with Go support

Like Vim, Atom is free. It provides tiling, package management, version control, debugging, autocomplete, and a myriad of additional features out of the box or through the use of the go-plus plug-in, which provides dedicated Go support (https://atom.io/packages/go-plus/).

Microsoft Visual Studio Code

Microsoft’s Visual Studio Code, or VS Code (https://code.visualstudio.com), is arguably one of the most feature-rich and easiest IDE applications to configure. VS Code, shown in Figure 1-3, is completely open source and distributed under an MIT license.

Image

Figure 1-3: The VS Code IDE with Go support

VS Code supports a diverse set of extensions for themes, versioning, code completion, debugging, linting, and formatting. You can get Go integration with the vscode-go extension (https://github.com/Microsoft/vscode-go/).

JetBrains GoLand

The JetBrains collection of development tools are efficient and feature-rich, making both professional development and hobbyist projects easy to accomplish. Figure 1-4 shows what the JetBrains GoLand IDE looks like.

GoLand is the JetBrains commercial IDE dedicated to the Go language. Pricing for GoLand ranges from free for students, to $89 annually for individuals, to $199 annually for organizations. GoLand offers all the expected features of a rich IDE, including debugging, code completion, version control, linting, formatting, and more. Although paying for a product may not sound appealing, commercial products such as GoLand typically have official support, documentation, timely bug fixes, and some of the other assurances that come with enterprise software.

Image

Figure 1-4: The GoLand commercial IDE

Using Common Go Tool Commands

Go ships with several useful commands that simplify the development process. The commands themselves are commonly included in IDEs, making the tooling consistent across development environments. Let’s take a look at some of these commands.

The go run Command

One of the more common commands you’ll execute during development, go run will compile and execute the main package—your program’s entry point.

As an example, save the following code under a project directory within $GOPATH/src (remember, you created this workspace during installation) as main.go:

package main
import (
    "fmt"
)
func main() {
    fmt.Println("Hello, Black Hat Gophers!")
}

From the command line, within the directory containing this file, execute go run main.go. You should see Hello, Black Hat Gophers! printed to your screen.

The go build Command

Note that go run executed your file, but it didn’t produce a standalone binary file. That’s where go build comes in. The go build command compiles your application, including any packages and their dependencies, without installing the results. It creates a binary file on disk but doesn’t execute your program. The files it creates follow reasonable naming conventions, but it’s not uncommon to change the name of the created binary file by using the -o output command line option.

Rename main.go from the previous example to hello.go. In a terminal window, execute go build hello.go. If everything goes as intended, this command should create an executable file with the name hello. Now enter this command:

$ ./hello
Hello, Black Hat Gophers!

This should run the standalone binary file.

By default, the produced binary file contains debugging information and the symbol table. This can bloat the size of the file. To reduce the file size, you can include additional flags during the build process to strip this information from the binary. For example, the following command will reduce the binary size by approximately 30 percent:

$ go build -ldflags "-w -s"

Having a smaller binary will make it more efficient to transfer or embed while pursuing your nefarious endeavors.

Cross-Compiling

Using go build works great for running a binary on your current system or one of identical architecture, but what if you want to create a binary that can run on a different architecture? That’s where cross-compiling comes in. Cross-compiling is one of the coolest aspects of Go, as no other language can do it as easily. The build command allows you to cross-compile your program for multiple operating systems and architectures. Reference the official Go documentation at https://golang.org/doc/install/source#environment/ for further details regarding allowable combinations of compatible operating system and architecture compilation types.

To cross-compile, you need to set a constraint. This is just a means to pass information to the build command about the operating system and architecture for which you’d like to compile your code. These constraints include GOOS (for the operating system) and GOARCH (for the architecture).

You can introduce build constraints in three ways: via the command line, code comments, or a file suffix naming convention. We’ll discuss the command line method here and leave the other two methods for you to research if you wish.

Let’s suppose that you want to cross-compile your previous hello.go program residing on a macOS system so that it runs on a Linux 64-bit architecture. You can accomplish this via the command line by setting the GOOS and GOARCH constraints when running the build command:

$ GOOS="linux" GOARCH="amd64" go build hello.go
$ ls
hello  hello.go
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

The output confirms that the resulting binary is a 64-bit ELF (Linux) file.

The cross-compilation process is much simpler in Go than in just about any other modern programming language. The only real “gotcha” happens when you try to cross-compile applications that use native C bindings. We’ll stay out of the weeds and let you dig into those challenges independently. Depending on the packages you import and the projects you develop, you may not have to worry about that very often.

The go doc Command

The go doc command lets you interrogate documentation about a package, function, method, or variable. This documentation is embedded as comments through your code. Let’s take a look at how to obtain details about the fmt.Println() function:

$ go doc fmt.Println
func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline
    is appended. It returns the number of bytes written and any write error
    encountered.

The output that go doc produces is taken directly out of the source code comments. As long as you adequately comment your packages, functions, methods, and variables, you’ll be able to automatically inspect the documentation via the go doc command.

The go get Command

Many of the Go programs that you’ll develop in this book will require third-party packages. To obtain package source code, use the go get command. For instance, let’s assume you’ve written the following code that imports the stacktitan/ldapauth package:

   package main

   import (
   "fmt"
   "net/http"

 "github.com/stacktitan/ldapauth"
   )

Even though you’ve imported the stacktitan/ldapauth package , you can’t access the package quite yet. You first have to run the go get command. Using go get github.com/stacktitan/ldapauth downloads the actual package and places it within the $GOPATH/src directory.

The following directory tree illustrates the placement of the ldapauth package within your GOPATH workspace:

   $ tree src/github.com/stacktitan/
 src/github.com/stacktitan/
   └── ldapauth
       ├── LICENSE
       ├── README.md
       └── ldap_auth.go

Notice that the path and the imported package name are constructed in a way that avoids assigning the same name to multiple packages. Using github.com/stacktitan as a preface to the actual package name ldapauth ensures that the package name remains unique.

Although Go developers traditionally install dependencies with go get, problems can arise if those dependent packages receive updates that break backward compatibility. Go has introduced two separate tools—dep and mod—to lock dependencies in order to prevent backward compatibility issues. However, this book almost exclusively uses go get to pull down dependencies. This will help avoid inconsistencies with ongoing dependency management tooling and hopefully make it easier for you to get the examples up and running.

The go fmt Command

The go fmt command automatically formats your source code. For example, running go fmt /path/to/your/package will style your code by enforcing the use of proper line breaks, indentation, and brace alignment.

Adhering to arbitrary styling preferences might seem strange at first, particularly if they differ from your habits. However, you should find this consistency refreshing over time, as your code will look similar to other third-party packages and feel more organized. Most IDEs contain hooks that will automatically run go fmt when you save your file, so you don’t need to explicitly run the command.

The golint and go vet Commands

Whereas go fmt changes the syntactical styling of your code, golint reports style mistakes such as missing comments, variable naming that doesn’t follow conventions, useless type specifications, and more. Notice that golint is a standalone tool, and not a subcommand of the main go binary. You’ll need to install it separately by using go get -u golang.org/x/lint/golint.

Similarly, go vet inspects your code and uses heuristics to identify suspicious constructs, such as calling Printf() with the incorrect format string types. The go vet command attempts to identify issues, some of which might be legitimate bugs, that a compiler might miss.

Go Playground

The Go Playground is an execution environment hosted at https://play.golang.org/ that provides a web-based frontend for developers to quickly develop, test, execute, and share snippets of Go code. The site makes it easy to try out various Go features without having to install or run Go on your local system. It’s a great way to test snippets of code before integrating them within your projects.

It also allows you to simply play with various nuances of the language in a preconfigured environment. It’s worth noting that the Go Playground restricts you from calling certain dangerous functions to prevent you from, for example, executing operating-system commands or interacting with third-party websites.

Other Commands and Tools

Although we won’t explicitly discuss other tools and commands, we encourage you to do your own research. As you create increasingly complex projects, you’re likely to run into a desire to, for example, use the go test tool to run unit tests and benchmarks, cover to check for test coverage, imports to fix import statements, and more.

Understanding Go Syntax

An exhaustive review of the entire Go language would take multiple chapters, if not an entire book. This section gives a brief overview of Go’s syntax, particularly relative to data types, control structures, and common patterns. This should act as a refresher for casual Go coders and an introduction for those new to the language.

For an in-depth, progressive review of the language, we recommend that you work through the excellent A Tour of Go (https://tour.golang.org/) tutorial. It’s a comprehensive, hands-on discussion of the language broken into bite-sized lessons that use an embedded playground to enable you to try out each of the concepts.

The language itself is a much cleaner version of C that removes a lot of the lower-level nuances, resulting in better readability and easier adoption.

Data Types

Like most modern programming languages, Go provides a variety of primitive and complex data types. Primitive types consist of the basic building blocks (such as strings, numbers, and booleans) that you’re accustomed to in other languages. Primitives make up the foundation of all information used within a program. Complex data types are user-defined structures composed of a combination of one or more primitive or other complex types.

Primitive Data Types

The primitive types include bool, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, byte, rune, float32, float64, complex64, and complex128.

You typically declare a variable’s type when you define it. If you don’t, the system will automatically infer the variable’s data type. Consider the following examples:

var x = "Hello World"
z := int(42)

In the first example, you use the keyword var to define a variable named x and assign to it the value "Hello World". Go implicitly infers x to be a string, so you don’t have to declare that type. In the second example, you use the := operator to define a new variable named z and assign to it an integer value of 42. There really is no difference between the two operators. We’ll use both throughout this book, but some people feel that the := operator is an ugly symbol that reduces readability. Choose whatever works best for you.

In the preceding example, you explicitly wrap the 42 value in an int call to force a type on it. You could omit the int call but would have to accept whatever type the system automatically uses for that value. In some cases, this won’t be the type you intended to use. For instance, perhaps you want 42 to be represented as an unsigned integer, rather than an int type, in which case you’d have to explicitly wrap the value.

Slices and Maps

Go also has more-complex data types, such as slices and maps. Slices are like arrays that you can dynamically resize and pass to functions more efficiently. Maps are associative arrays, unordered lists of key/value pairs that allow you to efficiently and quickly look up values for a unique key.

There are all sorts of ways to define, initialize, and work with slices and maps. The following example demonstrates a common way to define both a slice s and a map m and add elements to both:

var s = make([]string, 0)
var m = make(map[string]string)
s = append(s, "some string")
m["some key"] = "some value"

This code uses the two built-in functions: make() to initialize each variable and append() to add a new item to a slice. The last line adds the key/value pair of some key and some value to the map m. We recommend that you read the official Go documentation to explore all the methods for defining and using these data types.

Pointers, Structs, and Interfaces

A pointer points to a particular area in memory and allows you to retrieve the value stored there. As you do in C, you use the & operator to retrieve the address in memory of some variable, and the * operator to dereference the address. The following example illustrates this:

 var count = int(42)
 ptr := &count
 fmt.Println(*ptr)
 *ptr = 100
 fmt.Println(count)

The code defines an integer, count , and then creates a pointer by using the & operator. This returns the address of the count variable. You dereference the variable while making a call to fmt.Println() to log the value of count to stdout. You then use the * operator to assign a new value to the memory location pointed to by ptr. Because this is the address of the count variable, the assignment changes the value of that variable, which you confirm by printing it to the screen .

You use the struct type to define new data types by specifying the type’s associated fields and methods. For example, the following code defines a Person type:

 type Person struct {
     Name string
     Age int
   }
 func (p *Person) SayHello() {
       fmt.Println("Hello,", p.Name)
   }
   func main() {
       var guy =  new(Person)
     guy.Name = "Dave"
     guy.SayHello()
   }

The code uses the type keyword to define a new struct containing two fields: a string named Name and an int named Age .

You define a method, SayHello(), on the Person type assigned to variable p . The method prints a greeting message to stdout by looking at the struct, p , that received the call. Think of p as a reference to self or this in other languages. You also define a function, main(), which acts as the program’s entry point. This function uses the new keyword to initialize a new Person. It assigns the name Dave to the person and then tells the person to SayHello() .

Structs lack scoping modifiers—such as private, public, or protected—that are commonly used in other languages to control access to their members. Instead, Go uses capitalization to determine scope: types and fields that begin with a capital letter are exported and accessible outside the package, whereas those starting with a lowercase letter are private, accessible only within the package.

You can think of Go’s interface type as a blueprint or a contract. This blueprint defines an expected set of actions that any concrete implementation must fulfill in order to be considered a type of that interface. To define an interface, you define a set of methods; any data type that contains those methods with the correct signatures fulfills the contract and is considered a type of that interface. Let’s take a look at an example:

 type Friend interface {
     SayHello()
   }

In this sample, you’ve defined an interface called Friend that requires one method to be implemented: SayHello() . That means that any type that implements the SayHello() method is a Friend. Notice that the Friend interface doesn’t actually implement that function—it just says that if you’re a Friend, you need to be able to SayHello().

The following function, Greet(), takes a Friend interface as input and says hello in a Friend-specific way:

func Greet (f Friend) {
    f.SayHello()
}

You can pass any Friend type to the function. Luckily, the Person type used in the previous example can SayHello()—it’s a Friend. Therefore, if a function named Greet() , as shown in the preceding code, expects a Friend as an input parameter , you can pass it a Person, like this:

func main() {
    var guy = new(Person)
    guy.Name = "Dave"
    Greet(guy)
}

Using interfaces and structs, you can define multiple types that you can pass to the same Greet() function, so long as these types implement the Friend interface. Consider this modified example:

 type Dog struct {}
   func (d *Dog) SayHello() {
       fmt.Println("Woof woof")
   }
   func main() {
       var guy = new(Person)
       guy.Name = "Dave"
     Greet(guy)
       var dog = new(Dog)
     Greet(dog)
   }

The example shows a new type, Dog , that is able to SayHello() and, therefore, is a Friend. You are able to Greet() both a Person and a Dog , since both are capable of SayHello().

We’ll cover interfaces multiple times throughout the book to help you better understand the concept.

Control Structures

Go contains slightly fewer control structures than other modern languages. Despite that, you can still accomplish complex processing, including conditionals and loops, with Go.

Go’s primary conditional is the if/else structure:

if x == 1 {
    fmt.Println("X is equal to 1")
} else {
    fmt.Println("X is not equal to 1")
}

Go’s syntax deviates slightly from the syntax of other languages. For instance, you don’t wrap the conditional check—in this case, x == 1—in parentheses. You must wrap all code blocks, even the preceding single-line blocks, in braces. Many other modern languages make the braces optional for single-line blocks, but they’re required in Go.

For conditionals involving more than two choices, Go provides a switch statement. The following is an example:

switch x {
    case "foo":
        fmt.Println("Found foo")
    case "bar":
        fmt.Println("Found bar")
    default:
        fmt.Println("Default case")
}

In this example, the switch statement compares the contents of a variable x against various values—foo and bar —and logs a message to stdout if x matches one of the conditions. This example includes a default case , which executes in the event that none of the other conditions match.

Note that, unlike many other modern languages, your cases don’t have to include break statements. In other languages, execution often continues through each of the cases until the code reaches a break statement or the end of the switch. Go will execute no more than one matching or default case.

Go also contains a special variation on the switch called a type switch that performs type assertions by using a switch statement. Type switches are useful for trying to understand the underlying type of an interface. For example, you might use a type switch to retrieve the underlying type of an interface called i:

func foo(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Println("I'm an integer!")
    case string:
        fmt.Println("I'm a string!")
    default:
        fmt.Println("Unknown type!")
    }
}

This example uses special syntax, i.(type) , to retrieve the type of the i interface variable . You use this value in a switch statement in which each case matches against a specific type. In this example, your cases check for int or string primitive types, but you could very well check for pointers or user-defined struct types, for instance.

Go’s last flow control structure is the for loop. The for loop is Go’s exclusive construct for performing iteration or repeating sections of code. It might seem odd to not have conventions such as do or while loops at your disposal, but you can re-create them by using variations of the for loop syntax. Here’s one variation of a for loop:

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

The code loops through numbers 0 to 9, printing each number to stdout. Notice the semicolons in the first line. Unlike many other languages, which use semicolons as line delimiters, Go uses them for various control structures to perform multiple distinct, but related, subtasks in a single line of code. The first line uses the semicolons to separate the initialization logic (i := 0), the conditional expression (i < 10), and the post statement (i++). This structure should be very, very familiar to anyone who has coded in any modern language, as it closely follows the conventions of those languages.

The following example shows a slight variation of the for loop that loops over a collection, such as a slice or a map:

 nums := []int{2,4,6,8}
   for idx, val := range nums {
       fmt.Println(idx, val)
   }

In this example, you initialize a slice of integers named nums . You then use the keyword range within the for loop to iterate over the slice. The range keyword returns two values: the current index and a copy of the current value at that index. If you don’t intend to use the index, you could replace idx in the for loop with an underscore to tell Go you won’t need it.

You can use this exact same looping logic with maps as well to return each key/value pair.

Concurrency

Much like the control structures already reviewed, Go has a much simpler concurrency model than other languages. To execute code concurrently, you can use goroutines, which are functions or methods that can run simultaneously. These are often described as lightweight threads because the cost of creating them is minimal when compared to actual threads.

To create a goroutine, use the go keyword before the call to a method or function you wish to run concurrently:

 func f() {
       fmt.Println("f function")
   }

   func main() {
     go f()
       time.Sleep(1 * time.Second)
       fmt.Println("main function")
   }

In this example, you define a function, f() , that you call in your main() function, the program’s entry point. You preface the call with the keyword go , meaning that the program will run function f() concurrently; in other words, the execution of your main() function will continue without waiting for f() to complete. You then use a time.Sleep(1 * time.Second) to force the main() function to pause temporarily so that f() can complete. If you didn’t pause the main() function, the program would likely exit prior to the completion of function f(), and you would never see its results displayed to stdout. Done correctly, you’ll see messages printed to stdout indicating that you’ve finished executing both the f() and main() functions.

Go contains a data type called channels that provide a mechanism through which goroutines can synchronize their execution and communicate with one another. Let’s look at an example that uses channels to display the length of different strings and their sum simultaneously:

 func strlen(s string, c chan int) {
     c <- len(s)
   }

   func main() {
     c := make(chan int)
     go strlen("Salutations", c)
       go strlen("World", c)
     x, y := <-c, <-c
       fmt.Println(x, y, x+y)
   }

First, you define and use a variable c of type chan int. You can define channels of various types, depending on the type of data you intend to pass via the channel. In this case, you’ll be passing the lengths of various strings as integer values between goroutines, so you should use an int channel.

Notice a new operator: <-. This operator indicates whether the data is flowing to or from a channel. You can think of this as the equivalent of placing items into a bucket or removing items from a bucket.

The function you define, strlen() , accepts a word as a string, as well as a channel that you’ll use for synchronizing data. The function contains a single statement, c <- len(s) , which uses the built-in len() function to determine the length of the string, and then puts the result into the c channel by using the <- operator.

The main() function pieces everything together. First, you issue a call to make(chan int) to create the integer channel. You then issue multiple concurrent calls to the strlen() function by using the go keyword , which spins up multiple goroutines. You pass to the strlen() function two string values, as well as the channel into which you want the results placed. Lastly, you read data from the channel by using the <- operator , this time with data flowing from the channel. This means you’re taking items out of your bucket, so to speak, and assigning those values to the variables x and y. Note that execution blocks at this line until adequate data can be read from the channel.

When the line completes, you display the length of each string as well as their sum to stdout. In this example, it produces the following output:

5 11 16

This may seem overwhelming, but it’s key to highlight basic concurrency patterns, as Go shines in this area. Because concurrency and parallelism in Go can become rather complicated, feel free to explore on your own. Throughout this book, we’ll talk about more realistic and complicated implementations of concurrency as we introduce buffered channels, wait groups, mutexes, and more.

Error Handling

Unlike most other modern programming languages, Go does not include syntax for try/catch/finally error handling. Instead, it adopts a minimalistic approach that encourages you to check for errors where they occur rather than allowing them to “bubble up” to other functions in the call chain.

Go defines a built-in error type with the following interface declaration:

type error interface {
    Error() string
}

This means you can use any data type that implements a method named Error(), which returns a string value, as an error. For example, here’s a custom error you could define and use throughout your code:

 type MyError string
   func (e MyError) Error() string {
       return string(e)
   }

You create a user-defined string type named MyError and implement an Error() string method for the type.

When it comes to error handling, you’ll quickly get accustomed to the following pattern:

func foo() error {
    return errors.New("Some Error Occurred")
}
func main() {
    if err := foo();err != nil {
        // Handle the error
    }
}

You’ll find that it’s fairly common for functions and methods to return at least one value. One of these values is almost always an error. In Go, the error returned may be a value of nil, indicating that the function generated no error and everything seemingly ran as expected. A non-nil value means something broke in the function.

Thus, you can check for errors by using an if statement, as shown in the main() function. You’ll typically see multiple statements, separated by a semicolon. The first statement calls the function and assigns the resulting error to a variable . The second statement then checks whether that error is nil . You use the body of the if statement to handle the error.

You’ll find that philosophies differ on the best way to handle and log errors in Go. One of the challenges is that, unlike other languages, Go’s built-in error type doesn’t implicitly include a stack trace to help you pinpoint the error’s context or location. Although you can certainly generate one and assign it to a custom type in your application, its implementation is left up to the developers. This can be a little annoying at first, but you can manage it through proper application design.

Handling Structured Data

Security practitioners will often write code that handles structured data, or data with common encoding, such as JSON or XML. Go contains standard packages for data encoding. The most common packages you’re likely to use include encoding/json and encoding/xml.

Both packages can marshal and unmarshal arbitrary data structures, which means they can turn strings to structures, and structures to strings. Let’s look at the following sample, which serializes a structure to a byte slice and then subsequently deserializes the byte slice back to a structure:

 type Foo struct {
       Bar string
       Baz string
   }

   func main() {
     f := Foo{"Joe Junior", "Hello Shabado"}
       b, _ := json.Marshal(f)
     fmt.Println(string(b))
       json.Unmarshal(b, &f)
   }

This code (which deviates from best practices and ignores possible errors) defines a struct type named Foo . You initialize it in your main() function and then make a call to json.Marshal() , passing it the Foo instance . This Marshal() method encodes the struct to JSON, returning a byte slice that you subsequently print to stdout . The output, shown here, is a JSON-encoded string representation of our Foo struct:

{"Bar":"Joe Junior","Baz":"Hello Shabado"}

Lastly, you take that same byte slice and decode it via a call to json.Unmarshal(b, &f). This produces a Foo struct instance . Dealing with XML is nearly identical to this process.

When working with JSON and XML, you’ll commonly use field tags, which are metadata elements that you assign to your struct fields to define how the marshaling and unmarshaling logic can find and treat the affiliated elements. Numerous variations of these field tags exist, but here is a short example that demonstrates their usage for handling XML:

type Foo struct {
    Bar     string    `xml:"id,attr"`
    Baz     string    `xml:"parent>child"`
}

The string values, wrapped in backticks and following the struct fields, are field tags. Field tags always begin with the tag name (xml in this case), followed by a colon and the directive enclosed in double quotes. The directive defines how the fields should be handled. In this case, you are supplying directives that declare that Bar should be treated as an attribute named id, not an element, and that Baz should be found in a subelement of parent, named child. If you modify the previous JSON example to now encode the structure as XML, you would see the following result:

<Foo id="Joe Junior"><parent><child>Hello Shabado</child></parent></Foo>

The XML encoder reflectively determines the names of elements, using the tag directives, so each field is handled according to your needs.

Throughout this book, you’ll see these field tags used for dealing with other data serialization formats, including ASN.1 and MessagePack. We’ll also discuss some relevant examples of defining your own custom tags, specifically when you learn how to handle the Server Message Block (SMB) Protocol.

Summary

In this chapter, you set up your Go environment and learned about the fundamental aspects of the Go language. This is not an exhaustive list of all Go’s characteristics; the language is far too nuanced and large for us to cram it all into a single chapter. Instead, we included the aspects that will be most useful in the chapters that follow. We’ll now turn our attention to practical applications of the language for security practitioners and hackers. Here we Go!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset