Defining And Using Functions In Go: Creating Reusable Functions, Including Variadic Functions And Closures

Overview

This chapter explores the fundamentals of functions in Go, guiding you through creating reusable functions with multiple arguments and return values. You will learn how to define variadic functions that can handle a dynamic number of arguments, utilize closures to capture external variables, and understand methods for attaching functions to types. Additionally, the chapter delves into advanced concepts like deferred function calls, error handling, function types, and the use of init functions to perform pre-execution setup. Through practical examples, you’ll build the skills needed to create flexible, efficient Go functions.

Functions

Functions in Go resemble functions from most other programming languages.


package main

import "fmt"

func main() {
	sayHello()
}

func sayHello() {
	fmt.Println("Hello")
}

Arguments

Functions can take N number of arguments, including other functions.


package main

import "fmt"

func main() {
	sayHello("Hello")
}

func sayHello(greeting string) {
	fmt.Println(greeting)
}

Because Go is a typed language you must defined the types of the arguments in the function definition.

The type of the argument follows the name of the argument.

Arguments Of Same Type

Identical typed arguments can be declared once.


package main

import "fmt"

func main() {
	sayHello("Hello", "Mark")
	sayHello2("Hello", "Mark")
}

func sayHello(greeting, name string) {
	fmt.Printf("%s, %s", greeting, name)
}

func sayHello2(greeting string, name string) {
	fmt.Printf("%s, %s", greeting, name)
}

Both examples are functionally identical. Although for clarity the more explicit (longer) version is preferred.

Return Arguments

Functions can return 0 to N numbers of values, though it is considered best practice to not return more than two or three.

When returning an error it is considered best practice to return that value as the last value.

func one() string
func two() (string, error)
func three() (int, string, error)

Declaring Return Arguments

The type of the return value(s) is declared after the function definition.


package main

import "fmt"

func main() {
	fmt.Println(sayHello())
}

func sayHello() string {
	return "Hello"
}

Multiple Return Arguments

Multiple return arguments need to be placed inside of ().


package main

import "fmt"

func main() {
	l, c := slicer([]string{"John", "Paul", "George", "Ringo"})
	fmt.Println(l)
	fmt.Println(c)
}

func slicer(s []string) (int, int) {
	return len(s), cap(s)
}

Functions Accepting Arguments From Functions

You can accept the return arguments of a function into a function, as long as the return types (and number of return arguments) lines up with the number of arguments expected to the calling function.


package main

import (
	"fmt"
)

func main() {
	takeTwo(returnTwo())
}

func returnTwo() (string, string) {
	return "hello", "world"
}

func takeTwo(a, b string) {
	fmt.Println(a, b)
}

Named Returns

The return of a go function can be given a name and used just like a variable declaration.

  • Initialized with zero value in function scope
  • Executing a return with no value will return the value of the named return.
  • Names are not mandatory
func exists() (exist bool) {
    exist = true
    return
}

It is best practice to not use named returns, unless they are needed for documentation.

Given the following function that returns latitude and longitude, it would be hard to remember which parameter is meant for which value with this given signature:

func coordinates() (int, int)

Using named returns should only be used for documentation purposes. Even then, it could be argued to never use it.

func coordinates() (lat, lng int)

When Named Returns Go Bad

Here is an example of what can happen with named returns.

Please don’t do this in your code.


func someFunction(val int) (ok bool, err error) {
	if val == 0 {
		return false, nil
	}
	if val < 0 {
		return false, fmt.Errorf("value can't be negative %d", val)
	}
	ok = true
	return
}

Shadowing

Go has the ability to shadow a variable. As such, care must be taken to ensure you are accessing the proper variable instance.


func main() {
	i := 0
	for n := 0; n < 5; n++ {
		i := n // redeclare i (shadow)
		i++
		fmt.Println("loop:", i)
	}
	fmt.Println("final:", i)
}

Output:


loop: 1
loop: 2
loop: 3
loop: 4
loop: 5
final: 0

Shadowing Errors

It’s very common to shadow errors as a result of calling functions and being inside another scope, such as a loop:


func main() {
	err := boom()
	fmt.Println(err)
	for i := 5; i >= -1; i-- {
		n, err := inc(i) // err get's shadowed here
		if err != nil {
			fmt.Println(err)
			continue
		}
		fmt.Println(n)
	}
	fmt.Println(err)
}

Output:


