Book Image

Mastering Go – Third Edition - Third Edition

By : Mihalis Tsoukalos
5 (2)
Book Image

Mastering Go – Third Edition - Third Edition

5 (2)
By: Mihalis Tsoukalos

Overview of this book

Mastering Go is the essential guide to putting Go to work on real production systems. This freshly updated third edition includes topics like creating RESTful servers and clients, understanding Go generics, and developing gRPC servers and clients. Mastering Go was written for programmers who want to explore the capabilities of Go in practice. As you work your way through the chapters, you’ll gain confidence and a deep understanding of advanced Go concepts, including concurrency and the operation of the Go Garbage Collector, using Go with Docker, writing powerful command-line utilities, working with JavaScript Object Notation (JSON) data, and interacting with databases. You’ll also improve your understanding of Go internals to optimize Go code and use data types and data structures in new and unexpected ways. This essential Go programming book will also take you through the nuances and idioms of Go with exercises and resources to fully embed your newly acquired knowledge. With the help of Mastering Go, you’ll become an expert Go programmer by building Go systems and implementing advanced Go techniques in your projects.
Table of Contents (17 chapters)
14
Other Books You May Enjoy
15
Index

Non-numeric data types

Go has support for Strings, Characters, Runes, Dates, and Times. However, Go does not have a dedicated char data type. We begin by explaining the string-related data types.

For Go, dates and times are the same thing and are represented by the same data type. However, it is up to you to determine whether a time and date variable contains valid information or not.

Strings, Characters, and Runes

Go supports the string data type for representing strings. A Go string is just a collection of bytes and can be accessed as a whole or as an array. A single byte can store any ASCII character—however, multiple bytes are usually needed for storing a single Unicode character.

Nowadays, supporting Unicode characters is a common requirement—Go is designed with Unicode support in mind, which is the main reason for having the rune data type. A rune is an int32 value that is used for representing a single Unicode code point, which is an integer value that is used for representing single Unicode characters or, less frequently, providing formatting information.

Although a rune is an int32 value, you cannot compare a rune with an int32 value. Go considers these two data types as totally different.

You can create a new byte slice from a given string by using a []byte("A String") statement. Given a byte slice variable b, you can convert it into a string using the string(b) statement. When working with byte slices that contain Unicode characters, the number of bytes in a byte slice is not always connected to the number of characters in the byte slice, because most Unicode characters require more than one byte for their representation. As a result, when you try to print each single byte of a byte slice using fmt.Println() or fmt.Print(), the output is not text presented as characters but integer values. If you want to print the contents of a byte slice as text, you should either print it using string(byteSliceVar) or using fmt.Printf() with %s to tell fmt.Printf() that you want to print a string. You can initialize a new byte slice with the desired string by using a statement such as []byte("My Initialization String").

We will cover byte slices in more detail in the Byte slices section.

You can define a rune using single quotes: r := '€' and you can print the integer value of the bytes that compose it as fmt.Println(r)—in this case, the integer value is 8364. Printing it as a single Unicode character requires the use of the %c control string in fmt.Printf().

As strings can be accessed as arrays, you can iterate over the runes of the string using a for loop or point to a specific character if you know its place in the string. The length of the string is the same as the number of characters found in the string, which is usually not true for byte slices because Unicode characters usually require more than one byte.

The following Go code illustrates the use of strings and runes and how you can work with strings in your code. You can find the entire program as text.go in the ch02 directory of the GitHub repository of the book.

The first part of the program defines a string literal that contains a Unicode character. Then it accesses its first character as if the string was an array.

func main() {
    aString := "Hello World! €"
    fmt.Println("First character", string(aString[0]))

The next part is about working with runes.

    // Runes
    // A rune
    r := '€'
    fmt.Println("As an int32 value:", r)
    // Convert Runes to text
    fmt.Printf("As a string: %s and as a character: %c\n", r, r)
    // Print an existing string as runes
    for _, v := range aString {
        fmt.Printf("%x ", v)
    }
    fmt.Println()

First, we define a rune named r. What makes this a rune is the use of single quotes around the character. The rune is an int32 value and is printed as such by fmt.Println(). The %c control string in fmt.Printf() prints a rune as a character.

Then we iterate over aString as a slice or an array using a for loop with range and print the contents of aString as runes.

    // Print an existing string as characters
    for _, v := range aString {
        fmt.Printf("%c", v)
    }
    fmt.Println()
}

