The Go Compiler

in Development

Last week, we released our Golang course, On Track with Golang, which explores the Go programming language and covers some of its most interesting features.

Today, we’re going to talk about one of Go’s most powerful features: the Go compiler.

The Go Compiler

Go is a compiled language. This means we must run our source code files through a compiler, which reads source code and generates a binary, or executable, file that is used to run the program. Examples of other popular compiled languages include C, C++, and Swift. Programs written in these languages are transformed into machine code and can perform extremely fast.

The Go compiler offers plenty of benefits. Today, we’ll look at three of them:

  • Error checking
  • Optimization
  • Ease of deployment

In many situations, the Go compiler can smell trouble and raise errors during the build process — that is, before our program is run.

Gunslingers staring each other down. Plus a young girl and Mark Wahlberg.

This can prevent unexpected errors in production and save developers lots of bug-hunting time. The compiler can help detect things like unused variables, missing imports, invalid operations, and more. Let’s look at the following example:

package main

import "fmt"

func main() {
    result := 10 / 0
    fmt.Println(result)
}

In this code, we are trying to assign to the result variable the result of a division by zero. If we try and compile this code, here’s what happens:

$ go build
./not-used.go:6: division by zero

As we can see, an invalid operation is detected and the build process is immediately halted.

The Go compiler can also optimize our code so it runs faster. See the code below:

package main

import (
    "fmt"
)

type user struct {
    name string
    age  int
}

func (u user) isAdult() bool {
    return u.age >= 21
}

func main() {
    gopher := user{"gopher", 32}
    fmt.Println(gopher.isAdult())
}

In this code, we have a user struct with a method named isAdult(). This method returns a boolean, true or false, depending on whether the user’s age is greater-than-or-equal-to 21. From the main() function we create a new user struct, setting name to "gopher" and age to 31. Finally, we invoke the isAdult() method and print the result to the screen.

We can compile and run this code, placed under the example folder, like this:

$ go build
$ ./example
true

Although this example may look simple, under the hood, the Go compiler went above and beyond to optimize our code. In order to see the optimization, let’s run the compiler again — but this time, passing the -gcflags=-m flag. This flag tells the compiler to display additional information for the build process.

$ go build -gcflags=-m
# example
./main.go:17: can inline user.isAdult
./main.go:14: inlining call to user.isAdult
(...)

Running the compiler again with the special flag, we can see it inlines the call to the user.isAdult() method. To inline a function call is to bring the body of the function over to where it’s being called — thus, reducing the (small but unavoidable) overhead of a function call. Tweaks like this can get our code from fast to super fast.

Lastly, we’ve mentioned the outcome of a compiled program is a single binary file. Using the previous code as an example, we have an executable file named example. Here’s the list of files in the example folder after compiling our program:

$ go build
$ ls
-rwxr-xr-x  1 caike  staff   2.2M Jul 29 12:09 example
-rw-r--r--  1 caike  staff   209B Jul 29 12:09 main.go

Notice the x permission on the far left, meaning the example file is an executable. All we need to run our program is this single file. Compare this with interpreted languages, like Ruby and Node.js, or even languages that generate byte-code like Java and Erlang. In order to run programs written in these languages, we need access to all of the source code files and an entire runtime environment installed. With Go, that’s not necessary. We can simply tell the Go compiler which machine architecture our program will run on and it generates the proper executable file for us. This means that in a lot of cases, deploying a Go application to production essentially involves moving an executable file over to our production server.

The compiler defaults to the current architecture used for development, but we can also tell it to compile to a different architecture. This is known as cross-compilation.

In order to compile our previous program for a 64-bit Windows machine, here’s the command we have to run:

$ GOOS=windows GOARCH=amd64 go build

Using these two environment variables, GOOS and GOARCH, we can generate binaries for many different operating systems running on a variety of architectures.

If we list our files again, we can see a new Windows-specific binary:

$ ls
-rwxr-xr-x  1 caike  staff   2.2M Jul 29 12:09 hello
-rwxr-xr-x  1 caike  staff   2.3M Jul 29 12:15 hello.exe
-rw-r--r--  1 caike  staff   209B Jul 29 12:09 main.go

These three benefits are just the tip of the iceberg. The Go compiler is an extremely powerful tool that makes writing Go programs simple and fun!

Do you have a favorite feature from the Go compiler? Let us know in the comments below! And if you’d like to learn more about Go, check out our Golang course, On Track With Golang.

Code School

Code School teaches web technologies in the comfort of your browser with video lessons, coding challenges, and screencasts. We strive to help you learn by doing.

Visit codeschool.com

About the Author

Carlos Souza

Developer, Instructor and Metal aficionado.

Might We Suggest