Learning Go By Example: Part 1
I strive to be a lifelong learner and enjoy learning new things. I prefer to learn skills by seeing and working with examples. I especially like learning new programming languages and frameworks and have been wanting to pick up Go for quite a while. There are plenty of great resources to learn Go including the wonderful https://tour.golang.org/ that offers a hands on tour of the Go language. There are also several good books on Go including:
These are wonderful resources. I was eager to start coding and used these resources as I dove into the Golang pool.
Why Learn Go?
I know quite a few programming languages why should I learn another one, especially Go? First of all, Go has quite a pedigree. It was created by software engineering legends Brian W. Kerninghan, Rob Pike, and Robert Griesemer. A number of important software projects including Docker and Kubernetes, have been written in Go. Here is a post that makes a compelling case for Go. A better question might be, why not learn Go?
The First Example: Finding Hashtags
My daughter recently started taking her first programming course and her school selected Python to teach students the fundamentals. I am very fond of Python and thought it was a good choice. I eagerly volunteered to be my daughter’s unofficial TA.
I’ve known Python for a long time, but can remember only one time in which I used it professionally. Nevertheless, I “think” in Python. When I write pseudocode, I usually write it in a Python-esque manner. One of my daughter’s first assignments was to write a hashtag finder. The program had to read in tweets and print out all the included hashtags. It was a fairly straightforward exercise:
def main():
while True:
tweet = input("Enter tweet: ")
if tweet.lower() == "quit":
print("Goodbye!")
break
words = tweet.split(" ")
has_hastags = False
for word in words:
if word[0] == '#':
print("Hashtag: ", word)
has_hastags = True
if not has_hastags:
print("No hastags")
if __name__ == "__main__":
main()
The code above is very readable except for the somewhat esoteric manner in which main code blocks are defined in Python. Rewriting this example in Go turned out to be a fairly simple exercise:
package main
import "bufio"
import "fmt"
import "os"
import "strings"
func main() {
var reader = bufio.NewReader(os.Stdin)
for {
fmt.Print("Enter tweet: ")
var tweet, _ = reader.ReadString('\n')
if strings.EqualFold(tweet, "quit\n") {
fmt.Println("Goodbye!")
break
}
var words = strings.Split(tweet, " ")
var hasHashTags = false;
for _, word := range words {
if word[0] == '#' {
fmt.Println("Hashtag: ", word)
hasHashTags = true
}
}
if !hasHashTags {
fmt.Println("No hashtags")
}
}
}
This code is also very readable even for a Go newbie.
package main
import "bufio"
import "fmt"
import "os"
import "strings"
The snippet above defines the main code block (a bit simpler than Python) and the required imported packages (i.e. libraries) for the program.
func main() {
var reader = bufio.NewReader(os.Stdin)
This code snippet defines the main
function and creates a reader
variable that holds a reference to a buffered reader from standard input. The keyword var
is used to define variables. We’ll use the reader
variable to read a string from the keyboard. This is similar to something we would see in other languages like C or Java. It’s not needed in Python because standard input is implied in Python’s input
function. Notice that the NewReader
function is from the bufio
package which we included in an import
statement above.
for {
fmt.Print("Enter tweet: ")
var tweet, _ = reader.ReadString('\n')
if strings.EqualFold(tweet, "quit\n") {
fmt.Println("Goodbye!")
break
}
...
This code snippet defines an infinite loop which is equivalent to the Python while True:
loop. We then prompt the user to enter their tweet. The call to the ReadString
method is interesting because it returns two values. All Go functions (and methods) are able to return multiple values which can be very useful. The value is the string from standard input and stored in the tweet
variable. The second value is the resulting error code. In this case, we use the _
character to ignore it. We could’ve also written var tweet, errorCode := reader.ReadString('\n'),
but since we won’t be using the error code, we can avoid a compiler error by ignoring it (the Go compiler doesn’t allow unused variables). The '\n'
defines the terminating delimiter of the string. In other words, it returns the string up to, and including the new line delimiter.
The EqualFold
function performs a case insensitive comparison of the tweet and the value “quit\n”. Notice that we had to include the trailing newline character in the comparison.
Functions versus methods
I want to make a quick note about the difference between functions and methods. In the snippet above, EqualFold
is considered to be a function because is referenced directly from the strings
package. ReadString
is considered a method because it’s referenced from the object reference contained in the reader
variable.
var words = strings.Split(tweet, " ")
var hasHashTags = false;
for _, word := range words {
if word[0] == '#' {
fmt.Println("Hashtag: ", word)
hasHashTags = true
}
}
if !hasHashTags {
fmt.Println("No hashtags")
}
In the code snippet above, we use the Split
function to break up the tweet into separate words that are stored in the array contained in the words
variable. We then define a strange looking for
loop. We can define an index in the first part of a for
loop. Since we won’t be needing one, we use the _
character to ignore it. The for
loop iterates through all the words in words
array. The word
variable contains the word of the current iteration. This variable is only scoped within the for
loop and cannot be used outside the loop. If the first byte of the word is a #
character, that word is a hashtag and we print it and set the hasHashTags
variable to true. After the for
loop, if hasHashTags
is still false, then we print that we did not find any hashtags.
Refactoring the code
There are few things that we can do to improve this example and make it a better Go program:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Enter tweet: ")
tweet, _ := reader.ReadString('\n')
if strings.EqualFold(strings.TrimSpace(tweet),"quit") {
fmt.Print("Exiting...")
break
}
hashtags := findHashTags(tweet)
for _, hashtag := range hashtags {
fmt.Printf("Hashtag: %s\n", hashtag)
}
if len(hashtags) == 0 {
fmt.Print("No hashtags")
}
}
}
func findHashTags(tweet string) []string {
var hashtags[] string
words := strings.Split(tweet, " ")
for _, word := range words {
if word[0] == '#' {
hashtags = append(hashtags, word)
}
}
return hashtags
}
It’s better style to group all the imports into a single parenthesized, “factored” import statement:
import (
"bufio"
"fmt"
"os"
"strings"
)
That makes it a little better to read.
reader := bufio.NewReader(os.Stdin)
We can use the :=
operator to define and assign variables. This is a shortcut for var reader = bufio.NewReader(os.Stdin)
.
if strings.EqualFold(strings.TrimSpace(tweet),"quit") {
fmt.Print("Exiting...")
break
}
In this code snippet, we use the TrimSpace
function to remove all whitespace from the tweet
variable so we can compare to a more simple “quit” value.
hashtags := findHashTags(tweet)
for _, hashtag := range hashtags {
fmt.Printf("Hashtag: %s\n", hashtag)
}
if len(hashtags) == 0 {
fmt.Print("No hashtags")
}
In the code snippet above, we broke out the hashtag code into its own separate function, findHashTags
which returns an array with the associated hashtags. The for
loop iterates through this array and prints out the hashtags. If the array is empty (its length is zero), we print out a message that no hashtags were found.
The findHashTags
function is fairly straightforward:
func findHashTags(tweet string) []string {
var hashtags[] string
words := strings.Split(tweet, " ")
for _, word := range words {
if word[0] == '#' {
hashtags = append(hashtags, word)
}
}
return hashtags
}
The first line defines findHashTags
as a function that takes in a string parameter and returns a string array. The function uses array slices to build the return value contained in the hashtags
variable. We can view slices as references to arrays. That effectively allows us to treat them as dynamic arrays to which we can append values: hashtags = append(hashtags, word)
.
Type inference
Go is a statically typed language but it allows for inferred typing. For example, in this snippet:
words := strings.Split(tweet, " ")
the variable words
is not explicitly typed. The type is inferred from the function strings.Split
. We could’ve also written: var words[] string = strings.Split)tweet, " ")
.
Summary
This was a fairly simple example, but I hope that it helps start your journey to learning and becoming literate in Go. We’ll continue to post more intricate examples to learn additional Go features and idioms. Here is my second Go post.