Lastly, we iterate over aString as a slice or an array using a for loop with range and print the contents of aString as characters.

Running text.go produces the following output:

$ go run text.go
First character H
As an int32 value: 8364
As a string: %!s(int32=8364) and as a character: €
48 65 6c 6c 6f 20 57 6f 72 6c 64 21 20 20ac
Hello World! €

The first line of the output shows that we can access a string as an array whereas the second line verifies that a rune is an integer value. The third line shows what to expect when you print a rune as a string and as a character—the correct way is to print it as a character. The fifth line shows how to print a string as runes and the last line shows the output of processing a string as characters using range and a for loop.

Converting from int to string

You can convert an integer value into a string in two main ways: using string() and using a function from the strconv package. However, the two methods are fundamentally different. The string() function converts an integer value into a Unicode code point, which is a single character, whereas functions such as strconv.FormatInt() and strconv.Itoa() convert an integer value into a string value with the same representation and the same number of characters.

This is illustrated in the intString.go program—its most important statements are the following. You can find the entire program in the GitHub repository of the book.

    input := strconv.Itoa(n)
    input = strconv.FormatInt(int64(n), 10)
    input = string(n)

Running intString.go generates the following kind of output:

$ go run intString.go 100
strconv.Itoa() 100 of type string
strconv.FormatInt() 100 of type string
string() d of type string

The data type of the output is always string, however, string() converted 100 into d because the ASCII representation of d is 100.

The unicode package

The unicode standard Go package contains various handy functions for working with Unicode code points. One of them, which is called unicode.IsPrint(), can help you to identify the parts of a string that are printable using runes.

The following code excerpt illustrates the functionality of the unicode package:

    for i := 0; i < len(sL); i++ {
        if unicode.IsPrint(rune(sL[i])) {
            fmt.Printf("%c\n", sL[i])
        } else {
            fmt.Println("Not printable!")
        }
    }

The for loop iterates over the contents of a string defined as a list of runes ("\x99\x00ab\x50\x00\x23\x50\x29\x9c") while unicode.IsPrint() examines whether the character is printable or not—if it returns true then a rune is printable.

You can find this code excerpt inside the unicode.go source file at the ch02 directory in the GitHub repository of the book. Running unicode.go produces the following output:

Not printable!
Not printable!
a
b
P
Not printable!
#
P
)
Not printable!

This utility is very handy for filtering your input or filtering data before printing it on screen, storing it in log files, transferring it on a network, or storing it in a database.

The strings package

The strings standard Go package allows you to manipulate UTF-8 strings in Go and includes many powerful functions. Many of these functions are illustrated in the useStrings.go source file, which can be found in the ch02 directory of the book GitHub repository.

If you are working with text and text processing, you definitely need to learn all the gory details and functions of the strings package, so make sure that you experiment with all these functions and create many examples that will help you to clarify things.

The most important parts of useStrings.go are the following:

import (
    "fmt"
    s "strings"
    "unicode"
)
var f = fmt.Printf

As we are going to use the strings package multiple times, we create a convenient alias for it named s. We do the same for the fmt.Printf() function where we create a global alias using a variable named f. These two shortcuts make code less populated with long, repeated lines of code. You can use it when learning Go but this is not recommended in any kind of production software, as it makes code less readable.

The first code excerpt is the following.

f("EqualFold: %v\n", s.EqualFold("Mihalis", "MIHAlis"))
f("EqualFold: %v\n", s.EqualFold("Mihalis", "MIHAli"))

The strings.EqualFold() function compares two strings without considering their case and returns true when they are the same and false otherwise.

