I have decided to have a go at Go (pun intended!).
The reasons are less important than the decision itself, so I will not discuss them in this blog. Instead, I will focus on the journey and will document the experience in a series of posts (hopefully, more to come!).
Anyone who has worked with a programming language professionally knows that learning a programming language involves more than learning syntax. Here is a list of things that I feel I need to learn if I want to be productive in Go (in no particular order) -
- Setting up relevant tools and being comfortable with them
- The language syntax
- Language APIs
- Writing tests
- 3rd party libraries and frameworks
- Debugging in Go
- Compiling and packaging applications with Go
- Set up CI/CD
- Different failure modes and language quarks and learning how to watch out for them
- Monitoring for failures, including learning how to collect metrics
I am currently at stage 2, trying to learn the language syntax. I expect to jump back and forth between different stages as I continue learning Go.
Installing Go
Installing Go on my machine was relatively easy as I am on a Mac. All I had to do was use brew
:
brew install go
I verified my Go installation with:
go version
which printed:
go version go1.20.1 darwin/arm64
Writing my first hello world script
Next, it was time to write my first ever hello world in Go. I used Visual Studio Code to create a file named hello.go
with the following content -
// Go organises code in packages! Similar to Java
package main
// I can import other packages and reuse their code
import "fmt"
// This is how I can define a function in Go
func main() {
fmt.Print("Hello World\n")
}
Now it is time to compile and run the code. I opened up the terminal, navigated to the directory containing the file, and executed:
go run hello.go
The go run
command compiled my script, built a native binary, and simultaneously executed it. Which then printed the expected output in the command line:
Hello World
Success!
Like Java, Go also has the concept of packages. I am still not fully aware of all the rules and gotchas, but I assume their purpose is similar to Java - they have been designed to group relevant code under a common namespace.
I also learned that packages could decide to expose functions/variables to other packages. To do that, I have to define a function with a capital letter, and Go automatically assumes that it is intended to be used from other packages. This is why the Print
function starts with a capital P
.
Another observation is that if I change my package name to something else, like hellopackage
, the code no longer compiles. Instead, I get the following error message:
package command-line-arguments is not a main package
The package containing the main
function must also be named main
. This makes me wonder how people organise code in Go. Do they use reversed company domain name to name packages (i.e., com.sayemahmed
) and then have only one package named main
with only the main
function to bootstrap the application? Or do they use something else?
go build
go run
isn’t the only way to build a Go script. We have another command at our disposal - go build
- which compiles the script and generates a native executable binary:
go build -o hello_world hello.go
I used the -o
flag to specify a name for the binary file.
The compilation process reminded me of how I used to compile C/C++ files with GCC (gcc hello.c -o hello
would be equivalent for a C file). With Java, however, I hardly needed to generate a native binary file (though I could, using GraalVM). Most of the times I have used Maven to build and deploy my web applications.
What are the differences between go run
and go build
? The go run
command takes either a single file, a list of files, or the name of a package. It then compiles the code and creates a native binary file in a temporary directory, executes the binary, and then deletes it immediately.
The go build
command also takes either a single file, a list of files, or the name of a package. However, it only compiles the code and creates a native binary. We can use the -o
flag with this command to set the name of the compiled binary.
Built-in tools
Besides the compiler, Go comes with many helpful tools with default installation. One such tool is go fmt
which reformats code based on official standards. I was pleased to find out Go has an officially recommended formatting which is enforced during compile time! Ha! No more fighting over tabs vs spaces!!
The reason Go comes with standardised formatting seems to be easier tooling support. People claim that it makes it easy to write tools that manipulate Go source code, which makes it easy to write code generation tools. Nice.
Similar to a formatter, Go installation also includes a linter, a dependency manager, and a test runner. I have yet to try them out.
My first real script
A few weeks ago, I needed to prepare a report. The report contained some information I collected over the last year. It was a ~500 lines file, and I wanted to check it had no duplicates before submitting it. I wrote a Go script to automate the process. The entire script is given below:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// First argument is the full path to the binary when
// executed with `go run`
if len(os.Args) != 2 {
fmt.Println("Usage: <exeutable> <full-path>")
return
}
fileName := os.Args[1]
readFile, err := os.Open(fileName)
if err != nil {
fmt.Println("Error while trying to open file", err)
}
fileScanner := bufio.NewScanner(readFile)
fileScanner.Split(bufio.ScanLines)
// https://stackoverflow.com/a/34020023
set := make(map[string]struct{})
// https://dave.cheney.net/2014/03/25/the-empty-struct
var empty struct{}
for fileScanner.Scan() {
line := strings.TrimSpace(fileScanner.Text())
if line == "" {
continue
}
_, found := set[line]
if found {
fmt.Println("Duplicate exists: ", line)
} else {
set[line] = empty
}
}
readFile.Close()
}
I invoked the script as follows:
go run my-script.go ~/Documents/my-report.md
Which then displayed all the duplicate lines in my-report.md
file.
I first learned how to pass command line arguments to a Go script. To do that, I needed to import the built-in os
package and use it’s Args
array. It looks like arrays in Go work similarly to Java and C - zero-based and indexed using []
. The first element contains the executable’s name (in my case, it was something like /var/folders/mr/k_b0wtwd11q66p3z3787w9082200gq/T/go-build907299260/b001/exe/my-script
). Starting from the second element, we can access any passed-in arguments. The script expects the file name, which should be checked for duplicates, so we access the file name at index 1.
Next, I learned that to find the length of an array, I need to use the built-in len
function. This is similar to how the len function in Python works.
The line:
fileName := os.Args[1]
declares and simultaneously initialises a variable named fileName
with the name of our file. Go seems to allow a couple of ways to declare variables - one using var
and the other without it. The above line for example could have also been written as:
// Declare the variable first, with explicit type
var fileScanner *bufio.Scanner
// Initialise the variable
fileScanner = bufio.NewScanner(readFile)
There are some differences between the two approaches. For example, we can only use :=
within a function. It can also infer the variable type automatically as we are required to specify a value immediately.
On the other hand, we can use =
outside a function as well. It also cannot infer the type of a variable automatically.
When I saw the *
in the type, I started thinking - does Go have explicit pointers like C
? And it seems it does! That means caring about Dangling Pointers and SegFaults and memory safety and all that!
The following line opens the file for reading:
readFile, err := os.Open(fileName)
I didn’t specify file open (i.e., read-write/append only/read-only) mode when calling Open
, but I assume there must be a way to do it.
One interesting thing to note is how I used two variables to store the return values from Open
. It looks similar to how Python can return multiple values from a method (which it converts into a Tuple). I am not 100% sure whether something similar is happening here. The Go by Example site mentions multiple return values, but it doesn’t explain how it actually works under the hood.
Also, after looking at some examples, it seems many functions in Go often return a couple of return values, especially the ones that do some I/O or have a chance of running into errors/issues. In Java, we used to throw exceptions when an I/O failed, but in Go, the convention is to return a second value describing the error.
The following two lines:
fileScanner := bufio.NewScanner(readFile)
fileScanner.Split(bufio.ScanLines)
instantiates and configures something like a Scanner to read the file line by line.
My initial idea was to create a Set where I store the lines after reading from the file. Then, if I ever find another matching line by doing a simple contains check, I have found a duplicate. While trying to translate this idea into Go code, I discovered that it does not have any built-in Set type. It does have a Map, and the recommendation was to use it to simulate a Set, which I did:
set := make(map[string]struct{})
I also came across empty
struct. It seems Go does not have anything that looks like a Java class, but it does have Structures like in C. An empty struct
is a special kind of structure in that it does not occupy any memory. And so, simulating a Set of strings with a Map of String to empty struct was the most space-efficient way.
The next few lines are relatively easy to understand - we stream through the lines in the files and check for duplicates. The interesting bit here is the line:
_, found := set[line]
which shows that, like some programming languages i.e., Scala Go also supports _
as a way to ignore values when we don’t care about them.
Final thoughts
After writing my first script, my first impression was that Go is between C and Java. It has pointers like in C, and it has packages like Java. Its similarity to C might be why people often say that Go applications have less resource footprint. On the other hand, I wonder if Go has something like a Spring-framework, which removes a lot of boilerplate for anyone developing applications in Java.
But all in all, I like my initial brush with Go.