t0ul

Automate the Boring Stuff with Go, Ch. 2: Go Basics

Part of Automate the Boring Stuff with Go, a beginner-friendly guide to programming in Go.

Updated for Go 1.27 in October 2026. What changed: the toolchain tour now matches how modules work today (go get inside a module, go install for tools, go vet), float64 is the float you reach for, and every example is a file in the repo you can run.

In this chapter I’ll cover the basics you’ll need for writing programs in Go: using the toolchain, data types, and some handy conversions. There are plenty of examples in the sections below. It’s okay if they do not all make sense; we aim for familiarity, not mastery. That said, the best way to learn any programming language is to code along to build muscle memory, so crack open your new editor.

Note: every example here lives in the book’s repository under ch2/examples. If something errors or you get a little lost, run the file from there: go run ./ch2/examples/ex1.

I. The Go Toolchain

One thing that makes developing with Go so different from Python or Java is that it ships with a complete toolchain from the creators of the language: building, formatting, testing, dependency management and vetting are all the one go command.

In chapter 1 we used go run . and go build to run the hello world program. Those are subcommands of the toolchain. Here are the ones you will use most.

I.a go mod

go mod manages dependencies. A folder with a go.mod file is a module, and the file records which other modules your code needs and which versions. Anyone who clones your project gets the same versions, so it builds the same way on their machine as on yours.

You’ll need two subcommands.

go mod init

go mod init declares that the folder you are in is a module and writes the go.mod file. The argument is the module’s name, which by convention is where it would live if you published it:

go mod init github.com/<yourUserName>/nameOfProject

For throwaway programs example.com/anything is fine. The name only matters once other code imports yours.

go mod tidy

go mod tidy makes go.mod match your code: it adds anything you import but have not declared, and removes anything declared but no longer imported. Run it whenever you add or remove an import and you will rarely see a dependency error.

go mod tidy

I.b go get and go install

When you import someone else’s package, say the Excel library we use in chapter 9, Go needs to know which version you want. Try to run a program that imports a package your go.mod does not list and you will see:

main.go:8:2: no required module provides package github.com/xuri/excelize/v2; to add it:
	go get github.com/xuri/excelize/v2

Do what it says:

go get github.com/xuri/excelize/v2

That downloads the latest version into a cache on your machine and records it in go.mod (and a checksum in go.sum, which you also commit). go get only works inside a module, which is why go mod init comes first.

go install is the sibling for tools: programs written in Go that you want on your PATH rather than in your project. For example, the tool that checks your dependencies for known vulnerabilities:

go install golang.org/x/vuln/cmd/govulncheck@latest

The rule of thumb: go get for packages your code imports, go install name@version for commands you run. If the terminal cannot find a command you just installed, your Go bin folder is not on your PATH yet; chapter 1, section III.b, fixes that in one line.

I.c gofmt

There are many heated arguments on the internet about how a programming language’s code should be formatted. In Go there is one answer and it is a program. The editor extension runs it every time you save; when you are somewhere without it, run it yourself.

Suppose the hello world program from chapter 1 looked like this:

package main 
import "fmt"
func main(){
fmt.Println("Hello, world!")
}

Then:

gofmt -w main.go

and it becomes:

package main

import "fmt"

func main() {
	fmt.Println("Hello, world!")
}

go fmt ./... does the same for every file in the module. The ./... pattern means “this folder and everything under it” and you will type it a lot.

I.d go run

Compiles and runs your program in one step. Useful when you are working on new code and want instant feedback.

go run .

You will also see go run main.go out there. Prefer .: it builds every file in the folder, so it keeps working when your program grows past one file.

I.e go build

Run this when you have finished a program and want an executable you can copy somewhere or hand to someone. Command-line tools and web servers both end up as one file this way.

go build -o hello .

And you can build for a different operating system or processor than the one you are sitting at, by setting two environment variables for the duration of the command:

GOOS=linux GOARCH=arm64 go build -o hello-linux-arm64 .

(In PowerShell: $env:GOOS="linux"; $env:GOARCH="arm64"; go build -o hello-linux-arm64 ..) Don’t worry if this does not mean much yet. Just note that taking a Go program to another machine is one command.

I.f go vet

go vet reads your code looking for mistakes the compiler accepts but that are almost certainly bugs: a Printf with the wrong number of arguments, an error you computed and then ignored, that sort of thing. It is free, so run it before you call something done:

go vet ./...

I.g go test

Testing your code speeds up development and is a requirement in most professional settings, so having it built in is a gift. If you see *_test.go files in a folder, this runs them:

go test ./...

We’ll write tests of our own once the projects are big enough to deserve them; every project from chapter 7 onward in the repo has a test beside it you can read.

II. Data Types

Computers, unlike humans, need precise instructions, and part of that precision is which type of data an instruction works on. This section covers the types you’ll use and meet most while working with Go.

II.a Integers

Integers are whole numbers, no decimal point.

You use a signed integer type if a number can be negative; otherwise the unsigned type can be used.

Signed

Imagine a program that works out your bank balance after the monthly bills, and you have unfortunately had to think about the case where you are negative money. You’ve overspent and might need a credit card, a loan, or part of the next paycheck to cover the rest.

The result can be negative, which tells us to use a signed integer.

Create a new folder ex1 with a file main.go and type the following:

// ex1
package main

import "fmt"

func pay(money, bills int) int {
	return money - bills
}

func main() {
	var paycheck, bills int = 4000, 5000
	fmt.Printf("bank account balance: %d\n", pay(paycheck, bills))
}

then run go run . and you’ll see you indeed might have a problem.

Go lets you say how many bits an integer holds with int8, int16, int32 and int64, but for almost everything in this book plain int is the right choice.

Unsigned

Unsigned integers cannot be negative, so if we swap int for uint in the same code, you’ll see that it breaks:

// ex2
package main

import "fmt"

// changed int to uint
func pay(money, bills uint) uint {
	return money - bills
}
func main() {
	// changed int to uint
	var paycheck, bills uint = 4000, 5000
	fmt.Printf("bank account balance: %d\n", pay(paycheck, bills))
}

It prints the nonsensical 18446744073709550616 instead of -1000. The number wrapped around, the way a car’s odometer would if you could drive it backwards past zero.

Just like int, there are uint8 through uint64, and just like with int, most readers only ever need the plain one, and most of the time not even that.

II.b Floats

Floats are your decimal numbers, positive or negative. For the bank balance above a float is the honest choice, because money is rarely whole numbers.

// ex3
package main

import "fmt"

// int -> uint -> float64 if you're coding along
func pay(money, bills float64) float64 {
	return money - bills
}

func main() {
	// a literal like 5000.0 is a float64 unless you say otherwise
	var paycheck, bills float64 = 4000, 5000.0
	fmt.Printf("bank account balance: %.2f\n", pay(paycheck, bills))
}

There is no plain float the way there is a plain int: you pick float32 or float64. Pick float64. It is what a literal like 5000.0 already is, it is what the math package works in, and float32 only earns its place when you are storing millions of numbers and memory matters.

An explanation of bits is outside this book’s scope, and it isn’t necessary to write Go code that automates your daily tasks.

II.c string

The text you are reading, and most of what sits inside documents, is the string type. Chances are it is the type you’ll use most.

In Go, as in many languages, a string literal goes in double quotes:

// ex4
package main

import "fmt"

func main() {
	var example string = "Hello, world!"
	fmt.Printf("%s", example)
}

II.d booleans

A boolean is either true or false. You’ll use them when checking conditions in a program, which is the whole of the next chapter.

// ex5
package main

import "fmt"

func main() {
	//Default value will be false
	var a bool
	fmt.Printf("a's value is %t\n", a)
}

III. String Concatenation

Concatenation is joining two strings into one, and it uses the + symbol:

// ex6
package main

import "fmt"

func main() {
	var s1 string = "Hello "
	var s2 string = "world!"
	fmt.Printf("%s", s1+s2)
}

IV. Variables

We’ve used variables throughout the examples, as the var keyword shows. Think of a variable as an empty moving box. Before you move, you put things in it, and if you are the organised type you put one kind of thing per box: the box labelled “kitchen glasses” holds glasses.

Go expects you to use variables in that organised way: one type per variable.

There is a shortcut that lets you skip both var and the type, the := short declaration, and it works inside functions:

// ex7
package main

import "fmt"

func pay(money, bills int) int {
	return money - bills
}
func main() {
	// var, int -> :=
	paycheck, bills := 4000, 5000
	fmt.Printf("bank account balance: %d\n", pay(paycheck, bills))
}

