The Problems of Golang init Function

4 min

Before we talk about the Golang init function, we should understand what a package is in Golang. A Go program is organized into packages. A package collects some source files in the same directory. It works like a box containing some tools or a small machine. It is the starting point to initialize the entire package, which aligns with the purpose of the init function.

Suppose you had some code without an init function like this:

//foo.go
package foo
var A int
func bar(){}
//main.go
import("foo")
function main(){
    fmt.Println(foo.A)
}

In this case, you import the foo package and use the variable A without the other parts. Everything is explicit. You might wonder, if I only use the variable A, can I just import A without the other variables and functions in this package? The answer in Golang is: No. You can’t do this; you must import the entire package because it is a programming unit that can’t be divided. This code works effectively until the init function joins the game.

A package can have several init functions, which might look like this:

//foo.go
package foo
var A int
func init(){A=1}
func bar(){}
//bar.go
package foo
var B int
func init(){B=2}
func bar(){}

As a package user, your code doesn’t change; it still only uses the variable A:

//main.go
import("foo")
function main(){
    fmt.Println(foo.A)
}

The package still works, but the init function runs implicitly without your knowledge. In Golang, you must accept the cost of init when you use the package. It’s simple, but the cost is not just the implicit running; it also couples the entire package.

When you try to write some unit tests, you can’t prevent the init function from running. Especially if it initializes some external resources (such as databases, files, logs, or others), your unit tests could break down because they must load the resources, even if you just want to write a tiny unit test.

If you want your code to work effectively, you should avoid using the init function. Because the init function is global, you can’t control its running timing. The worst disadvantage of the init function is that it hides the processing of a package, making it hard to know its running order, even if you can write some test code to determine the ordering.

The init function is not called by the package user; it is called before main. When an error occurs in the init function, what can you do? How do you use the usual error mechanism (if err != nil) to handle the errors? Maybe you can use panic in it, but how do you use recover to handle this panic? How do you tell the package users that they must ensure the package will not panic? How do you explain that the package might panic on startup, even if the package user just includes an import line in their code?

func init(){
    f, err := file.Open(path) //how to handle the err?
}

The above code will open a file path for writing or reading. When you run your code in the correct path, everything is okay. But if your working directory changes or you want to use some relative paths, how do you handle the errors? That’s why you should never put code that might have errors in the init function, and don’t initialize other package’s resources in it.

pakage foo
import "bar"
function init(){
    bar.Initlization()
}

If you do this, your package will not work independently. To keep your code clean, you should never put any other package code in the init function. If other packages need to be initialized, they must provide an initialization entry, or they must initialize themselves.

After thinking about the problems I'd met in init function, and read some discussions about removing init function in Go. I got the best practice of using init function is: Don't use.

After thinking about the problems I encountered with the init function and reading some discussions about removing the init function in Go, I realized the best practice for using the init function is simply: DON'T USE IT.

There are several ways to avoid using the init function.

If you have a global variable at the package level, initialize it at the declaration.

var(
    a = 0
    p *foo= nil
)

If the other package’s resources need to be initialized, or some extra resources need to be initialized, use an exported initialization function.

package foo
var (
    f *os.File
)
func InitFoo(path string) (error){
    f, err := file.Open(path)
    _ := f
    return err
}

If you want to ensure the init function runs only once, use sync.Once.Do:

package foo
var(
    once sync.Once
    f *os.File
)
func InitFoo(path string) (error){
    var err error
    once.Do(func(){
        f, err = os.Open(path)
    })
    return err
}

If your package has several parts of resources, and you want them to be initialized individually, use the old and reliable Object-Oriented Programming (OOP) approach.

//foo.go
package foo
struct Foo type{
}
func NewFoo() (*Foo, error){
    return &Foo{}, nil
}

//bar.go
package foo
struct Bar type{
}
func NewBar() (*Bar, error){
    return &Bar{}, nil
}

If you still want to use the init function in your code, the only advice is don’t call any other packages in the init function, even if it’s just a variable.

Removing the init function will make your code more transparent and decoupled. Everything will work explicitly, the costs will be visible, and your code will be simple and easy to read.


Note: This post was originally published on liyafu.com (One of our makers' personal blog)


Nowadays, we spend most of my time building softwares. This means less time writing. Building softwares has become my default way of online expression. Currently, we are working on Slippod, a privacy-first desktop note-taking app and TextPixie, a tool to transform text including translation and extraction.