Without the foundations, hopefully, you’d agree it would be more difficult to grasp some of the stuff we’ll cover later.
Which is why so far we’ve been scene-setting. First, we covered packages then, in the last edition, Go functions.
In this edition, we’re going to look at more building blocks of the Go language, namely variables, constants, and types.
With this knowledge in place, we’ll be able to explore the Go programming language further, faster, and syntax will not be a barrier to our understanding.
Overview
Variables and constants both provide us with a mechanism to store values.
The difference is that variables can be changed after initialisation with further assignments, whereas constants, as the name implies, can not; once initialised that is the value they hold.
Types provide information about what “kind” of data a variable or constant can hold; strings and integers, for example.
Go is a statically-typed language, which means that when a variable is initialised with a type, it can only store values of that type for the life of the program - we call this life the “run-time”.
Type is checked and enforced by the compiler. Type can be inferred by the compiler meaning it is not necessary to always explicitly state a variable or constant’s type.
Although doing so could be said to improve the readability of your program code you have a choice in this regard.
A dynamically-typed language, by contrast, allows a variable to hold data of differing type at run-time. There are no compile-time checks to enforce type so problems may manifest themselves as difficult to find bugs when the type of a variable is unexpectedly changed by assigning a value of a different type.
That’s the overview done, now let’s get into the specifics.
Variables
Go variables can be declared and initialised in a number of different ways. In fact, for an opinionated language, I often marvel at how many different ways there are to do this!
When a variable is declared and no assignment is made to it (i.e. no value is given to it), it is initialised to the empty value for the type it stores.
Below we look at the various different ways we can create variables. We then also provide a recommendation for using just two or three of these approaches - our goal being to improve both readability and consistency in our code.
Since we haven’t covered types in any detail yet, the examples use a single type, string. There are more, and we will get to them.
In the following examples, we declare two variables, project and description. In the first examples, we simply declare them so they are initialised to their empty values.
In the second set of examples, we assign our own values to them.
Just look at how many different ways we can do something so simple!
Declaration only, initialised to type’s empty value
// Example 1
var project string
var description string
// Example 2
var (
project string
description string
)
// Example 3
var project, description string
Declaration with assignment
// Example 4
var project string = "GolangAtSpeed"
var description string = "Go tutorials, tips and tricks"
// Example 5
var project = "GolangAtSpeed"
var description = "Go tutorials, tips and tricks"
// Example 6
var (
project string = "GolangAtSpeed"
description string = "Go tutorials, tips and tricks"
)
// Example 7
var (
project = "GolangAtSpeed"
description = "Go tutorials, tips and tricks"
)
// Example 8
var project, description string = "GolangAtSpeed","Go tutorials, tips and tricks"
// Example 9
var project, description = "GolangAtSpeed","Go tutorials, tips and tricks"
// Example 10
project := "GolangAtSpeed",
description := "Go tutorials, tips and tricks"
// Example 11
project, description := "GolangAtSpeed","Go tutorials, tips and tricks"
Can you pick out patterns?
When we initialise variables with an assignment there are three patterns to be aware of.
In the first, we use the var keyword and explicitly state type for each variable. See examples 4, 6, and 8 above.
In the second pattern, again we use the var keyword, though we don’t state type in the variable declaration. Instead, we allow the compiler to infer the type from the assignment made. See examples 5, 7 and 9.
In the third pattern, see examples 10 and 11, we omit the var keyword and use the short-form for initialising a variable with an assignment like so:
project := "GolangAtSpeed"
One thing to note is that this “short-form” declaration with assignment can only be used inside of functions.
That’s a lot of ways to achieve the same thing, so what is the recommendation; how many of these approaches should we use?
Like most things, it’s a matter of preference.
Experienced Go developers will recognise any and all of these approaches, but you should probably limit yourself to choosing two or three if possible.
The short-form version is the one you should use most within functions. It simply makes things easier to read.
project := "GolangAtSpeed"
If declaring a variable with no assignment then I favour the style in example 1 above.
var project string
var description string
If I’m declaring and initialising a global variable (outside of a function) then I favour example 4 - although linting tools will point out that the type is not required as the compiler can infer it. Many developers will omit the type, this is just my preference.
var project string = "GolangAtSpeed"
var description string = "Go tutorials, tips and tricks"
So pick your favourite approaches, but try to limit them to two or three, and above all aim to be consistent.
Constants
You’ll be pleased to know that constants can be declared in much the same way as variables, but instead of the var keyword, the const keyword is used to show it is a constant.
There is one exception to the above: we cannot use the short-form syntax to declare and assign to a constant, not even inside of a function.
Here is an example with type inferred, although you can explicitly include the type too.
const project = "GolangAtSpeed"
Types
Finally, we’re going to explore type and the Go type system.
As we’ve mentioned in the overview, since Go is a statically-typed language it’s important that you have a good grasp of types, how and when to use them, and also how to convert between them - you will definitely need to.
When you are starting with Go, you may encounter errors at compile time stating that a variable of type X cannot be used as an input to Y, or similar. These errors can be frustrating to fix when you just want to see your program run, but remember, the compiler is helping you.
By getting type correct at compile-time we totally eliminate a whole class of errors that can occur at run-time, and I know where I prefer to take my pain!
As of Go version 1.14, Go has 26 different types, of which 17 are classified as basic (or primitive) types and the remainder, composite types. Composite types are made of up basic types or other composite types.
Basic types
In practice, when you are starting with Go - and even after you’ve been using it for some time - you may find yourself repeatedly using only a small subset of Go’s built-in basic types.
I’ve listed these below, there are just four.
// true or false
var answer bool
// whole number
var myNumber int
// number with decimal level of precision e.g 3.14
var pi float64
// string
var project string
Go also supports two alias types, byte and rune.
// alias for uint8
var myByte byte
// alias for int32
var myRune rune
int is not an alias, though depending on the architecture of the compiling system it will represent either int32 or int64 types. The difference is the size of integer which can be stored.
Conversion
Of course, it is possible to convert between values of different types so that one can be assigned to another, but the conversion must be explicit.
Here is a small example which shows how to convert one int to an int8 type.
var first int = 65
var second int8
second = int8(first) // conversion happens here
fmt.Println(second)
// output
65
To convert between byte and string as we often need to, we can use the []byte() and string() conversion operations, as a string can also be represented as a slice of bytes. Again, we’ll get to slices soon.
This approach does not work for all types. For example, when converting between int and string. In these circumstances, you should check out the conversion functions in the strconv package, a part of the Go standard library.
A simple example follows:
package main
import (
"fmt"
"strconv"
)
func main() {
a := strconv.Itoa(65)
fmt.Printf("%q", a)
}
// output
"65"
Composite types
Go supports the following composite types. We won’t go into much detail here, because each of these composite types will be the subject of a future tutorial, with the exception of function, which we’ve already covered.
Struct
Function
Array
Slice
Map
Channel
Interface
Custom types
In Go, we can create custom types which are aliases for provided built-in types.
Why would we do this? Mainly for safety and clarity. For example, when dealing with multiple variables of the same type if we pass the wrong variable as the wrong argument in a function, the results could be very different than those we expected - and we would not know.
Custom types help us mitigate this risk.
To illustrate the point more clearly I’ve created two simple example programs below. Each also has a live link to the Go Playground so that you may try them for yourself.
In our first example we have a simple divider function that accepts two ints, a numerator and a denominator, and returns the result as a float64.
func main() {
var n, d int
n, d = 1, 4
r := divider(n, d)
// try these
//r = divider(d, n)
//r = divider(n, n)
fmt.Println(r)
}
func divider(n int, d int) float64 {
return float64(n)/float64(d)
}
The example on the Go Playground is here:- https://play.golang.org/p/P8pWFgiovL2
What happens if you pass the denominator variable as the numerator and vice versa, or pass the same variable as both the numerator and denominator arguments? Try uncommenting the additional lines and running the program to see for yourself.
See how we could get this wrong and never notice when writing our code.
Now, let’s check out this second example which uses custom types.
type numer int
type denom int
func main() {
var n numer
var d denom
n, d =1, 4
r := divider2(n, d)
// try these
//r = divider2(d,n)
//r = divider2(n,n)
fmt.Println(r)
}
func divider2(n numer, d denom) float64 {
return float64(n)/float64(d)
}
This example on the Go Playground is here:- https://play.golang.org/p/smRWiD2c9_N
Notice the “numer” and “denom” custom types? Now, try uncommenting those additional lines and then running the program - the result is that the program will not compile.
We have added more specific type requirements on the function parameters, and, even though both numer and denom custom types are aliases for int, a number is not a denom, and our helpful Go compiler will point this out!
Summary
So, that’s variables, constants and types introduced. We’ll do much more with these later, but this is the grounding I think you need for now.
As always, please share this widely if you enjoyed it! I publish one Golang “learning” newsletter a week, and occasionally another with a few interesting links I’ve found.
Until next time!