Chapter 1: Go Language Basics
DevOps is a concept that has been floating around since the early 2000s. It is a popularization of an operations discipline that relies on programming skills with development psychology popularized by Agile.
Site reliability engineering (SRE) is now considered a subdiscipline of DevOps, though it is likely the precursor to DevOps and relies more heavily on software skills and Service - Level Obligation (SLO)/Service - Level Agreement (SLA) modeling.
During my early time at Google, like many of today's DevOps shops, we used Python heavily. I think C++ was too painful for many SREs, and we had Python celebrities in Guido van Rossum and Alex Martelli.
But, as time wore on, many of the groups working in Python started having scaling issues. This included everything from Python running out of memory (requiring us to hack in our own malloc
) to the Global Interpreter Lock (GIL) preventing us from true multithreading. At scale, we found that the lack of static types was giving us an abundance of errors that should have been caught at compile time. This mirrored what production services had seen years before.
But, Python came with more than compile-time and service-scaling issues. Simply moving to a new version of Python in the fleet might cause a service to stop working. The Python version run on Google machines would often get upgraded and expose bugs in your code that the previous version did not. Unlike a compiled binary, you could not just roll back to an old version.
Several of us in different organizations were looking to solve these types of problems without having to use C++. For my personal journey, I heard about Go from my colleagues in our Sydney office (Hey, Ross!). It was the pre-1.0 days, but they said it was already showing a lot of promise. I can't say I was even remotely convinced that what we needed was another language.
About 6 months later, however, I had bought Go hook, line, and sinker. It had everything we needed without everything we didn't. Now, it was still pre-1.0 days, so there was a certain amount of churn back then that was unpleasant (such as finding that Russ Cox had changed the time
package over the weekend, so I had to rewrite a bunch of code). But, the benefits after writing my first service were undeniable.
I spent the next 4 years moving my department from a complete Python shop to almost a complete Go shop. I started teaching classes in Go across the world, targeted at operations engineers, rewriting core libraries for Go, and evangelizing to what was probably an annoying amount. Just because Go was invented at Google, it didn't mean that the engineers wanted to throw away their Python code and learn something new; there was more than a little resistance.
Now, Go has become the de facto language for cloud orchestration and software in the larger world (from Kubernetes to Docker). Go comes with all the tools you need to make huge strides in the reliability of your tooling and ability to scale.
Because many of these cloud services are written in Go, their parts are available to you by accessing their packages for your own tooling needs. This can make writing tooling for the cloud an easier experience.
For the next two chapters, I will be sharing my 10+ years' experience of teaching Go to engineers around the world to give you the basics and essentials of the Go language. Much of what you will read here is based on my free Go basics video training course, https://www.golangbasics.com. This course will differ slightly from that one in that it is more condensed. As you work your way through the book, we will continue to extend your knowledge of the Go language's standard library and third-party packages.
This chapter will cover the following main topics:
- Using the Go Playground
- Utilizing Go packages
- Using Go's variable types
- Looping in Go
- Using conditionals
- Learning about functions
- Defining public and private
- Using arrays and slices
- Getting to know about structs
- Understanding Go pointers
- Comprehending Go interfaces
Now, let's get the basics down and get you on your way!
Using the Go Playground
The Go Playground, which you can find at https://play.golang.org/, is an online code editor and compiler that allows you to run Go code without installing Go on your machine. This is the perfect tool for our introductory chapters, allowing you to save your work online without the initial fuss of installing the Go tooling, or finding a code editor, for example.
There are four important parts of the Go Playground:
- The code editing pane
- The console window
- The Run button
- The Share button
The code editing pane, which is the yellow portion of the page, allows you to type in the Go code for your program. When you hit the Run button, the code will be compiled and then run with the output sent to the console, which is the white portion of the page below the code editor.
The following screen shows a glimpse of what the Go Playground does:

Figure 1.1 – Go Playground code editor
Clicking the Share button will store an immutable copy of the code and will change the URL from play.golang.org
into a shareable link, such as play.golang.org/p/HmnNoBf0p1z
. This link is a unique URL that you can bookmark and share with others. The code in this link cannot be changed, but if you hit the Share button again, it will create a new link with any changes.
Later chapters, starting with Chapter 4, Filesystem Interaction, will require installing the Go tooling for your platform.
This section taught you about the Go Playground and how to use it to write, view, share, and run your Go code. The Playground will be used extensively throughout the book to share runnable code examples.
Now, let's jump into writing Go code, starting with how Go defines packages.
Utilizing Go packages
Go provides reusable blocks of code that can be imported into other code using packages. Packages in Go are synonymous with libraries or modules in other languages. Packages are the building blocks of Go programs that divide the content into understandable parts.
This section will cover how to declare and import a package. We will discuss how to deal with package name conflicts, explore rules around packages, and we will write our first main package.
Declaring a package
Go divides programs into packages, sometimes called modules or libraries in other languages. Packages live on a path, and the path is made to look like a path to a directory on a Unix-like filesystem.
All Go files in a directory must belong to the same package. The package is most commonly named the same as the directory it lives in.
Declaring a package happens at the top of the file, and should only be preceded by a comment. Declaring a package is as simple as the following:
// Package main is the entrance point for our binary. // The double slashes provides a comment until the end of the line. /* This is a comment that lasts until the closing star slash. */ package main
package main
is special. All other package names declare a package that must be imported into another package to be used. package main
will declare func main()
, which is the starting point for a binary to run.
All Go files in a directory must have the same package header (compiler-enforced). These files, for most practical purposes, act as if they are concatenated together.
Let's say you have a directory structure as follows:
mypackage/ file1.go file2.go
Then, file1.go
and file2.go
should have the following:
package mypackage
When mypackage
is imported by another package, it will include everything declared in all files in the mypackage
directory.
Importing a package
There are two general types of packages:
- The standard library (stdlib) packages
- All other packages
Standard library packages stand out because they don't list some repository information in their path, such as the following:
"fmt" "encoding/json" "archive/zip"
All other packages generally have repository information preceding them, as follows:
"github.com/johnsiilver/golib/lru" "github.com/kylelemons/godebug/pretty"
Note
A complete listing of stdlib
packages can be found at the following link: https://golang.org/pkg/.
To import packages, we use the import
keyword. So, let's import the standard library fmt
package and the mypackage
package, which lives at github.com/devopsforgo/mypackage
:
package main import ( "fmt" "github.com/devopsforgo/mypackage" )
It is important to note that the filenames are not part of the package path, but simply the directory path.
Using a package
Once you've imported a package, you can start accessing functions, types, or variables declared in the package by prefacing what you want to access with the name of the package and a period.
For example, the fmt
package has a function called Println()
that can be used to print a line to stdout. If we want to use it, it is as simple as the following:
fmt.Println("Hello!")
Package name conflicts
Let's say you have two packages named mypackage
. They both have the same name, so our program won't be able to tell which one we are referring to. You can rename a package import into whatever name you want:
import( "github.com/devopsforgo/mypackage" jpackage "github.com/johnsiilver/mypackage" )
jpackage
declares that in this package, we will refer to github.com/johnsiilver/mypackage
as jpackage
.
This ability allows us to use two similarly named packages as follows:
mypackage.Print() jpackage.Send()
Now, we will look at an important rule around packages that improves compile-time and binary size.
Packages must be used
Let's introduce you to the following rule: If you import a package, you must use it.
One of the things that the Go authors noticed about many of the other programming languages being used at Google was that they often had unused imports.
This was leading to compile times that were longer than needed and, in some cases, binary sizes that were much bigger than required. Python files were packaged in a proprietary format to ship around production, and some of these unused imports were adding hundreds of megabytes to the files.
To prevent these types of problems, Go will not compile a program that imports a package but doesn't use it, as shown here:
package main import ( "fmt" "sync" ) func main() { fmt.Println("Hello, playground") }
The preceding code outputs the following:
./prog.go:5:2: imported and not used: "sync"
In certain rare circumstances, you may need to do a side effects import, in which just loading the package causes something to happen, but you don't use the package. This should always be done in package main
and requires prepending with an underscore (_
):
package main import ( "fmt" _ "sync" //Just an example ) func main() { fmt.Println("Hello, playground") }
Next, we will declare a main package and discuss the basics of writing a Go program that imports a package.
A Go Hello World
Let's write a simple hello world program that is similar to the default program in the Go Playground. This example will demonstrate the following:
- Declaring a package
- Importing the
fmt
package from the standard library, which can print to our screen - Declaring the
main()
function of a program - Declaring a string variable using the
:=
operator - Printing the variable to the screen
Let's see what this looks like:
1 package main 2 3 import "fmt" 4 5 func main() { 6 hello := "Hello World!" fmt.Println(hello) 7 8 }
In our first line, we declared the name of our package using the package
keyword. The entrance point for any Go binary is a package named main
that has a function called main()
.
In our third line, we import the fmt
package. fmt
has functions for doing string formatting and writing to various outputs.
On our fifth line, we declare a function called main
that takes no arguments and returns no values. main()
is special, as when a binary is run, it starts by running the main()
function.
Go uses {}
to show where a function starts and where a function ends (similar to C).
The sixth line declares a variable named hello
using the :=
operator. This operator indicates that we wish to create a new variable and assign it a value in a single line. This is the most common, but not the only, way to declare a variable.
As Go is typed, so :=
will assign the type based on the value. In this case, it will be a string, but if the value was an integer (such as 3
), it would be the in
t type, and if a floating-point (such as 2.4
), it would be the float64
type. If we wanted to declare a specific type, such as int8
or float32
, we would need some modifications (which we will talk about later).
On the seventh line, we call a function that is in the fmt
package called Println
. Println()
will print the contents of the hello
variable to stdout
followed by a new line character (\n
).
You will notice that the way to use a function declared in another package is to use the package name (without quotes) + a period + the name of the function. In this case, fmt.Println()
.
In this section, you have learned how to declare a package, import a package, what the function of the main
package is, and how to write a basic Go program with a variable declaration. In the next section, we will go into some depth on declaring and using variables.
Using Go's variable types
Modern programming languages are built with primitives called types. When you hear that a variable is a string or integer, you are talking about the variable's type.
With today's programming languages, there are two common type systems used:
Go is a statically typed language. For many of you who might be coming from languages such as Python, Perl, and PHP, then those languages are dynamically typed.
In a dynamically typed language, you can create a variable and store anything in it. In those languages, the type simply indicates what is stored in the variable. Here is an example in Python:
v = "hello" v = 8 v = 2.5
In this case, v
can store anything, and the type held by v
is unknown without using some runtime checks (runtime meaning that it can't be checked at compile time).
In a statically typed language, the type of the variable is set when it is created. That type cannot change. In this type of language, the type is both what is stored in the variable and what can be stored in the variable. Here is a Go example:
v := "hello" // also can do: var v string = "hello"
The v
value cannot be set to any other type than a string.
It might seem like Python is superior because it can store anything in its variable. But in practice, this lack of being specific means that Python must wait until a program is running before it can find out there is a problem (what we call a runtime error). It is better to find the problem when the software is compiled than when it is deployed.
Let's take a look at a function to add two numbers together as an example.
Here is the Python version:
def add(a, b): return a+b
Here is the Go version:
func add(a int, b int) int { return a + b }
In the Python version, we can see that a
and b
will be added together. But, what types are a
and b
? What is the result type? What happens if I pass an integer and a float or an integer and a string?
In some cases, two types cannot be added together in Python, which will cause a runtime exception, and you can never be sure of what the result type will be.
Note
Python has added type hints to the language to help avoid these problems. But, practical experience has taught us with JavaScript/Dart/TypeScript/Closure that while it can help, optional type support means that a lot of problems fall through the cracks.
Our Go version defines the exact types for our arguments and our result. You cannot pass an integer and a float or an integer and a string. You will only ever receive an integer as a return. This allows our compiler to find any errors with variable types when the program is compiled. In Python, this error could show up at any time, from the instant it ran to 6 months later when a certain code path was executed.
Note
A few years ago, there was a study done on the Rosetta Code repository for some of the top languages in use to see how they fared in processing time, memory use, and runtime failures. For runtime failures, Go had the least failures, with Python towards the bottom of the ranking. Static typing would have certainly played into that.
The study can be found here: https://arxiv.org/pdf/1409.0252.pdf.
Go's types
Go has a rich type system that not only specifies that a type might be an integer but also the size of the integer. This allows a Go programmer to reduce the size of a variable both in memory and when encoding for network transport.
The following table shows the most common types used in Go:

Table 1.1 – Common types used in Go and their descriptions
We will be keeping our discussion mostly to the preceding types; however, the following table is the full list of types that can be used:

Table 1.2 – Full list of types that you can use in Go
Go doesn't just provide these types; you can also create new types based on these basic types. These custom types become their own type and can have methods attached to them.
Declaring a custom type is done with the type
keyword and will be discussed during the section on the struct
type. For now, we are going to move on to the basics of declaring variables.
Now that we've talked about our variable types, let's have a look at how we can create them.
Declaring variables
As in most languages, declaring a variable allocates storage that will hold some type of data. In Go, that data is typed so that only that type can be stored in the allocated storage. As Go has multiple ways to declare a variable, the next parts will talk about the different ways this can be done.
The long way to declare a variable
The most specific way to declare a variable is using the var
keyword. You can use var
to declare a variable both at the package level (meaning not inside a function) and within a function. Let's look at some examples of ways to declare variables using var
:
var i int64
This declares an i
variable that can hold an int64
type. No value is assigned, so the value is assigned the zero value of an integer, which is 0
:
var i int = 3
This declares an i
variable that can hold an int
type. The value 3
is assigned to i
.
Note that the int
and int64
types are distinct. You cannot use an int
type as an int64
type, and vice versa. However, you can do type conversions to allow interchanging these types. This is discussed later:
var ( i int word = "hello" )
Using ()
, we group together a set of declarations. i
can hold an int
type and has the integer zero value, 0
. word
doesn't declare the type, but it is inferred by the string value on the right side of the equal (=
) operator.
The shorter way
In the previous example, we used the var
keyword to create a variable and the =
operator to assign values. If we do not have an =
operator, the compiler assigns the zero value for the type (more on this later).
The important concept is as follows:
var
created the variable but did not make an assignment.=
assigned a value to the variable.
Within a function (not at the package level), we can do a create and assign by using the :=
operator. This both creates a new variable and assigns a value to it:
i := 1 // i is the int type word := "hello" // word is the string type f := 3.2 // f is the float64 type
The important thing to remember when using :=
is that it means create and assign. If the variable already exists, you cannot use :=
, but must use =
, which just does an assignment.
Variable scopes and shadowing
A scope is the part of the program in which a variable can be seen. In Go, we have the following variable scopes:
- Package scoped: Can be seen by the entire package and is declared outside a function
- Function scoped: Can be seen within
{}
which defines the function - Statement scoped: Can be seen within
{}
of a statement in a function (for
loop,if
/else
)
In the following program, the word
variable is declared at the package level. It can be used by any function defined in the package:
package main import "fmt" var word = "hello" func main() { fmt.Println(word) }
In the following program, the word
variable is defined inside the main()
function and can only be used inside {}
which defines main
. Outside, it is undefined:
package main import "fmt" func main() { var word string = "hello" fmt.Println(word) }
Finally, in this program, i
is statement scoped. It can be used on the line starting our for
loop and inside {}
of the loop, but it doesn't exist outside the loop:
package main import "fmt" func main() { for i := 0; i < 10; i++ { fmt.Println(i) } }
The best way to think of this is that if your variable is declared on a line that has {
or within a set of {}
, it can only be seen within those {}
.
Cannot redeclare a variable in the same scope
The rule for this, You cannot declare two variables with the same name within the same scope.
This means that no two variables within the same scope can have the same name:
func main() { var word = "hello" var word = "world" fmt.Println(word) }
This program is invalid and will generate a compile error. Once you have declared the word
variable, you cannot recreate it within the same scope. You can change the value to a new value, but you cannot create a second variable with the same name.
To assign word
a new value, simply remove var
from the line. var
says create variable where we want to only do an assignment:
func main() { var word = "hello" word = "world" fmt.Println(word) }
Next, we will look at what happens when you declare two variables with the same name in the same scope, but within separate code blocks.
Variable shadowing
Variable shadowing occurs when a variable that is within your variable scope, but not in your local scope, is redeclared. This causes the local scope to lose access to the outer scoped variable:
package main import "fmt" var word = "hello" func main() { var word = "world" fmt.Println("inside main(): ", word) printOutter() } func printOutter() { fmt.Println("the package level 'word' var: ", word) }
As you can see, word
is declared at the package level. But inside main
, we define a new word
variable, which overshadows the package level variable. When we refer to word
now, we are using the one defined inside main()
.
printOutter()
is called, but it doesn't have a locally shadowed word
variable (one declared between its {}
), so it used the one at the package level.
Here's the output of this program:
inside main(): world the package level 'word' var: hello
This is one of the more common bugs for Go developers.
Zero values
In some older languages, a variable declaration without an assignment has an unknown value. This is because the program creates a place in memory to store the value but doesn't put anything in it. So, the bits representing the value are set to whatever happened to be in that memory space before you created the variable.
This has led to many unfortunate bugs. So, in Go, declaring a variable without an assignment automatically assigns a value called the zero value. Here is a list of the zero values for Go types:

Table 1.3 – Zero values for Go types
Now that we understand what zero values are, let's see how Go prevents unused variables in our code.
Function/statement variable must be used
The rule here is that if you create a variable within a function or statement, it must be used. This is much for the same reason as package imports; declaring a variable that isn't used is almost always a mistake.
This can be relaxed in much the same way as an import, using _
, but is far less common. This assigns the value stored in someVar
to nothing:
_ = someVar
This assigns the value returned by someFunc()
to nothing:
_ = someFunc()
The most common use for this is when a function returns multiple values, but you only need one:
needed, _ := someFunc()
Here, we create and assign to the needed
variable, but the second value isn't something we use, so we drop it.
This section has provided the knowledge of Go's basic types, the different ways to declare a variable, the rules around variable scopes and shadows, and Go's zero values.
Looping in Go
Most languages have a few different types of loop statements: for
, while
, and do while
.
Go differs in that there is a single loop type, for
, that can implement the functionality of all the loop types in other languages.
In this section, we will discuss the for
loop and its many uses.
C style
The most basic form of a loop is similar to C syntax:
for i := 0; i < 10; i++ { fmt.Println(i) }
This declares an i
variable that is an integer scoped to live only for this loop statement. i := 0;
is the loop initialization statement; it only happens once before the loop starts. i < 10;
is the conditional statement; it happens at the start of each loop and must evaluate to true
or the loop ends.
i++
is the post
statement; it occurs at the end of every loop. i++
says to increment the i
variable by 1
. Go also has common statements, such as i += 1
and i--
.
Removing the init statement
We don't need to have an init
statement, as shown in this example:
var i int for ;i < 10;i++ { fmt.Println(i) } fmt.Println("i's final value: ", i)
In this, we declared i
outside the loop. This means that i
will be accessible outside the loop once the loop is finished, unlike our previous example.
Remove the post statement too and you have a while loop
Many languages have a while
loop that simply evaluates whether a statement is true or not. We can do the same by eliminating our init
and post
statements:
var i int for i < 10 { i++ } b := true for b { // This will loop forever fmt.Println("hello") }
You might be asking, how do we make a loop that runs forever? The for
loop has you covered.
Creating an infinite loop
Sometimes you want a loop to run forever or until some internal condition inside the loop occurs. Creating an infinite loop is as simple as removing all statements:
for { fmt.Println("Hello World") }
This is usually useful for things such as servers that need to process some incoming stream forever.
Loop control
With loops, you occasionally need to control the execution of the loop from within the loop. This could be because you want to exit the loop or stop the execution of this iteration of the loop and start from the top.
Here's an example of a loop where we call a function called doSomething()
that returns an error if the loop should end. What doSomething()
does is not important for this example:
for { if err := doSomething(); err != nil { break } fmt.Println("keep going") }
The break
function here will break out of the loop. break
is also used to break out of other statements, such as select
or switch
, so it's important to know that break
breaks out of the first statement it is nested inside of.
If we want to stop the loop on a condition and continue with the next loop, we can use the continue
statement:
for i := 0; i < 10; i++ { if i % 2 == 0 { // Only 0 for even numbers continue } fmt.Println("Odd number: ", i) }
This loop will print out the odd numbers from zero to nine. i % 2
means i modulus 2. Modulus divides the first number by the second number and returns the remainder.
Loop braces
Here is the introduction of this rule: A for
loop’s open brace must be on the same line as the for
keyword.
With many languages, there are arguments about where to put the braces for loops/conditionals. With Go, the authors decided to pre-empt those arguments with compiler checks. In Go, you can do the following:
for { fmt.Println("hello world") }
However, the following is incorrect as the opening brace of the for
loop is on its own line:
for { fmt.Println("hello world") }
In this section we learned to use for
loops as C style loops, as while
loops.
Using conditionals
Go supports two types of conditionals, as follows:
if
/else
blocksswitch
blocks
The standard if
statement is similar to other languages with the addition of an optional init
statement borrowed from the standard C-style for
loop syntax.
switch
statements provide a sometimes-cleaner alternative to if
. So, let's jump into the if
conditional.
if statements
if
statements start with a familiar format that is recognizable in most languages:
if [expression that evaluates to boolean] { ... }
Here's a simple example:
if x > 2 { fmt.Println("x is greater than 2") }
The statements within {}
in if
will execute if x
has a value greater than 2
.
Unlike most languages, Go has the ability to execute a statement within the if
scope before the evaluation is made:
if [init statement];[statement that evaluates to boolean] { ... }
Here is a simple example that is similar to the init statement in a for
loop:
if err := someFunction(); err != nil { fmt.Println(err) }
Here, we initialize a variable called err
. It has a scope of the if
block. If the err
variable does not equal the nil
value (a special value that indicates certain types are not set – more on this later), it will print the error.
else
If you need to execute something when the condition of an if
statement is not met, you can use the else
keyword:
if condition { function1() }else { function2() }
In this example, if the if
condition is true, function1
will be executed. Otherwise, function2
occurs.
It should be noted that most uses of else
can generally be eliminated for cleaner code. If your if
condition results in returning from a function using the return
keyword, you can eliminate else
.
An example is as follows:
if v, err := someFunc(); err != nil { return err }else{ fmt.Println(v) return nil }
This can be simplified to the following:
v, err := someFunc() if err != nil { return err } fmt.Println(v) return nil
Sometimes, you want to only execute code if the if
condition is not met and another condition is. Let's look at that next.
else if
An if
block can also contain else if
, providing multiple levels of execution. The first if
or else if
that is matched in order is executed.
Note that often Go developers choose the switch
statement as a cleaner version of this type of conditional.
An example is as follows:
if x > 0 { fmt.Println("x is greater than 0") } else if x < 0 { fmt.Println("x is less than 0") } else{ fmt.Println("x is equal to 0") }
Now that we have seen the basics of this conditional, we need to talk about brace style.
if/else braces
It's time to introduce this rule: Opening braces for if
/else
must be on the line with the associated keyword. If there is another statement in the chain, it must start on the same line as the previous close brace.
With many languages, there are arguments about where to put the braces for loops/conditionals.
With Go, the authors decided to pre-empt those arguments with compiler checks. In Go, you can't do the following:
if x > 0 { // This must go up on the previous line fmt.Println("hello") } else { // This line must start on the previous line fmt.Println("world") }
So, with the arguments on bracing style in Go settled, let's look at an alternative to if
/else
, the switch
statement.
The switch statement
switch
statements are more elegant if
/else
blocks that are very flexible in their use. They can be used for doing exact matching and multiple true/false evaluations.
Exact match switch
The following is an exact match switch
:
switch [value] { case [match]: [statement] case [match], [match]: [statement] default: [statement] }
[value]
is matched against each case
statement. If it matches, the case
statement executes. Unlike some languages, once a match occurs, no other case is considered. If no match occurs, the default
statement executes. The default
statement is optional.
This has a nicer syntax than if
/else
for handling cases where your value can be several values:
switch x { case 3: fmt.Println("x is 3") case 4, 5: // executes if x is 4 or 5 fmt.Println("x is 4 or 5") default: fmt.Println("x is unknown") }
switch can also have an init
statement, similar to if
:
switch x := someFunc(); x { case 3: fmt.Println("x is 3") }
True/false evaluation switch
We can also eliminate [match]
so that each case
statement isn't an exact match, but a true/false evaluation (as with if
statements):
switch { case x > 0: fmt.Println("x is greater than 0") case x < 0: fmt.Println("x is less than 0") default: fmt.Println("x must be 0") }
At the end of this section, you should be able to use Go's conditional statements to branch code execution in your program based on some criteria and handle cases where no statement was matched. As conditionals are one of the standard building blocks of software, we will use these in many of the remaining sections.
Learning about functions
Functions in Go are what you'd expect from a modern programming language. There are only a few things that make Go functions different:
- Multiple return values are supported
- Variadic arguments
- Named return values
The basic function signature is as follows:
func functionName([varName] [varType], ...) ([return value], [return value], ...){ }
Let's make a basic function that adds two numbers together and returns the result:
func add(x int, y int) int { return x + y }
As you can see, this takes in two integers, x
and y
, adds them together, and returns the result (which is an integer). Let's show how we can call this function and print its output:
result := add(2, 2) fmt.Println(result)
We can simplify this function signature by declaring both x
and y
types with a single int
keyword:
func add(x, y int) int { return x + y }
This is equivalent to the previous one.
Returning multiple values and named results
In Go, we can return multiple values. For example, consider a function that divides two integers and returns two variables, the result and the remainder, as follows:
func divide(num, div int) (res, rem int) { result = num / div remainder = num % div return res, rem }
This code demonstrates a few new features in our function:
- Argument
num
is the number to be divided - Argument
div
is the number to divide by - Return value
res
is the result of the division - Return value
rem
is the remainder of the division
First is named returns (res
and rem
). These variables are automatically created and ready for use inside the function.
Notice I use =
and not :=
when doing assignments to those variables. This is because the variable already exists, and we want to assign a value (=
). :=
means create and assign. You can only create a new variable that doesn't exist. You will also notice that now the return type is in parenthesis. You will need to use parenthesis if you use more than one return value or named returns (or in this case, both).
Calling this function is just as simple as calling add()
before, as shown here:
result, remainder := divide(3, 2) fmt.Printf("Result: %d, Remainder %d", result, remainder)
Strickly speaking, you don't have to use return
to return the values. However, doing so will prevent some ugly bugs that you will eventually encounter.
Next, we will look at how we can have a variable number of arguments as function input that allows us to create functions such as fmt.Println()
, which you have been using in this chapter.
Variadic arguments
A variadic argument is when you want to provide 0
to infinite arguments. A good example would be calculating a sum of integers. Without variadic arguments, you might use a slice (a growable array type, which we will talk about later), as follows:
func sum(numbers []int) int { sum := 0 for _, n := range numbers { sum += n } return sum }
While this is fine, using it is cumbersome:
args := []int{1,2,3,4,5} fmt.Println(sum(args))
We can accomplish this same thing by using the variadic (...
) notation:
func sum(numbers ...int) int { // Same code }
numbers
is still []int
, but has a different calling convention that is more elegant:
fmt.Println(sum(1,2,3,4,5))
Note
You can use variadic arguments with other arguments, but it must be the last argument in the function.
Anonymous functions
Go has a concept of anonymous functions, which means a function without a name (also called a function closure).
This can be useful to take advantage of special statements that honor function boundaries, such as defer
, or in goroutines
. We will show how to take advantage of these for goroutines
later, but for now let's show how to execute an anonymous function. This is a contrived example that is only useful in teaching the concept:
func main() { result := func(word1, word2 string) string { return word1 + " " + word2 }("hello", "world") fmt.Println(result) }
This code does the following:
- Defines a single-use function (
func(word1, word2 string) string
) - Executes the function with the
hello
andworld
arguments - Assigns the
string
return value to theresult
variable - Prints
result
Now that we have arrived at the end of this section, we have learned about how Go functions are declared, the use of multiple return values, variadic arguments for simplified function calling, and anonymous functions. Multiple return values will be important in future chapters where we deal with errors, and anonymous functions are key components of our future defer
statements and for use with concurrency.
In the next section, we will explore public and private types.
Defining public and private
Many modern languages provide a set of options when declaring constants/variables/functions/methods that detail when a method can be called.
Go simplifies these visibility choices down to two types:
- Public (exported)
- Private (not exported)
Public types are types that can be referred to outside of the package. Private types can only be referred to inside the package. To be public, the constant/variable/function/method must simply start with an uppercase letter. If it starts with a lowercase letter, it is private.
There is a third type of visibility that we don't cover here: internally exported. This occurs when a type is public but in a package, located within a directory called internal/
. Those packages can only be used by packages within a parent directory. You can read about this here: https://golang.org/doc/go1.4#internalpackages.
Let's declare a package and create some public and private methods:
package say import "fmt" func PrintHello() { fmt.Println("Hello") } func printWorld() { fmt.Println("World") } func PrintHelloWorld() { PrintHello() printWorld() }
We have three function calls, two public (PrintHello()
and PrintHelloWorld()
) and one private (printWorld()
). Now, let's create package main
, import the say
package, and call our functions:
package main import "github.com/repo/examples/say" func main() { say.PrintHello() say.PrintHelloWorld() }
Now, let's compile and run it:
$ go run main.go Hello Hello World
These work because PrintHello()
and PrintHelloWorld()
are both exported (public) functions. PrintHelloWorld()
calls the private printWorld()
, but that is legal because they are in the same package.
If we try to add say.printWorld()
to func main()
and run it, we will get the following:
./main.go:8:2: cannot refer to unexported name say.printWorld
Public and private apply to variables declared outside functions/methods and type declarations.
By the end of this short and sweet section, you've acquired the knowledge of Go's public and private types. This will be useful in code where you do not want to expose types in your public API. Next, we will look at arrays and slices.
Using arrays and slices
Languages require more than the basic types to hold data. The array
type is one of the core building blocks in lower-level languages, providing the base sequential data type. For most day-to-day use, Go's slice
type provides a flexible array that can grow as data needs grow and can be sliced into sections in order to share views of the data.
In this section, we will talk about arrays as the building blocks of slices, the difference between the two, and how to utilize them in your code.
Arrays
The base sequential type in Go is the array (important to know, but rarely used). Arrays are statically sized (if you create one that holds 10 int
types, it will always hold exactly 10 int
types).
Go provides an array
type designated by putting [size]
before the type you wish to create an array of. For example, var x [5]int or x := [5]int{}
creates an array holding five integers, indexed from 0
to 4
.
An assignment into an array is as easy as choosing the index. x[0] = 3
assigns 3
to index 0
. Retrieving that value is as simple as referring to the index; fmt.Println(x[0] + 2)
will output 5
.
Arrays, unlike slices, are not pointer wrapper types. Passing an array as a function argument passes a copy:
func changeValueAtZeroIndex(array [2]int) { array[0] = 3 fmt.Println("inside: ", array[0]) // Will print 3 } func main() { x := [2]int{} changeValueAtZeroIndex(x) fmt.Println(x) // Will print 0 }
Arrays present the following two problems in Go:
- Arrays are typed by size –
[2]int
is distinct from[3]int
. You cannot use[3]int
where[2]int
is required. - Arrays are a set size. If you need more room, you must make a new array.
While it is important to know what arrays are, the most common sequential type used in Go is the slice.
Slices
The easiest way to understand a slice is to see it as a type that is built on top of arrays. A slice is a view into an array. Changing what you can see in your slice's view changes the underlying array's value. The most basic use of slices acts like arrays, with two exceptions:
- A slice is not statically sized.
- A slice can grow to accommodate new values.
A slice tracks its array, and when it needs more room, it will create a new array that can accommodate the new values and copies the values from the current array into the new array. This happens invisibly to the user.
Creating a slice can be done similarly to an array, var x = []int
or x := []int{}
. This creates a slice of integers with a length of 0
(which has no room to store values). You can retrieve the size of the slice using len(x)
.
We can create a slice with initial values easily: x := []int{8,4,5,6}
. Now, we have len(x) == 4
, indexed from 0
to 3
.
Similar to arrays, we can change a value at an index by simply referencing the index. x[2] = 12
will change the preceding slice to []int{8,4,12,6}
.
Unlike arrays, we can add a new value to the slice using the append
command. x = append(x, 2)
will cause the underlying x
array references to be copied to a new array and assigns the new view of the array back to x
. The new value is []int{8,4,12,6,2}
. You may append multiple values by just putting more comma-delimited values in append
(that is, x = append(x, 2, 3, 4, 5)
).
Remember that slices are simply views into a trackable array. We can create new limited views of the array. y := x[1:3]
creates a view (y
) of the array, yielding []int{4, 12}
(1
is inclusive and 3
is exclusive in [1:3]
). Changing the value at y[0]
will change x[1]
. Appending a single value to y
via y = append(y, 10)
will change x[3]
, yielding []int{8,4,12,10,2}
.
This kind of use isn't common (and is confusing), but the important part is to understand that slices are simply views into an array.
While slices are a pointer-wrapped type (values in a slice passed to a function that are changed will change in the caller as well), a slice's view will not change.
func doAppend(sl []int) { sl = append(sl, 100) fmt.Println("inside: ", sl) // inside: [1 2 3 100] } func main() { x := []int{1, 2, 3} doAppend(x) fmt.Println("outside: ", x) // outside: [1 2 3] }
In this example, the sl
and x
variables both use the same underlying array (which has changed in both), but the view for x
does not get updated in doAppend()
. To update x
to see the addition to the slice would require passing a pointer to the slice (pointers are covered in a future chapter) or returning the new slice as seen here:
func doAppend(sl []int) []int { return append(sl, 100) } func main() { x := []int{1, 2, 3} x = doAppend(x) fmt.Println("outside: ", x) // outside: [1 2 3 100] }
Now that you see how to create and add to a slice, let's look at how to extract the values.
Extracting all values
To extract values from a slice, we can use the older C-type for
loop or the more common for
...range
syntax.
The older C style is as follows:
for i := 0; i < len(someSlice); i++{ fmt.Printf("slice entry %d: %s\n", i, someSlice[i]) }
The more common approach in Go uses range
:
for index, val := range someSlice { fmt.Printf("slice entry %d: %s\n", index, val) }
With range
, we often want to use only the value, but not the index. In Go, you must use variables that are declared in a function, or the compiler will complain with the following:
index declared but not used
To only extract the values, we can use _,
(which tells the compiler not to store the output), as follows:
for _, val := range someSlice { fmt.Printf("slice entry: %s\n", val) }
On very rare occasions, you may want to only print out indexes and not values. This is uncommon because it will simply count from zero to the number of items. However, this can be achieved by simply removing val
from the for
statement: for index := range someSlice
.
In this section, you have discovered what arrays are, how to create them, and how they relate to slices. In addition, you've acquired the skills to create slices, add data to slices, and extract data from slices. Let's move on to learning about maps next.
Understanding maps
Maps are a collection of key-value pairs that a user can use to store some data and retrieve it with a key. In some languages, these are called dictionaries (Python) or hashes (Perl). In contrast to an array/slice, finding an entry in a map requires a single lookup versus iterating over the entire slice comparing values. With a large set of items, this can give you significant time savings.
Declaring a map
There are several ways to declare a map. Let's first look at using make
:
var counters = make(map[string]int, 10)
The example just shared creates a map with string
keys and stores data that is an int
type. 10
signifies that we want to pre-size for 10 entries. The map can grow beyond 10 entries and the 10
can be omitted.
Another way of declaring a map is by using a composite literal:
modelToMake := map[string]string{ "prius": "toyota", "chevelle": "chevy", }
This creates a map with string
keys and stores the string
data. We also pre-populate the entry with two key-value entries. You can omit the entries to have an empty map.
Accessing values
You can retrieve a value as follows:
carMake := modelToMake["chevelle"] fmt.Println(carMake) // Prints "chevy"
This assigns the chevy
value to carMake
.
But what happens if the key isn't in the map? In that case, we will receive the zero value of the data type:
carMake := modelToMake["outback"] fmt.Println(carMake)
The preceding code will print an empty string, which is the zero value of the string type that is used as values in our map.
We can also detect if the value is in the map:
if carMake, ok := modelToMake["outback"]; ok { fmt.Printf("car model \"outback\" has make %q", carMake) }else{ fmt.Printf("car model \"outback\" has an unknown make") }
Here we assign two values. The first (carMake
) is the data stored in the key (or zero value if not set), and the second (ok
) is a Boolean that indicates if the key was found.
Adding new values
Adding a new key-value pair or updating a key's value, is done the same way:
modelToMake["outback"] = "subaru" counters["pageHits"] = 10
Now that we can change a key-value pair, let's look at extracting values from a map.
Extracting all values
To extract values from a map, we can use the for
...range
syntax that we used for slices. There are a few key differences with maps:
- Instead of an index, you will get the map's key.
- Maps have a non-deterministic order.
Non-deterministic order means that iterating over the data will return the same data but not in the same order.
Let's print out all the values in our carMake
map:
for key, val := range modelToMake { fmt.Printf("car model %q has make %q\n", key, val) }
This will yield the following, but maybe not in the same order:
car model "prius" has make "toyota" car model "chevelle" has make "chevy" car model "outback" has make "subaru"
Note
Similar to a slice, if you don't need the key, you may use _
instead. If you simply want the keys, you can omit the value val
variable, such as for key := range modelToMake
.
In this section, you have learned about the map
type, how to declare them, add values to them, and finally how to extract values from them. Let's dive into learning about pointers.
Understanding Go pointers
Pointers are another essential tool for programming languages for efficient memory use. Some readers may have not encountered pointers in their current language, instead having used its cousin, the reference type. In Python, for example, the dict
, list
, and object
types are reference types.
In this section, we will cover what pointers are, how to declare them, and how to use them.
Memory addresses
In an earlier chapter, we talked about variables for storing data of some type. For example, if we want to create a variable called x
that stores an int
type with a value of 23
, we can write var x int = 23
.
Under the hood, the memory allocator allocates us space to store the value. The space is referenced by a unique memory address that looks like 0xc000122020
. This is similar to how a home address is used; it is the reference to where the data lives.
We can see the memory address where a variable is stored by prepending &
to a variable name:
fmt.Println(&x)
This would print 0xc000122020
, the memory address of where x
is stored.
This leads to an important concept: functions always make a copy of the arguments passed.
Function arguments are copies
When we call a function and pass a variable as a function argument, inside the function you get a copy of that variable. This is important because when you change the variable, you are only affecting the copy inside the function.
func changeValue(word string) { word += "world" }
In this code, word
is a copy of the value that was passed. word
will stop existing at the end of this function call.
func main() { say := "hello" changeValue(say) fmt.Println(say) }
This prints "hello"
. Passing the string and changing it in the function doesn't work, because inside the function we are working with a copy. Think of every function call as making a copy of the variable with a copy machine. Editing the copy that came out of the copy machine does not affect the original.
Pointers to the rescue
Pointers in Go are types that store the address of a value, not the value. So, instead of storing 23
, it would store 0xc000122020
, which is where in memory 23
is stored.
A pointer type can be declared by prepending the type name with *
. If we want to create an intPtr
variable that stores a pointer to int
, we can do the following:
var intPtr *int
You cannot store int
in intPtr
; you can only store the address of int
. To get the address of an existing int
, you can use the &
symbol on a variable representing int
.
Let's assign intPtr
the address of our x
variable from previously:
intPtr = &x intPtr now stores 0xc000122020.
Now for the big question, how is this useful? This lets us refer to a value in memory and change that value. We do that through what is called dereferencing the pointer. This is done with the *
operator on the variable.
We can view or change the value held at x
by dereferencing the pointer. The following is an example:
fmt.Println(x) // Will print 23 fmt.Println(*intPtr) // Will print 23, the value at x *intPtr = 80 // Changes the value at x to 80 fmt.Println(x) // Will print 80
This also works across functions. Let's alter changeValue()
to work with pointers:
func changeValue(word *string) { // Add "world" to the string pointed to by 'word' *word += "world" } func main() { say := "hello" changeValue(&say) // Pass a pointer fmt.Println(say) // Prints "helloworld" }
Note that operators such as *
are called overloaded operators. Their meaning depends on the context in which they are used. When declaring a variable, *
indicates a pointer type, var intPtr *int
. When used on a variable, *
means dereference, fmt.Println(*intPtr)
. When used between two numbers, it means multiply, y := 10 * 2
. It takes time to remember what a symbol means when used in certain contexts.
But, didn't you say every argument is a copy?!
I did indeed. When you pass a pointer to a function, a copy of the pointer is made, but the copy still holds the same memory address. Therefore, it still refers to the same piece of memory. It is a lot like making a copy of a treasure map on the copy machine; the copy still points to the place in the world where you will find the treasure. Some of you are probably thinking, But maps and slices can have their values changed, what gives?
They are a special type called a pointer-wrapped type. A pointer-wrapped type hides internal pointers.
Don't go crazy with pointers
While in our examples we used pointers for basic types, typically pointers are used on long-lived objects or for storage of large data that is expensive to copy. Go's memory model uses the stack/heap model. Stack memory is created for exclusive use by a function/method call. Allocation on the stack is significantly faster than on the heap.
Heap allocation occurs in Go when a reference or pointer cannot be determined to live exclusively within a function's call stack. This is determined by the compiler doing escape analysis.
Generally, it is much cheaper to pass copies into a function via an argument and another copy in the return value than it is to use a pointer. Finally, be careful with the number of pointers. Unlike C, it is uncommon in Go to see pointers to pointers, such as **someType
, and, in over 10 years of coding Go, I have only once seen a single use for ***someType
that was valid. Unlike in the movie Inception, there is no reason to go deeper.
To sum up this section, you have gained an understanding of pointers, how to declare them, how to use them in your code, and where you should probably use them. You will use them on long-lived objects or types holding large amounts of data where copies are expensive. Next, let's explore structs.
Getting to know about structs
Structs represent a collection of variables. In the real world, we work with data all the time that would be well represented by a struct. For example, any form that is filled out in a job application or a vaccine card is a collection of variables (for example, last name, first name, and government ID number) that each has types (for example, string
, int
, and float64
) and are grouped together. That grouping would be a struct in Go.
Declaring a struct
There are two methods for declaring a struct. The first way is uncommon except in tests, as it doesn't allow us to reuse the struct's definition to create more variables. But, as we will see it later in tests, we will cover it here:
var record = struct{ Name string Age int }{ Name: "John Doak", Age: 100, // Yeah, not publishing the real one }
Here, we created a struct that contains two fields:
Name
(string
)Age
(int
)
We then created an instance of that struct that has those values set. To access those fields, we can use the dot .
operator:
fmt.Printf("%s is %d years old\n", record.Name, record.Age)
This prints "John Doak is 100 years old"
.
Declaring single-use structs, as we have here, is rarely done. Structs become more useful when they are used to create custom types in Go that are reusable. Let's have a look at how we can do that next.
Declaring a custom type
So far, we have created a single-use struct, which generally is not useful. Before we talk about the more common way to do this, let's talk about creating custom types.
Up until this point, we've seen the basic and pointer-wrapped types that are defined by the language: string
, bool
, map
, and slice
, for example. We can create our own types based on these basic types using the type
keyword. Let's create a new type called CarModel
that is based on the string
type:
type CarModel string
CarModel
is now its own type, just like string
. While CarModel
is based on a string
type, it is a distinct type. You cannot use CarModel
in place of a string or vice versa.
Creating a variable of CarModel
can be done similar to a string
type:
var myCar CarModel = "Chevelle"
Or, by using type conversion, as shown here:
myCar = CarModel("Chevelle")
Because CarModel
is based on string
, we can convert CarModel
back to string
with type conversion:
myCarAsString := string(myCar)
We can create new types based on any other type, including maps, slices, and functions. This can be useful for naming purposes or adding custom methods to a type (we will talk about this in a moment).
Custom struct types
The most common way to declare a struct is using the type
keyword. Let's create that record again, but this time let's make it reusable by declaring a type:
type Record struct{ Name string Age int } func main() { david := Record{Name: "David Justice", Age: 28} sarah := Record{Name: "Sarah Murphy", Age: 28} fmt.Printf("%+v\n", david) fmt.Printf("%+v\n", sarah) }
By using type
, we have made a new type called Record
that we can use again and again to create variables holding Name
and Age
.
Note
Similar to how you may define two variables with the same type on a single line, you may do the same within a struct
type, such as First, Last string
.
Adding methods to a type
A method is similar to a function, but instead of being independent, it is bound to a type. For example, we have been using the fmt.Println()
function. That function is independent of any variable that has been declared.
A method is a function that is attached to a variable. It can only be used on a variable of a type. Let's create a method that returns a string representation of the Record
type we created earlier:
type Record struct{ Name string Age int } // String returns a csv representing our record. func (r Record) String() string { return fmt.Sprintf("%s,%d", r.Name, r.Age) }
Notice func (r Record)
, which attaches the function as a method onto the Record
struct. You can access the fields of Record
within this method by using r.<field>
, such as r.Name
or r.Age
.
This method cannot be used outside of a Record
object. Here's an example of using it:
john := Record{Name: "John Doak", Age: 100} fmt.Println(john.String())
Let's look at how we change a field's value.
Changing a field's value
Struct values can be changed by using the variable attribute followed by =
and the new value. Here is an example:
myRecord.Name = "Peter Griffin" fmt.Println(myRecord.Name) // Prints: Peter Griffin
It is important to remember that a struct is not a reference type. If you pass a variable representing a struct to a function and change a field in the function, it will not change on the outside. Here is an example:
func changeName(r Record) { r.Name = "Peter" fmt.Println("inside changeName: ", r.Name) } func main() { rec := Record{Name: "John"} changeName(rec) fmt.Println("main: ", rec.Name) }
This will output the following:
Inside changeName: Peter Main: John
As we learned in the section on pointers, this is because the variable is copied, and we are changing the copy. For struct types that need to have fields that change, we normally pass in a pointer. Let's try this again, using pointers:
func changeName(r *Record) { r.Name = "Peter" fmt.Println("inside changeName: ", r.Name) } func main() { // Create a pointer to a Record rec := &Record{Name: "John"} changeName(rec) fmt.Println("main: ", rec.Name) } Inside changeName: Peter Main: Peter
This will output the following:
Inside changeName: Peter Main: Peter
Note that .
is a magic operator that works on struct
or *struct
.
When I declared the rec
variable, I did not set the age
. Non-set fields are set to the zero value of the type. In the case of Age
, which is int
, this would be 0
.
Changing a field's value in a method
In the same way that a function cannot alter a non-pointer struct, neither can a method. If we had a method called IncrAge()
that increased the age on the record by one, this would not do what you wanted:
func (r Record) IncrAge() { r.Age++ }
The preceding code passes a copy of Record
, adds one to the copy's Age
, and returns.
To actually increment the age, simple make Record
a pointer, as follows:
func (r *Record) IncrAge() { r.Age++ }
This will work as expected.
Tip
Here is a basic rule that will keep you out of trouble, especially when you are new to the language. If the struct
type should be a pointer, then make all methods pointer methods. If it shouldn't be, then make them all non-pointers. Don't mix and match.
Constructors
In many languages, constructors are specially-declared methods or syntax that are used to initialize fields in an object and sometimes run internal methods as setup. Go doesn't provide any specialized code for that, instead, we use a constructor pattern using simple functions.
Constructors are commonly either called New()
or New[Type]()
when declaring a public constructor. Use New()
if there are no other types in the package (and most likely won't be in the future).
If we wanted to create a constructor that made our Record
from the previous section, it might look like the following:
func NewRecord(name string, age int) (*Record, error) { if name == "" { return nil, fmt.Errorf("name cannot be the empty string") } if age <= 0 { return nil, fmt.Errorf("age cannot be <= 0") } return &Record{Name: name, Age: age}, nil }
This constructor takes in a name
and age
argument and returns a pointer to Record
with those fields set. If we pass bad values for those fields, it instead returns the pointer's zero value (nil
) and an error. Using this looks like the following:
rec, err := NewRecord("John Doak", 100) if err != nil { return err }
Don't worry about the error, as we will discuss it in the course of the book's journey.
By now, you have learned how to use struct
, Go's base object type. This included creating a struct, creating custom structs, adding methods, changing field values, and creating constructor functions. Now, let's look at using Go interfaces to abstract types.
Comprehending Go interfaces
Go provides a type called an interface that stores any value that declares a set of methods. The implementing value must have declared this set of methods to implement the interface. The value may also have other methods besides the set declared in the interface type.
If you are new to interfaces, understand that they can be a little confusing. Therefore, we will take it one step at a time.
Defining an interface type
Interfaces are most commonly defined using the type
keyword that we discussed in the earlier section on structs. The following defines an interface that returns a string representing the data:
type Stringer interface { String() string }
Note
Stringer
is a real type defined in the standard library's fmt
package. Types that implement Stringer
will have their String()
method called when passed to print
functions in the fmt
package. Don't let the similar names confuse you; Stringer
is the interface type's name, and it defines a method called String()
(which is uppercase to distinguish it from the string
type, which is lowercase). That method returns a string
type that should provide some human-readable representation of your data.
Now, we have a new type called Stringer
. Any variable that has the String()
string
method can be stored in a variable of type Stringer
. The following is an example:
type Person struct { First, Last string } func (p Person) String() string { return fmt.Sprintf("%s,%s", p.Last, p.First) }
Person
represents a record of a person, first and last name. We define String() string
on it, so Person
implements Stringer
:
type StrList []string func (s StrList) String() string { return strings.Join(s, ",") }
StrList
is a slice of strings. It also implements Stringer
. The strings.Join()
function used here takes a slice of strings and creates a single string with each entry from the slice separated by a comma:
// PrintStringer prints the value of a Stringer to stdout. func PrintStringer(s Stringer) { fmt.Println(s.String()) }
PrintStringer()
allows us to print the output of Stringer.String()
of any type that implements Stringer
. Both the types we created above implement Stringer
.
Let's see this in action:
func main() { john := Person{First: "John", Last: "Doak"} var nameList Stringer = StrList{"David", "Sarah"} PrintStringer(john) // Prints: Doak,John PrintStringer(nameList) // Prints: David,Sarah }
Without interfaces, we would have to write a separate Print[Type]
function for every type we wanted to print. Interfaces allow us to pass values that can do common operations defined by their methods.
Important things about interfaces
The first thing to note about interfaces is that values must implement every method defined in the interface. Your value can have methods not defined for the interface, but it doesn't work the other way.
Another common issue new Go developers encounter is that once the type is stored in an interface, you cannot access its fields, or any methods not defined on the interface.
The blank interface – Go's universal value
Let's define a blank interface variable: var i interface{}. i
is an interface with no defined methods. So, what can you store in that?
That's right, you can store anything.
interface{}
is Go's universal value container that can be used to pass any value to a function and then figure out what it is and what to do with it later. Let's put some things in i
:
i = 3 i = "hello world" i = 3.4 i = Person{First: "John"}
This is all legal because each of those values has types that define all the methods that the interface defined (which were no methods). This allows us to pass around values in a universal container. This is actually how fmt.Printf()
and fmt.Println()
work. Here are their definitions from the fmt
package:
func Println(a ...interface{}) (n int, err error) func Printf(format string, a ...interface{}) (n int, err error)
However, as the interface did not define any methods, i
is not useful in this form. So, this is great for passing around values, but not using them.
Note about interface{} in 1.18:
Go 1.18 has introduced an alias for the blank interface{}
, called any
. The Go standard library now uses any
in place of interface{}
. However, all packages prior to 1.18 will still use interface{}
. Both are equivalent and can be used interchangeably.
Type assertion
Interfaces can have their values asserted to either another interface type or to their original type. This is different than type conversion, where you change the type from one to another. In this case, we are saying it already is this type.
Type assertion allows us to change an interface{}
value into a value that we can do something with.
There are two common ways to do this. The first uses the if
syntax, as follows:
if v, ok := i.(string); ok { fmt.Println(v) }
i.(string)
is asserting that i
is a string
value. If it is not, ok == false
. If ok == true
, then v
will be the string
value.
The more common way is with a switch
statement and another use of the type
keyword:
switch v := i.(type) { case int: fmt.Printf("i was %d\n", i) case string: fmt.Printf("i was %s\n", i) case float: fmt.Printf("i was %v\n", i) case Person, *Person: fmt.Printf("i was %v\n", i) default: // %T will print i's underlying type out fmt.Printf("i was an unsupported type %T\n", i) }
Our default
statement prints out the underlying type of i
if it did not match any of the other cases. %T
is used to print the type information.
In this section, we learned about Go's interface
type, how it can be used to provide type abstraction, and converting an interface into its concrete type for use.
Summary
In this chapter, you have learned the basics of the Go language. This includes variable types, functions, loops, methods, pointers, and interfaces. The skills acquired in this chapter provide the basic foundation needed to explore more advanced features of the Go language in our next chapter.
Next, we will be looking at essential capabilities of the Go language, such as handling errors, using concurrency, and Go's testing framework.