Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

Init functions are called only once, after all the variable declarations and before the main function. It allows you to initialize whatever your program needs before running.

Multiple init functions

A package can define many init functions, inside the same file or across many files, and can also import packages with their own init functions. Here is an example:

go file with two init functions

go file with one init functions in the same package

As we can see, our package main has two init functions: in the file main.go and in init.go . Let’s run it and get the order from the console:

init 1

init 2

init 3

main

Generating the asm code will show us the order that Go chooses to call our functions.

Compilation

The asm code can be generated thanks to the following command:

go tool compile -S main.go init.go

Here is the part of the generated code that involves the init functions:

0x0039 00057 (<autogenerated>:1) MOVB $1, "".initdone·(SB)

0x0040 00064 (<autogenerated>:1) CALL os.init(SB)

0x0045 00069 (<autogenerated>:1) CALL "".init.0(SB)

0x004a 00074 (<autogenerated>:1) CALL "".init.1(SB)

0x004f 00079 (<autogenerated>:1) CALL "".init.2(SB)

0x0054 00084 (<autogenerated>:1) MOVB $2, "".initdone·(SB)

We can now observe:

Go is using a variable initdone· as a guarder during the initialization

as a guarder during the initialization Go first imports the init functions from the imported packages, here os

Go lastly imports our custom init functions

The file init.go of the Go standard library also confirms this behavior:

// fninit hand-crafts package initialization code.

//

// func init.ializers() { (0)

// <init stmts>

// }

// var initdone· uint8 (1)

// func init() { (2)

// if initdone· > 1 { (3)

// return (3a)

// }

// if initdone· == 1 { (4)

// throw() (4a)

// }

// initdone· = 1 (5)

// // over all matching imported symbols

// <pkg>.init() (6)

// init.ializers() (7)

// init.<n>() // if any (8)

// initdone· = 2 (9)

// return (10)

// }

The variable initdone· prevents multiple loading of the same init functions. As seen previously in the asm code, the instruction MOVB (move byte) will load the value 1 first to initdone· and 2 when all init functions will be loaded and will block any new initialization.

Cold start

Regarding the performance, let’s run a benchmark with hyperfine with a program with more than 130 imported packages and another one with no import at all. Here is the benchmark for the first one:

Benchmark #1: ./bench-imports

Time (mean ± σ): 16.2 ms ± 0.7 ms [User: 8.7 ms, System: 6.4 ms]

Range (min … max): 14.8 ms … 18.5 ms 155 runs

Here is the benchmark for the code with no import:

Benchmark #1: ./bench-no-imports

Time (mean ± σ): 3.9 ms ± 0.5 ms

Range (min … max): 3.1 ms … 5.8 ms 410 runs

The cold start added by the init functions in this case is close to 12ms. Go team worked already to reduce the init-time for those methods and is still going on. This cold start will only impact your command line applications and not your server applications.