f("Index: %v\n", s.Index("Mihalis", "ha"))
f("Index: %v\n", s.Index("Mihalis", "Ha"))

The strings.Index() function checks whether the string of the second parameter can be found in the string that is given as the first parameter and returns the index where it was found for the first time. On an unsuccessful search, it returns -1.

    f("Prefix: %v\n", s.HasPrefix("Mihalis", "Mi"))
    f("Prefix: %v\n", s.HasPrefix("Mihalis", "mi"))
    f("Suffix: %v\n", s.HasSuffix("Mihalis", "is"))
    f("Suffix: %v\n", s.HasSuffix("Mihalis", "IS"))

The strings.HasPrefix() function checks whether the given string, which is the first parameter, begins with the string that is given as the second parameter. In the previous code, the first call to strings.HasPrefix() returns true, whereas the second returns false.

Similarly, the strings.HasSuffix() function checks whether the given string ends with the second string. Both functions take into account the case of the input string and the case of the second parameter.

    t := s.Fields("This is a string!")
    f("Fields: %v\n", len(t))
    t = s.Fields("ThisIs a\tstring!")
    f("Fields: %v\n", len(t))

The handy strings.Fields() function splits the given string around one or more white space characters as defined by the unicode.IsSpace() function and returns a slice of substrings found in the input string. If the input string contains white characters only, it returns an empty slice.

    f("%s\n", s.Split("abcd efg", ""))
    f("%s\n", s.Replace("abcd efg", "", "_", -1))
    f("%s\n", s.Replace("abcd efg", "", "_", 4))
    f("%s\n", s.Replace("abcd efg", "", "_", 2))

The strings.Split() function allows you to split the given string according to the desired separator string—the strings.Split() function returns a string slice. Using "" as the second parameter of strings.Split() allows you to process a string character by character.

The strings.Replace() function takes four parameters. The first parameter is the string that you want to process. The second parameter contains the string that, if found, will be replaced by the third parameter of strings.Replace(). The last parameter is the maximum number of replacements that are allowed to happen. If that parameter has a negative value, then there is no limit to the number of replacements that can take place.

    f("SplitAfter: %s\n", s.SplitAfter("123++432++", "++"))
    trimFunction := func(c rune) bool {
        return !unicode.IsLetter(c)
    }
    f("TrimFunc: %s\n", s.TrimFunc("123 abc ABC \t .", trimFunction))

The strings.SplitAfter() function splits its first parameter string into substrings based on the separator string that is given as the second parameter to the function. The separator string is included in the returned slice.

The last lines of code define a trim function named trimFunction that is used as the second parameter to strings.TrimFunc() in order to filter the given input based on the return value of the trim function—in this case, the trim function keeps all letters and nothing else due to the unicode.IsLetter() call.

Running useStrings.go produces the next output:

To Upper: HELLO THERE!
To Lower: hello there
THis WiLL Be A Title!
EqualFold: true
EqualFold: false
Prefix: true
Prefix: false
Suffix: true
Suffix: false
Index: 2
Index: -1
Count: 2
Count: 0
Repeat: ababababab
TrimSpace: This is a line.
TrimLeft: This is a      line. 
TrimRight:      This is a        line.
Compare: 1
Compare: 0
Compare: -1
Fields: 4
Fields: 3
[a b c d   e f g]
_a_b_c_d_ _e_f_g_
_a_b_c_d efg
_a_bcd efg
Join: Line 1+++Line 2+++Line 3
SplitAfter: [123++ 432++ ]
TrimFunc: abc ABC

Visit the documentation page of the strings package at https://golang.org/pkg/strings/ for the complete list of available functions. You will see the functionality of the strings package in other places in this book.

Enough with strings and text; the next section is about working with dates and times in Go.

Times and dates

Often, we need to work with date and time information to store the time an entry was last used in a database or the time an entry was inserted into a database, which brings us to another interesting topic: working with dates and times in Go.

The king of working with times and dates in Go is the time.Time data type, which represents an instant in time with nanosecond precision. Each time.Time value is associated with a location (time zone).

