Working with Variables in Go

Can you program without variables? Maybe in some esoteric languages, but in Go, you can’t. So it’s important to get acquainted with how variables work in the language so that you can write effective programs. And that’s what this article is all about.

Variables are an essential component of Go programs. A variable is a name given to a location in memory where some data is stored. Each variable also has a data type (string, int, bool e.t.c.) associated with it.

To write good Go programs, you need to learn the idiomatic way of working with variables in the language, so that’s what this tutorial will cover. You can run the code snippets presented on this page in the Go playground, or locally on your computer if you prefer.

Naming conventions

Before we discuss how variables are created in Go, let’s look at the naming conventions that you should be aware of. A name in Go begins with a Unicode letter or an underscore, followed by any number of additional letters, digits and underscores. Names are also case sensitive in Go, so fetchNews and fetchnews are different names.

The convention in Go is to use camelCase for names, and Go programmers tend to lean towards short names especially if the scope of the variable is small, although there is no limit for how long a name could be. Go also has 25 keywords such as var and func which cannot be used as identifiers in the language.

How to create variables in Go

The snippet below shows the two main ways of creating variables in Go:

package main

import (
	"fmt"
)

func main() {
	var hello = "Hello"
	world := "world!"
	fmt.Println(hello, world) // Hello world!
}

We have a variable hello that is declared using the var keyword and it is assigned the string “Hello” and a second variable world declared using the short variable declaration syntax and is assigned the string “world!”. The fmt.Println() method subsequently prints the text “Hello world!” to the standard output.

There are some differences between these two ways of creating variables in Go. Let’s talk about the var keyword first.

var declarations

A var declaration has the general form shown below:

var name [type] = expression

such as

var str string = "A String"

Either the type or the expression may be omitted, but not both at the same time. As you’ve already seen, the hello variable declared in the previous section omits the type but includes the expression. In the case where the type is omitted from the declaration, it would be inferred from the expression. This is known as type inference and it makes it easy to declare a variable without having to explicitly annotate its type.

Here are a some examples:

var a = "Hello"
var b = 23
var c = true
var d = 2.3

fmt.Printf("The type of a, b, c, d is: %T, %T, %T and %T respectively", a, b, c, d)
// Prints: The type of a, b, c, d is: string, int, bool and float64 respectively

It’s also possible to declare multiple variables using a single var declaration as shown below:

var a, b, c, d = "Hello", 23, true, 2.3
// or
var (
	a = "Hello"
	b = 23
	c = true
	d = 2.3
)

Zero values

Go allows you to create variables without explicitly initialising them to a value. In this case, the type of the variable must be present.

var a string
var b int
var c bool
var d float64

// which can also be written as

var (
	a string
	b int
	c bool
	d float64
)

If you’re declaring multiple variables that are all of the same type without initialising them, you can use the syntax below:

var a, b, c int // a, b, c are all of type int

When the expression part of a variable declaration is omitted, the variable will be assigned the zero value for that type which is 0 for number types, an empty string ("") for strings, false for booleans, and nil for interfaces and pointers.

Here’s an example that demonstrates this concept:

package main

import (
	"fmt"
)

func main() {
	var (
		a string
		b int
		c bool
		d float64
	)
	fmt.Println(a, b, c, d) // Output: "" 0 false 0
}

This concept of zero values has an important implication that you must be aware of: there is nothing like an uninitialised variable in Go. A variable will always contain a value, either the one you assigned to it, or an implicitly assigned zero value for its type. This behaviour exempts Go programmers from dealing with the uninitialised variable problem that exists in other programming languages.

Short variable declaration

The second type of variable declaration in Go is called the short variable declaration using the := operator. This is the way the majority of variables are declared in Go due to its terse syntax.

str := "A string"
ans := 22 + 20

Just like var declarations, multiple short variable declarations can be made in a single line:

a, b, c := 1, 2, 3
d, e, f := "Hello", true, 5.8

Unlike var declarations, there is no implicit assignment to zero values when using the short variable declaration syntax, and you cannot annotate the type of the variable; it is always inferred from the expression on the right-hand side.

Variable assignment

You can assign to an already declared variable using the = operator. All variables are mutable in Go, but the type associated with a variable cannot be changed after declaration.

var name = "sally"
fmt.Println(name) // sally
name = "polly"
fmt.Println(name) // polly
name = true // cannot use true (type untyped bool) as type string in assignment