boom
6
5
4
3
2
1
must be a positive number: -1
boom

While shadowing isn’t usually an issue, it’s something to be aware of to ensure you don’t use the wrong scope of a variable.

Detecting Shadows

The shadow tool can detect variable shadowing in your code. You can install it with:

go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest

This will install the shadow command in your $GOPATH/bin directory, and can now be run in any project with this syntax:

shadow ./...

Running it on the previous code returns this result:

main.go:13:6: declaration of "err" shadows declaration at line 10

Alternatively, you can run the shadow analyzer via go vet:

go vet -vettool=$(which shadow) ./...

For comprehensive static analysis including shadow detection, consider using golangci-lint which is covered in the tooling module.

Function Exercise (10 Mins)


package main

import (
	"fmt"
	"strings"
)

func main() {
	// Say "Good morning" to John
	echo(sayHello("John"))
	fmt.Println(Duplicate("gopher"))
	fmt.Println(ProperName("rob", "pike"))
}

// Create a function called `echo` that takes a single argument
// of type string and prints it out.
// func() {}

// Change sayHello to allow the greeting to be customized
func sayHello(name string) string {
	return "Hello " + name
}

// Change the following function to remove the named return
func Duplicate(s string) (dup string) {
	dup = s + " " + s
	return
}

// Change the following function to declare the argument type for each argument provided
func ProperName(first, last string) string {
	return fmt.Sprintf("%s %s", strings.Title(first), strings.Title(last))
}

// Fix the following function declaration
func Broken(s string) string, error {
	return s, errors.New("I'm broken")
}

Function Solution


package main

import (
	"errors"
	"fmt"
	"strings"
)

func main() {
	// Say "Good morning" to John
	echo(sayHello("Good morning", "John"))
	fmt.Println(Duplicate("gopher"))
	fmt.Println(ProperName("rob", "pike"))
}

// Create a function called `echo` that takes a single argument
// of type string and prints it out.
func echo(s string) {
	fmt.Println(s)
}

// Change sayHello to allow the greeting to be customized
func sayHello(greeting string, name string) string {
	return greeting + " " + name
}

// Change the following function to remove the named return
func Duplicate(s string) string {
	return s + " " + s
}

// Change the following function to declare the argument type for each argument provided
func ProperName(first string, last string) string {
	return fmt.Sprintf("%s %s", strings.Title(first), strings.Title(last))
}

// Fix the following function declaration
func Broken(s string) (string, error) {
	return s, errors.New("I'm broken")
}

Variadic Arguments

Functions can take variadic arguments, meaning 0 to N arguments of the same type. This is done by prepending the type of the argument with ....


package main

import "fmt"

func main() {
	sayHello("John", "Paul", "George", "Ringo")
	// Hello John
	// Hello Paul
	// Hello George
	// Hello Ringo
	sayHello("George")
	// George
	sayHello()
	//
}

func sayHello(names ...string) {
	for _, n := range names {
		fmt.Printf("Hello %s\n", n)
	}
}

Variadic Argument Position

Variadic arguments must be the last argument of the function.


package main

import "fmt"

func main() {
	sayHello("John", "Paul", "George", "Ringo")
}

func sayHello(names ...string, group string) {
	for _, n := range names {
		fmt.Printf("Hello %s\n", n)
	}
}

Error


variadic-bad.go:9: can only use ... with final parameter in list

Exploding Variadic Arguments

Slices can be “exploded” using trailing ... to send them as variadic arguments to a function.


package main

import "fmt"

func main() {
	beatles := []string{"John", "Paul", "George", "Ringo"}
	sayHello(beatles...)
	// Hello John
	// Hello Paul
	// Hello George
	// Hello Ringo
}

func sayHello(names ...string) {
	for _, n := range names {
		fmt.Printf("Hello %s\n", n)
	}
}

When To Use Variadic

While using a variadic argument is not the common use case, it certainly can make your code look nicer and easier to read and use. The most common reason to use a variadic is when your function is commonly called with zero, one, or many arguments.

Take the following code that looks up Users by ID.

func LookupUsers(ids []int) []Users {
    for i, id := range ids {
        ...
    }
}

You may have one or many users to look up. Without using a variadic signature, your code would look like this:

var id1, id2, id3 int
id1 = 1
id2 = 2
id3 = 3
var ids := []int{1, 2, 3)
LookupUsers([]int{id1})
LookupUsers([]int{id1, id2, id3})
LookupUsers(ids)