If you are a UNIX person, you might already know about the UNIX epoch time and wonder how to get it in Go. The time.Now().Unix() function returns the popular UNIX epoch time, which is the number of seconds that have elapsed since 00:00:00 UTC, January 1, 1970. If you want to convert the UNIX time to the equivalent time.Time value, you can use the time.Unix() function. If you are not a UNIX person, then you might not have heard about the UNIX epoch time before but now you know what it is!

The time.Since() function calculates the time that has passed since a given time and returns a time.Duration variable—the duration data type is defined as type Duration int64. Although a Duration is, in reality, an int64 value, you cannot compare or convert a duration to an int64 value implicitly because Go does not allow implicit data type conversions.

The single most important topic about Go and dates and times is the way Go parses a string in order to convert it into a date and a time. The reason that this is important is usually such input is given as a string and not as a valid date variable. The function used for parsing is time.Parse() and its full signature is Parse(layout, value string) (Time, error), where layout is the parse string and value is the input that is being parsed. The time.Time value that is returned is a moment in time with nanosecond precision and contains both date and time information.

The next table shows the most widely used strings for parsing dates and times.

Parse Value

Meaning (examples)

05

12-hour value (12pm, 07am)

15

24-hour value (23, 07)

04

Minutes (55, 15)

05

Seconds (5, 23)

Mon

Abbreviated day of week (Tue, Fri)

Monday

Day of week (Tuesday, Friday)

02

Day of month (15, 31)

2006

Year with 4 digits (2020, 2004)

06

Year with the last 2 digits (20, 04)

Jan

Abbreviated month name (Feb, Mar)

January

Full month name (July, August)

MST

Time zone (EST, UTC)