You can also assign to several variables at once using another form called tuple assignment. The right-hand side expression is evaluated before any left-hand side variables are updated.

// declaration
a, b := 1, 2
// tuple assignment
a, b = a + 1, b + 2 // 2, 4

When using this tuple assignment style to assign variables to a function’s return values, the number of variables must match the number of return values.

Variable scope

A variable in Go can be declared either at package or block level. A package-level variable is one that appears outside of any function. It is accessible throughout the package and can only be a var declaration due to the := operator not being available outside functions.

On the other hand, a block-level variable is one that is declared inside a block such as inside a function, for loop, if block, or even a standalone block. The idiomatic way to declare block-level variables is by using the short variable declaration syntax.

Here’s an example that denotes both package-level, and block-level variable declarations:

package main

import (
	"fmt"
)

// t is a package-level variable that can be accessed
// anywhere in the `main` package. You can say that it is
// global to the package
var t = true

func main() {
	// f is a block-level variable accessible from its point of declation to
	// to the end of the function
	f := false

	{
		// i is a block-level variable that's only valid from this point
    // until the end of the block
		i := 20
		fmt.Println(i) // 20
	} // this block scope is now over so i is no longer valid

	fmt.Println(t, f) // true false
}

If you’ve worked with a lexically scoped programming language before, this behavior should be familiar to you. Aside from explicit blocks that are clearly defined with a matching pair of curly braces, Go has a few implicit blocks that you should be aware of. For example, each clause in a switch or select statement is also an implicit block even though curly braces are not present:

func main() {
	s := "world"
	switch s {
	case "hello":
		// i can be accessed only within this clause
		// and not the whole switch block
		i := 10
		fmt.Println(i)
	case "world":
		// i is out of scope here so this code will fail to compile
		fmt.Println(i)
	}
}

Another example is in if statements. This is one you are likely to encounter more frequently in Go programs:

package main

import (
	"errors"
	"fmt"
)

func main() {
	// this `err` variable is scoped to the whole
	// function
	err := errors.New("An error")

	// this `err` variable is scoped to only this `if`
	// block (and `else` blocks if any). It shadows the
	// previous `err` variable
	if err := fmt.Errorf("Another error"); err != nil {
		fmt.Println(err) // Another error
	} // `err` goes out of scope here

	fmt.Println(err) // An error
}

As an aside, the var declaration syntax may also be used to declare a block-level variables, but it’s typically only used when you want to initialise a variable to its zero value, then assign to it later.

func main() {
	var s string // s is initialised to its zero value which is ""

	s = "foo" // assignment occurs later in the function
}

Shadowing

Shadowing is a feature in Go that allows you to declare a variable in one block, and declare another variable with the same name in an inner block. Here’s an example:

func main() {
	str := "world"

	{
		str := "hello" // outer str is inaccessible from this point
		fmt.Println(str) // hello
	}

	fmt.Println(str) // world
}

The first str variable declared in the main function is assigned the string value “world”. In an inner block, a second str variable is declared and assigned the string value of “hello”. Whenever you access the str variable in the inner block, it will always refer to the inner declaration. So we say that the first str variable is shadowed by the second.

Note that the inner str does not affect the outer str in any way. In fact, the inner variable does not have to be of the same type as the outer one because they are actually completely different from each other. They just happen to share the same name. Keep in mind that due to this behaviour you will not be able to access the outer str from the inner block unless you change its name.

Variable shadowing does not apply if you attempt to redeclare a variable within the same block. The program will fail to compile:

func main() {
	s := "world"
	s := "hello" // no new variables on left side of :=
}

However, the program below will compile and run just fine.

func main() {
	var a, b int
	c, a, b := 3, 1, 2
	fmt.Println(a, b, c) // 1 2 3
}

Although it appears as if the a and b variables are being redeclared on the second line of the main function, that’s actually not the case. What happens here is that only the c variable is declared while the other two are assigned to. This reason is that the := operator does always declare all the variables on its left-hand side.

At least one new variable needs to be present when using the short declaration syntax for this to work though. If we remove the c variable declaration from the above snippet, the code will fail to compile as before:

func main() {
	var a, b int
	a, b := 1, 2 // no new variables on left side of :=
	fmt.Println(a, b)
}

Wrap up

In this article, you learned how to create and assign variables in Go, what zero values are and how variable scoping and shadowing work in the language. Now that you’ve explored how variables work, the next article will look at the data types they can have.

Thanks for reading, and happy coding!