By changing the signature to be variadic as follows:

func LookupUsers(ids ...int) []Users {
    for i, id := range ids {
        ...
    }
}

The code will now look like this, but function the same way as before:

var id1, id2, id3 int
id1 = 1
id2 = 2
id3 = 3
var ids := []int{1, 2, 3)

LookupUsers(id1)
LookupUsers(id1, id2, id3)
LookupUsers(ids...)

Variadic Exercise (10 Mins)


package main

import "fmt"

// Create a function called `printSlice` that takes a slice of strings
// and prints them out.
// func () {}

// Create a function called `printAll` that takes a string as a variadic
// argument and prints them out.
// func () {}

func main() {
	fruit := []string{"banana", "orange", "strawberry", "apple"}
	fmt.Println("Print out a slice")
	printSlice(fruit)
	fmt.Println()

	fmt.Println("Print out multiple items")
	printAll("banana", "orange", "strawberry", "apple")

	// Explode the arguments to pass a slice to printAll
	fmt.Println() // insert blank line in output
	printAll(fruit...)

	// You can also call printAll with no arguments
	fmt.Println() // insert blank line in output
	printAll()
}

Variadic Solution


package main

import "fmt"

// Create a function called `printSlice` that takes a slice of strings and prints them out.
func printSlice(sl []string) {
	for _, s := range sl {
		fmt.Println(s)
	}
	// or printAll(sl...)
}

// Create a function called `printAll` that takes a string as a variadic argument and prints them out.
func printAll(sl ...string) {
	for _, s := range sl {
		fmt.Println(s)
	}
}

func main() {
	fruit := []string{"banana", "orange", "strawberry", "apple"}
	fmt.Println("Print out a slice")
	printSlice(fruit)
	fmt.Println()

	fmt.Println("Print out multiple items")
	printAll("banana", "orange", "strawberry", "apple")

	// Explode the arguments to pass a slice to printAll
	fmt.Println() // insert blank line in output
	printAll(fruit...)

	// You can also call printAll with no arguments
	fmt.Println() // insert blank line in output
	printAll()
}

Methods

Methods are syntactic sugar for declaring functions on types.


package main

import "fmt"

type Beatle struct {
	Name       string
	Instrument string
}

func (b Beatle) String() string {
	return fmt.Sprintf("%s plays %s", b.Name, b.Instrument)
}

func main() {
	b := Beatle{Name: "Ringo", Instrument: "Drums"}
	fmt.Println(b.String()) // Ringo plays Drums
}

Method Receivers

Methods also allow us to namespace a function to a receiver.


func (b Beatle) String() string {
	return fmt.Sprintf("%s plays %s", b.Name, b.Instrument)
}

func String(b Beatle) string {
	return fmt.Sprintf("%s plays %s", b.Name, b.Instrument)
}

func main() {
	b := Beatle{Name: "Ringo", Instrument: "Drums"}
	fmt.Println(b.String()) // Ringo plays Drums
	fmt.Println(String(b))  // Ringo plays Drums
}

Both functions are equivalent, but the former namespaces the String function to the Beatle type, allowing us to have multiple String functions with different receivers.

Value Vs. Pointer Receivers

You can define a receiver for a method as either a value or a pointer receiver. They will behave differently.

A value receiver can not mutate the data, while a pointer receiver can.


package main

import (
	"fmt"
	"strings"
)

type User struct {
	First string
	Last  string
}

// Pointer Receiver
func (u *User) Titleize() {
	u.First = strings.Title(u.First)
	u.Last = strings.Title(u.Last)
}

// Value Receiver
func (u User) Reset() {
	u.First = ""
	u.Last = ""
}

func main() {
	u := User{First: "cory", Last: "lanou"}
	u.Titleize()
	fmt.Println(u)

	// Reset can't mutate the data as it's defined with a value receiver
	u.Reset()
	fmt.Println(u)
}

Nil Receivers

Care must be taken when using pointer receivers. If the receiver itself is nil, you can still call methods that don’t reference the receiver, but if you call a method that does reference a receiver, it will panic with a nil pointer dereference.

In the code below, there is a IAmGroot method, and as it doesn’t reference the pointer receiver, we can still call the method even though the variable u is nil.

However, when we try to call Reset, this will panic, as it tries to reference the receiver, which is nil.


package main