The previous table shows that if you want to parse the 30 January 2020 string and convert it into a Go date variable, you should match it against the 02 January 2006 string—you cannot use anything else in its place when matching a string with the 30 January 2020 format. Similarly, if you want to parse the 15 August 2020 10:00 string, you should match it against the 02 January 2006 15:04 string. The documentation of the time package (https://golang.org/pkg/time/) contains even more detailed information about parsing dates and times—however, the ones presented here should be more than enough for regular use.

A utility for parsing dates and times

On a rare occasion, a situation can happen when we do not know anything about our input. If you do not know the exact format of your input, then you need to try matching your input against multiple Go strings without being sure that you are going to succeed in the end. This is the approach that the example uses. The Go matching strings for dates and times can be tried in any order.

If you are matching a string that only contains the date, then your time will be set to 00:00 by Go and will most likely be incorrect. Similarly, when matching the time only, your date will be incorrect and should not be used.

The formatting strings can be also used for printing dates and times in the desired format. So in order to print the current date in the 01-02-2006 format, you should use time.Now().Format("01-02-2006").

The code that follows illustrates how to work with epoch time in Go and showcases the parsing process—create a text file, type the following code, and save it as dates.go.

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

This is the expected preamble of the Go source file.

func main() {
    start := time.Now()
    if len(os.Args) != 2 {
        fmt.Println("Usage: dates parse_string")
        return
    }
    dateString := os.Args[1]

This is how we get user input that is stored in the dateString variable. If the utility gets no input, there is no point in continuing its operation.

    // Is this a date only?
    d, err := time.Parse("02 January 2006", dateString)
    if err == nil {
        fmt.Println("Full:", d)
        fmt.Println("Time:", d.Day(), d.Month(), d.Year())
    }

The first test is for matching a date only using the 02 January 2006 format. If the match is successful, you can access the individual fields of a variable that holds a valid date using Day(), Month(), and Year().

    // Is this a date + time?
    d, err = time.Parse("02 January 2006 15:04", dateString)
    if err == nil {
        fmt.Println("Full:", d)
        fmt.Println("Date:", d.Day(), d.Month(), d.Year())
        fmt.Println("Time:", d.Hour(), d.Minute())
    }

This time we try to match a string using "02 January 2006 15:04", which contains a date and a time value. If the match is successful, you can access the fields of a valid time using Hour() and Minute().

    // Is this a date + time with month represented as a number?
    d, err = time.Parse("02-01-2006 15:04", dateString)
    if err == nil {
        fmt.Println("Full:", d)
        fmt.Println("Date:", d.Day(), d.Month(), d.Year())
        fmt.Println("Time:", d.Hour(), d.Minute())
    }

This time we try to match against the "02-01-2006 15:04" format, which contains both a date and a time. Note that it is compulsory that the string that is being examined contains the - and the : characters as specified in the time.Parse() call and that "02-01-2006 15:04" is different from "02/01/2006 1504".

    // Is it time only?
    d, err = time.Parse("15:04", dateString)
    if err == nil {
        fmt.Println("Full:", d)
        fmt.Println("Time:", d.Hour(), d.Minute())
    }

The last match is for time only using the "15:04" format. Note that the : should exist in the string that is being examined.

    t := time.Now().Unix()
    fmt.Println("Epoch time:", t)
    // Convert Epoch time to time.Time
    d = time.Unix(t, 0)
    fmt.Println("Date:", d.Day(), d.Month(), d.Year())
    fmt.Printf("Time: %d:%d\n", d.Hour(), d.Minute())
    duration := time.Since(start)
    fmt.Println("Execution time:", duration)
}

The last part of dates.go shows how to work with UNIX epoch time. You get the current date and time in epoch time using time.Now().Unix() and you can convert that to a time.Time value using a call to time.Unix().

Lastly, you can calculate the time duration between the current time and a time in the past using a call to time.Since().

Running dates.go creates the following kind of output, depending on its input:

$ go run dates.go 
Usage: dates parse_string
$ go run dates.go 14:10
Full: 0000-01-01 14:10:00 +0000 UTC
Time: 14 10
Epoch time: 1607964956
Date: 14 December 2020
Time: 18:55
Execution time: 163.032µs
$ go run dates.go "14 December 2020"
Full: 2020-12-14 00:00:00 +0000 UTC
Time: 14 December 2020
Epoch time: 1607964985
Date: 14 December 2020
Time: 18:56
Execution time: 180.029µs

If a command-line argument such as 14 December 2020 contains space characters, you should put it in double quotes for the UNIX shell to treat it as a single command-line argument. Running go run dates.go 14 December 2020 does not work.

Now that we know how to work with dates and times, it is time to learn more about time zones.

Working with different time zones

The presented utility accepts a date and a time and converts them into different time zones. This can be particularly handy when you want to preprocess log files from different sources that use different time zones in order to convert these different time zones into a common one.

Once again, you need time.Parse() in order to convert a valid input into a time.Time value before doing the conversions. This time the input string contains the time zone and is parsed by the "02 January 2006 15:04 MST" string.

In order to convert the parsed date and time into New York time, the program uses the following code:

    loc, _ = time.LoadLocation("America/New_York")
    fmt.Printf("New York Time: %s\n", now.In(loc))

This technique is used multiple times in convertTimes.go.

Running convertTimes.go generates the following output:

$ go run convertTimes.go "14 December 2020 19:20 EET"
Current Location: 2020-12-14 19:20:00 +0200 EET
New York Time: 2020-12-14 12:20:00 -0500 EST
London Time: 2020-12-14 17:20:00 +0000 GMT
Tokyo Time: 2020-12-15 02:20:00 +0900 JST
$ go run convertTimes.go "14 December 2020 20:00 UTC"
Current Location: 2020-12-14 22:00:00 +0200 EET
New York Time: 2020-12-14 15:00:00 -0500 EST
London Time: 2020-12-14 20:00:00 +0000 GMT
Tokyo Time: 2020-12-15 05:00:00 +0900 JST
$ go run convertTimes.go "14 December 2020 25:00 EET"
parsing time "14 December 2020 25:00": hour out of range

In the last execution of the program, the code has to parse 25 as the hour of the day, which is wrong and generates the hour out of range error message.