main() is a function, so we can use := there, and it is the preferred form inside functions. In codebases, less is more.

IV.a Assignment statements

Unlike :=, which declares a new variable, = assigns to one that already exists. The long form var name type = value declares and assigns in one line and is what you use outside functions:

// ex8
package main

import "fmt"

func main() {
	var example string = "Gentle reminder"
	fmt.Printf("%s", example)
}

This says example is a variable of type string, and we are assigning the string “Gentle reminder” to it.

IV.b Naming conventions

Go programmers prefer short names in mixedCaps: userName, getUserName, never user_name. Underscores and hyphens are out. A name that starts with a capital letter is exported, meaning other packages can see it; a lower-case name is private to its package. Variable names in Go run short, and the shorter the scope the shorter the name:

// ex9
package main

import "fmt"

func getUserName() string {
	return "Reader"
}
func main() {
	n := getUserName()
	fmt.Printf("Hello Dear %s", n)
}

This is the opposite of Python’s taste for name_of_user = get_user_name(), which spells things out as much as possible.

V. Type Conversions

Imagine a document that contains 1.99 and you want to use it as a number in a calculation. Changing a value from one type to another is called conversion (or casting, in some languages), and you meet it most with string data.

Here are the conversions you will reach for most often.

string to int

// ex10
package main

import (
	"fmt"
	"strconv"
)

func main() {
	strVar := "100"
	intVar, err := strconv.Atoi(strVar)
	if err != nil {
		fmt.Println("not a number:", err)
		return
	}
	// %T prints the type of a value
	fmt.Printf("%v is a %T\n", intVar, intVar)
}

strconv.Atoi (“ASCII to integer”) returns two values, the number and an error, because not every string is a number. We check the error, which is a habit worth building from day one; chapter 4 explains the pattern.

int to string

// ex11
package main

import (
	"fmt"
	"strconv"
)

func main() {
	i := 10
	s1 := strconv.FormatInt(int64(i), 10)
	s2 := strconv.Itoa(i)
	fmt.Printf("%v, %v\n", s1, s2)
}

bytes to string

// ex12
package main

import "fmt"

func main() {
	s := string([]byte{65, 66, 67, 226, 130, 172})
	fmt.Println(s)
}

bool to string

// ex13
package main

import (
	"fmt"
)

func main() {
	B := true
	str := fmt.Sprintf("%v", B)
	fmt.Println(str)
}

VI. Comparisons

Comparisons are rarely used alone; they usually sit inside the control-flow statements of the next chapter. I’m showing them here to build familiarity.

equal to

// ex14
package main

import (
	"fmt"
)

func main() {
	fmt.Println("let's see if these two identical strings evaluate to equal")
	s1 := "hi"
	s2 := "hi"
	// okay, I showed a little conditional flow here
	if s1 == s2 {
		fmt.Printf("yes, they are!")
	}
}

not equal to

// ex15
package main

import (
	"fmt"
)

func main() {
	fmt.Println("let's see if these two non-identical strings evaluate to equal")
	s1 := "hi"
	s2 := "these are not the droids you're looking for"
	// okay, I showed a little conditional flow here
	if s1 != s2 {
		fmt.Printf("They're not the same string!")
	}
}

greater than

// ex16
package main

import (
	"fmt"
)

func main() {
	bills := 13642.32
	paycheck := 10000.00
	if bills > paycheck {
		fmt.Printf("Maybe I should update my resume...")
	}
}

less than

// ex17
package main

import (
	"fmt"
)

func main() {
	paycheck := 13642.32
	bills := 10000.00
	if bills < paycheck {
		fmt.Printf("alright, alright, alright")
	}
}

greater than or equal to

// ex18
package main

import (
	"fmt"
)

func main() {

	bills := 13642.32
	paycheck := 10000.00
	if bills >= paycheck {
		fmt.Printf("I'll never gonna financially recover from this")
	}
}

less than or equal to

// ex19
package main

import (
	"fmt"
)

func main() {
	paycheck := 13642.32
	bills := 10000.00
	if bills <= paycheck {
		fmt.Printf("seems risky.")
	}
}

Conclusion

That is the vocabulary: the toolchain, the handful of types you will meet every day, variables, conversions and comparisons. Next chapter we put the comparisons to work and make programs decide things.

← Back to the blog