import "fmt"

type User struct {
	First string
	Last  string
}

func (u *User) Reset() {
	u.First = ""
	u.Last = ""
}

func (u *User) IAmGroot() string {
	return "I am Groot"
}

func main() {
	var u *User

	fmt.Println(u.IAmGroot())

	u.Reset()
}

Nil Receivers Checks

You can check to see if the receiver is nil before referencing it. While this may work in some scenarios, it may inadvertently end up as a Nop in your code and therefore fail silently, creating a very difficult bug to find later on.

You must decide is it better for the code to panic, or better for the code to fail silently. In most cases, you would prefer a panic and make sure you have test coverage to ensure this doesn’t happen at runtime.


package main

import "fmt"

type User struct {
	First string
	Last  string
}

func (u *User) Reset() {
	if u == nil {
		return
	}
	u.First = ""
	u.Last = ""
}

func (u *User) IAmGroot() string {
	return "I am Groot"
}

func main() {
	var u *User

	fmt.Println(u.IAmGroot())

	u.Reset()
}

Resetting With Pointer Receivers

We saw earlier that we defined a reset method by setting each field one by one to a default value. You can also use a pointer receiver to set the entire instance back to another value:


package main

import "fmt"

type User struct {
	First string
	Last  string
}

func (u *User) Reset() {
	*u = User{}
}

func main() {
	u := User{First: "Rob", Last: "Pike"}

	fmt.Println(u)

	u.Reset()

	fmt.Println(u)
}

Output:


{Rob Pike}
{ }

Methods Expressions

Methods are really just syntactic sugar for functions that are name-spaced to a concrete type. While we typically don’t use them in the fashion shown below, this may help understand that the receiver is in fact just the first argument in the function call.


package main

import (
	"fmt"
)

type User struct {
	First string
	Last  string
}

func (u User) Greet(greeting string) {
	fmt.Println(greeting + " " + u.First + " " + u.Last)
}

func main() {
	u := User{"Cory", "LaNou"}
	u.Greet("hi")

	User.Greet(u, "goodbye")
}

Method Chaining

In many languages, method chaining is very common. While it’s not as common in the standard library for Go, there are many third party packages that take advantage of method chaining.

The key to method chaining is to return a pointer to the value of the current instance. Keep in mind that this really only works if you don’t have to return any other values from the method.

The below example shows how you can use the Init method and immediatly chain to the Run method:


package main

import "fmt"

func main() {
	c := &Config{}

	if err := c.Init(80).Run(); err != nil { // <- Method Chain: Init.Run
		fmt.Println(err)
	}
}

type Config struct {
	port int
}

func (c *Config) Init(port int) *Config {
	c.port = port
	return c
}

func (c *Config) Run() error {
	return fmt.Errorf("something went wrong...")
}

Method Exercise (10 Mins)


package main

import "fmt"

type User struct {
	First string
	Last  string
	Email string
}

// Create a method on User called `FullName` that takes no arguments,
// and returns the First and Last name as a string
// Example User{First: "Rob", Last: "Pike", Email: "commander@google.com"} would return "Rob Pike"

// Create a method on User called `FormattedEmail` that takes no arguments,
// and returns a valid formatted email as a string
// Example User{First: "Rob", Last: "Pike", Email: "commander@google.com"} would return "Rob Pike <commander@google.com>"

// Create a method on User called `Reset` that sets all the fields to a blank string.
// Hint: we need to change the value so we need a special receiver for that...
// Extra Credit: do this in on line of code

func main() {
	u := User{"Rob", "Pike", "commander@google.com"}
	fmt.Printf("%+v\n", u)
	fmt.Println("Full Name", u.FullName())
	fmt.Println("Formatted Email", u.FormattedEmail())
	u.Reset()
	fmt.Printf("%+v\n", u)
}

Method Solution


package main

import "fmt"

type User struct {
	First string
	Last  string
	Email string
}

func (u User) FullName() string {
	return u.First + " " + u.Last
}

func (u User) FormattedEmail() string {
	return fmt.Sprintf("%s %s <%s>", u.First, u.Last, u.Email)
}

func (u *User) Reset() {
	u.First = ""
	u.Last = ""
	u.Email = ""
}
func main() {
	u := User{"Rob", "Pike", "commander@google.com"}
	fmt.Printf("%+v\n", u)
	fmt.Println("Full Name", u.FullName())
	fmt.Println("Formatted Email", u.FormattedEmail())
	u.Reset()
	fmt.Printf("%+v\n", u)
}

