init(), Package Variables and Import Order

How package variables and init functions run before main, and why application code usually should not use init.

Syntaxfunc init() { } // no parameters, no results, cannot be called by name

Before main runs, Go starts up every package in the program. Imported packages are initialised first, all the way down, so a package can rely on everything it imports being ready before any of its own code runs.

Variables first

Package-level variables are initialised in dependency order, not in the order they are written. If b is declared above a but its value uses a, then a is set first. Go works the order out from references to variables, functions and methods declared in the same package. When one package spans several files, the order follows the order the files reach the compiler, and the go command hands them over sorted by filename.

Then init

A function named init runs after every package-level variable in that package has a value. It takes no parameters and returns nothing. You may declare several, in one file or spread across files, and they run in that same filename order. init cannot be called by name and cannot be referred to at all. Package main's init functions run last, and then main.main starts.

Why it is usually a smell

init works invisibly. Nothing calls it, so nothing can decline it, including your tests. It cannot return an error, so it either panics or swallows the failure. It also runs before main has read flags or configuration. Prefer an ordinary function that returns a value and an error, called from main. The honest use of init is registration: a package adding itself to a registry, which is what blank imports are for.

Example

Example · go
package main

import "fmt"

// b is written first but depends on a, so a is initialised first.
var b = a + 1
var a = seed()

func seed() int {
	fmt.Println("seed runs")
	return 41
}

// init runs once the package variables above already have values.
// A file may declare more than one init; they run top to bottom.
func init() {
	fmt.Println("init 1, b =", b)
}

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

func main() {
	fmt.Println("main, b =", b)
}

// Output:
// seed runs
// init 1, b = 42
// init 2
// main, b = 42

When to use it

  • Working out why a package variable is already set, or still zero, when main starts.
  • Replacing an init that reads the environment with a loader that can report an error.
  • Understanding what a blank import such as _ "github.com/lib/pq" is doing in a file.
  • Debugging a program that panics before any of your own main code runs.

More examples

A blank import, which exists only to run init

This is the registration pattern, and the one place init earns its keep: a package announces itself to a registry so callers never name it in code. Database drivers work the same way. Recognising a blank import tells you the package is there for a side effect, not for any name you will call.

Example · go
package imageutil

import (
	"bytes"
	"image"

	// The blank identifier _ imports this package for the side effect
	// of its init alone: image/png's init calls image.RegisterFormat,
	// which teaches the image package how to read PNG bytes.
	// Without this import, DecodeConfig below cannot recognise a PNG.
	_ "image/png"
)

// FormatOf reports the image format of data, such as "png".
func FormatOf(data []byte) (string, error) {
	_, format, err := image.DecodeConfig(bytes.NewReader(data))
	return format, err
}

Explicit wiring instead of init

The replacement for the common init that reads environment variables into a package variable. The failure becomes a value main can print and exit on, the order is visible in main, and tests call LoadConfig as an ordinary function. Reach for this shape for configuration, database handles and clients.

Example · go
package main

import (
	"errors"
	"fmt"
	"os"
)

type Config struct {
	Addr string
}

// LoadConfig returns an error instead of panicking during startup.
// main decides what to do about a failure, and a test can call this
// directly with whatever environment it wants.
func LoadConfig() (Config, error) {
	addr := os.Getenv("APP_ADDR")
	if addr == "" {
		return Config{}, errors.New("APP_ADDR is not set")
	}
	return Config{Addr: addr}, nil
}

func main() {
	cfg, err := LoadConfig()
	if err != nil {
		fmt.Fprintln(os.Stderr, "config:", err)
		os.Exit(1)
	}
	fmt.Println("listening on", cfg.Addr)
}

// Output:
// listening on :8080     (when APP_ADDR=:8080)

Order across two files of one package

Variable initialisation follows dependencies across files, but init functions follow filename order. Worth knowing when you debug a startup problem, and a reason not to lean on it: renaming a file changes which init runs first, and nothing in the code shows that.

Example · go
// a_first.go
package main

import "fmt"

// greeting depends on prefix, which lives in the other file.
// Dependency order wins: prefix is set before greeting.
var greeting = prefix + "world"

func init() { fmt.Println("init in a_first.go, greeting =", greeting) }

// z_last.go
package main

import "fmt"

var prefix = "hello, "

func init() { fmt.Println("init in z_last.go") }

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

// Output:
// init in a_first.go, greeting = hello, world
// init in z_last.go
// main

Discussion

  • Be the first to comment on this lesson.