Extra Credit Solution:


package main

import "fmt"

type User struct {
	First string
	Last  string
	Email string
}

func (u User) FullName() string {
	return u.First + " " + u.Last
}

func (u User) FormattedEmail() string {
	return fmt.Sprintf("%s %s <%s>", u.First, u.Last, u.Email)
}

func (u *User) Reset() {
	*u = User{}
}

func main() {
	u := User{"Rob", "Pike", "commander@google.com"}
	fmt.Printf("%+v\n", u)
	fmt.Println("Full Name", u.FullName())
	fmt.Println("Formatted Email", u.FormattedEmail())
	u.Reset()
	fmt.Printf("%+v\n", u)
}

First Class Functions

In Go functions are first class citizens. That means we can treat functions as we do any other types.

  • Function signatures are a type
  • Functions can be arguments and return values to/from other functions

Functions As Variables

This function has no name, but is assigned to a variable that can be called later.


package main

import "fmt"

func main() {
	f := func() {
		fmt.Println("Hello")
	}

	f() // Hello
}

Closures

Functions close over their environment when they are defined. This means that a function has access to arguments declared before its declaration.


package main

import "fmt"

func main() {
	name := "Ringo"
	f := func() {
		fmt.Printf("Hello %s", name)
	}

	f() // Hello Ringo
}

Functions As Arguments

Functions in Go can be passed as values to other functions.


package main

import "fmt"

func main() {
	f := func() string {
		return "Hello"
	}
	sayHello(f)
}

func sayHello(fn func() string) {
	fmt.Println(fn())
}

The signature of the function being passed in must match the function signature of the argument in the callee function.

Anonymous Functions

Anonymous functions are functions that aren’t assigned to a variable, or that don’t have a “name” associated with them.

sayHello(func() string {
  return "Hello"
})

This is a very common pattern in Go, and one you should get comfortable seeing and using in Go code.

Functions As Types

Function signatures act as a defacto “type”, but explicit types can also be created for a function signature.

This has a few benefits:

  • Argument declaration is cleaner
  • The new type can have other functions hung off it
  • The new type can now easily be documented

package main

import "fmt"

type greeter func() string

func main() {
	sayHello(func() string {
		return "Hello"
	})
}

func sayHello(fn greeter) {
	fmt.Println(fn())
}

Functions On Functions

By creating a new type for a function, we can treat that type like all types in Go, and can hang other functions off of it.


type greeter func() string

func (greeter) Name() string {
	return "World"
}

func sayHello(fn greeter) {
	fmt.Println(fn(), fn.Name())
}

First Class Functions Exercise (10m)

Using the code below, refactor it to make it easier to read by using typed arguments and function variables.


package main

import (
	"fmt"
	"strings"
)

// TODO: declare a type called `replacer` that has the function signature for the second argument to the `replace` function below

// TODO: update the replace signature to take a `replacer` as the second argument
func replace(s string, r func(string) string) string {
	var result string
	for _, v := range s {
		result = result + r(string(v))
	}
	return result
}

func main() {
	// Capitalize each letter
	s := replace("go programmers are gophers", func(s string) string {
		return strings.ToUpper(s)
	})
	fmt.Println(s)

	// replace all vowels with the $ sign
	// TODO: Refactor the second argument to be stored as a variable (`r`) and pass in that variable as the second argument
	s = replace("go programmers are gophers", func(s string) string {
		switch s {
		case "a", "e", "i", "o", "u":
			return "$"
		default:
			return s
		}
	})
	fmt.Println(s)
}

The code currently works and has the following output:


GO PROGRAMMERS ARE GOPHERS
g$ pr$gr$mm$rs $r$ g$ph$rs

First Class Functions Solution

While the functinality of the code did not change at all, using functions as types and variables can make your code clearner and easier to read.


package main

import (
	"fmt"
	"strings"
)

// TODO: declare a type called `replacer` that has the function signature for the second argument to the `replace` function below
type replacer func(string) string

// TODO: update the replace signature to take a `replacer` as the second argument
func replace(s string, r replacer) string {
	var result string
	for _, v := range s {
		result = result + r(string(v))
	}
	return result
}

func main() {
	// Capitalize each letter
	s := replace("go programmers are gophers", func(s string) string {
		return strings.ToUpper(s)
	})
	fmt.Println(s)

	// replace all vowels with the $ sign
	// TODO: Refactor the second argument to be stored as a variable (`r`) and pass in that variable as the second argument
	r := func(s string) string {
		switch s {
		case "a", "e", "i", "o", "u":
			return "$"
		default:
			return s
		}
	}
	s = replace("go programmers are gophers", r)
	fmt.Println(s)
}

Note, the output did not change:


GO PROGRAMMERS ARE GOPHERS
g$ pr$gr$mm$rs $r$ g$ph$rs

Deferring Function Calls

The defer keyword in Go allows us to defer the execution of a function call until the return of the parent function.


package main

import "fmt"

func main() {
	defer goodbye()
	fmt.Println("hello")
}

func goodbye() {
	fmt.Println("goodbye")
}
hello
goodbye

Deferring With Multiple Returns

defer is useful for ensuring functions get called regardless of which path your code takes.


func loadPage() ([]byte, error) {
	resp, err := http.Get("https://www.gopherguides.com")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close() // <<<
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("bad status: %d", resp.StatusCode)
	}
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return b, nil
}

Defer Call Order

The order of execution for deferred function calls is LIFO (last in, first out).


package main

import "fmt"

func main() {
	defer fmt.Println("one")
	defer fmt.Println("two")
	defer fmt.Println("three")
}
three
two
one

Deferred Calls And Panic

Deferred calls will still get called even if another deferred call panics.


package main

import "fmt"

func main() {
	defer fmt.Println("one")
	defer panic("two")
	defer fmt.Println("three")
}
three
one
panic: two

goroutine 1 [running]:
main.main()
 main.go:9 +0x1ba
exit status 2
shell returned 1

You can recover from a panic as well. This will be covered in a later module.

Defers And Exit/Fatal

Deferred calls will not fire if the code explicitly calls os.Exit or log.Fatal


package main

import (
	"fmt"
	"os"
)

func main() {
	defer fmt.Println("one")
	os.Exit(1)
	defer fmt.Println("three")
}

package main

import (
	"fmt"
	"log"
)

func main() {
	defer fmt.Println("one")
	log.Fatal("boom")
	defer fmt.Println("three")
}

Defer And Anonymous Functions

It is also common to combine cleanup tasks into a single defer using an anonymous function.


package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	if err := fileCopy("readme.txt", "readme-copy.txt"); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println("file copied successfully")
}

func fileCopy(sourceName string, destName string) error {
	src, err := os.Open(sourceName)
	if err != nil {
		return err
	}
	defer func() {
		fmt.Println("closing", sourceName)
		src.Close()
	}()

	dst, err := os.Create(destName)
	if err != nil {
		return err
	}
	defer func() {
		fmt.Println("closing", destName)
		dst.Close()
	}()

	if _, err := io.Copy(dst, src); err != nil {
		return err
	}
	return nil
}

Defer And Scope

It is important to understand scope when using defer. If we take the following code, the output is a bit unexpected:


func main() {
	now := time.Now()
	defer fmt.Printf("time since program started: %s\n", time.Since(now))

	fmt.Println("sleeping for 2 seconds...")
	time.Sleep(2 * time.Second)
}

Output:


sleeping for 2 seconds...
time since program started: 157ns

The reason that we don’t see the proper elapsed time, is because we didn’t capture a scope to the now variable to use later in the defer function. Because of this, when the defer is added to the stack, it captures the value of now when the defer is scheduled, which in effect will have no elapsed time when it runs later.

Fixing Defer And Scope

To make sure your variables are evaluated when a defer executes, and not when they are scheduled, you need to scope them via an anonymous function.


func main() {
	now := time.Now()

	// use an anonymous function to scope variables to be used in the defer
	defer func(now time.Time) {
		fmt.Printf("time since program started: %s\n", time.Since(now))
	}(now)

	fmt.Println("sleeping for 2 seconds...")
	time.Sleep(2 * time.Second)
}

Output:


sleeping for 2 seconds...
time since program started: 2.004430822s

When the defer function runs, it will be passed the current value of now, instead of evaluating the value of now when the defer is scheduled.

Properly Closing Files

In Go, the rule is to never ignore an error. However, when you defer the Close on most types in Go, there is no easy way to handle the error.

As such, there are a couple of idioms that have become standard in the Go ecosystem.

The first is to explicitly call Close even if it’s deferred. They reason for this is that the documents in Go specify that any call to a Close function should be idempotent, and calling it more than once will be ok.

In the following code sample, the Close is deferred. In the event that the code returns early, it’s because you already encountered an error, and likely the Close is going to also encounter an error related to the same issue, and therefore is not important:


func closeit() error {
	f, err := os.Create("foo")
	if err != nil {
		return err
	}
	defer f.Close()

	if _, err := f.Write([]byte("bar")); err != nil {
		return err
	}

	if err := f.Close(); err != nil {
		return err
	}
	return nil
}

There are other factors that go beyond the scope of this training, but there is a great article that goes in depth on the topic.

Common Mistake: HTTP Response Body Leaks

When working with HTTP clients, failing to properly close response bodies is one of the most common mistakes in Go. This can lead to resource leaks and connection pool exhaustion.

The Problem: Deferring Close Too Late

A common mistake is to defer the Close() call after checking the response status code:


func fetchBad(client *http.Client) error {
	resp, err := client.Get("https://api.example.com/users/1")
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	defer resp.Body.Close() // too late; early returns leak the body

	_, err = io.ReadAll(resp.Body)
	return err
}

If the status code check fails, the function returns early and resp.Body.Close() is never called, causing a resource leak.

The Solution: Defer Immediately After Error Check

The correct pattern is to defer Close() immediately after checking that the HTTP request didn’t return an error:


func fetchGood(client *http.Client) error {
	resp, err := client.Get("https://api.example.com/users/1")
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close() // always defer immediately

	if resp.StatusCode != http.StatusOK {
		// drain the body so the transport can reuse the connection
		io.Copy(io.Discard, resp.Body)
		return fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	_, err = io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read body: %w", err)
	}

	return nil
}

This ensures the response body is always closed, regardless of which return path is taken.

Why This Matters: Connection Pool Implications

Go’s HTTP client maintains a connection pool for performance. When you don’t close the response body:

  1. Connections remain open and can’t be reused
  2. Connection pool gets exhausted after enough requests
  3. New requests block waiting for available connections
  4. Memory leaks as resources aren’t released

Even for error responses (4xx, 5xx), the body must be closed to return the connection to the pool.

Best Practice: Read And Discard Error Response Bodies

For connection reuse, you should also read and discard the body on error responses:


func fetchGood(client *http.Client) error {
	resp, err := client.Get("https://api.example.com/users/1")
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close() // always defer immediately

	if resp.StatusCode != http.StatusOK {
		// drain the body so the transport can reuse the connection
		io.Copy(io.Discard, resp.Body)
		return fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	_, err = io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read body: %w", err)
	}

	return nil
}

Using io.Copy(io.Discard, resp.Body) ensures:

  • The connection can be reused
  • No memory is wasted storing the error body
  • The connection pool remains healthy

Complete Example

Here’s a complete example showing the proper pattern:


func main() {
	badClient, badBody := newClient(http.StatusServiceUnavailable)
	err := fetchBad(badClient)
	fmt.Printf("bad version error: %v\n", err)
	fmt.Printf("bad version closed body: %t\n", badBody.closed)

	goodClient, goodBody := newClient(http.StatusServiceUnavailable)
	err = fetchGood(goodClient)
	fmt.Printf("good version error: %v\n", err)
	fmt.Printf("good version closed body: %t\n", goodBody.closed)
}

Output:


bad version error: unexpected status: 503
bad version closed body: false
good version error: unexpected status: 503
good version closed body: true

Remember: Always defer resp.Body.Close() immediately after checking the error from the HTTP request, before any other status checks.

Named Returns, Defer, And Scope

Now that we have a better understanding of functions, defer, and scope, this is another example of where named returns can be very difficult to determine behavior.

Remember, when writing code in Go, readability is very important. This code does not do what most people would expect.

Before this code is run, who can tell me what the value will be?


package main

import (
	"fmt"
)

func main() {
	fmt.Println(MeaningOfLife())
}

func MeaningOfLife() (rc int) {
	defer func() { rc++ }()
	return 41
}

Defer Exercise (10 Mins)

Using defer, only have one statement to close the file, and ensure that regardless of what branch of code you exit from, the file will be closed.

Extra Credit: Wrap your defer in an anonymous function and print out “Closing File” in addition to closing the actual file.


package main

import (
	"io"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		panic(err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	_, err = io.WriteString(file, text)
	if err != nil {
		file.Close()
		return err
	}
	file.Close()
	return nil
}

Defer Solution


package main

import (
	"io"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		panic(err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}
	// explicitly close the file
	if err := file.Close(); err != nil {
		return err
	}
	return nil
}

Extra credit:


package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		panic(err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer func() {
		fmt.Printf("closing %s\n", file.Name())
		file.Close()
	}()
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}
	// explicitly close the file
	if err := file.Close(); err != nil {
		return err
	}
	return nil
}

Init

Any time you declare an init() function, Go will load and run it prior to anything else in that package.


package main

import (
	"fmt"
	"time"
)

var weekday string

func init() {
	weekday = time.Now().Weekday().String()
}

func main() {
	fmt.Printf("Today is %s", weekday)
}

Multiple Init Statements

Unlike the main() function that can only be declared once, the init() function can be declared multiple times throughout a package. However, multiple init()s can make it difficult to know which one has priority over the others. In this section, we will show how to maintain control over multiple init() statements.

In most cases, init() functions will execute in the order that you encounter them.


package main

import "fmt"

func init() {
	fmt.Println("First init")
}

func init() {
	fmt.Println("Second init")
}

func init() {
	fmt.Println("Third init")
}

func init() {
	fmt.Println("Fourth init")
}

func main() {}

Output:


First init
Second init
Third init
Fourth init

Init Order

If you have multiple init() declarations in a package in different files, the compiler will run them based on the order in which it loads files.

Given the following directory structure, if you had an init() declaration in a.go and b.go, the first init() to run would be from a.go. However, if you renamed a.go to c.go, the init() from b.go would now run first.

Care must be taken if any init() declarations have an order of precedence when running and exist in different files.

└── cmd
    ├── a.go
    ├── b.go
    └── main.go

Using Init() For Side Effects

In Go, it is sometimes desirable to import a package not for its content, but for the side effects that occur upon importing the package. This often means that there is an init() statement in the imported code that executes before any of the other code, allowing for the developer to manipulate the state in which their program is starting. This technique is called importing for a side effect.

A common use case for importing for side effects is to register functionality in your code, which lets a package know what part of the code your program needs to use. In the image package, for example, the image.Decode function needs to know which format of image it is trying to decode (jpg, png, gif, etc.) before it can execute. You can accomplish this by first importing a specific program that has an init() statement side effect.

Let’s say you are trying to use image.Decode on a .png file with the following code snippet:


func decode(reader io.Reader) image.Rectangle {
	m, _, err := image.Decode(reader)
	if err != nil {
		log.Fatal(err)
	}
	return m.Bounds()
}

A program with this code will still compile, but any time we try to decode a png image, we will get an error.

To fix this, we would need to first register an image format for image.Decode. Luckily, the image/png package contains the following init() statement:

func init() {
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

Therefore, if we import image/png into our decoding snippet, then the image.RegisterFormat() function in image/png will run before any of our code:


package main

import (
	"fmt"
	"image"
	_ "image/png"
	"io"
	"log"
)

func main() {
	fmt.Println("vim-go")
}

func decode(reader io.Reader) image.Rectangle {
	m, _, err := image.Decode(reader)
	if err != nil {
		log.Fatal(err)
	}
	return m.Bounds()
}

This will set the state and register that we require the png version of image.Decode(). This registration will happen as a side effect of importing image/png.

Init Exercise (10 Mins)


package main

import (
	"fmt"
	"os"
	"strings"
	"time"
)

var version = "0.1"

func main() {
	// #TODO: Move all the logic to parse name to the `init` function
	var name = ""
	name = os.Args[0]
	if strings.Contains(name, string(os.PathSeparator)) {
		s := strings.Split(name, string(os.PathSeparator))
		name = s[len(s)-1]
	}
	fmt.Printf("%s version %s starting at %s\n", name, version, time.Now().Format(time.Kitchen))
}

Init Solution


package main

import (
	"fmt"
	"os"
	"strings"
	"time"
)

var version = "0.1"
var name string

func init() {
	name = os.Args[0]
	if strings.Contains(name, string(os.PathSeparator)) {
		s := strings.Split(name, string(os.PathSeparator))
		name = s[len(s)-1]
	}
}

func main() {
	fmt.Printf("%s version %s starting at %s\n", name, version, time.Now().Format(time.Kitchen))
}