Part 13 of the Go Common Mistakes series: Common Mistakes and Best Practices for init functions

foreword

This is Part 13 of the Go Common Mistakes series: Common Mistakes and Best Practices for init Functions.

The material comes from Teiva Harsanyi , a Go evangelist and now a senior engineer at Docker .

All the source codes involved in this article are open source: Go common error source code , welcome your attention, and get the latest updates of this series in time.

Common Mistakes and Best Practices

Many Go language developers mistakenly use the init function in the package, which makes the code difficult to understand and difficult to maintain.

Let's first review the concept of the init function in the package, and then explain the common mistakes and best practices of the init function.

init basic concept

The init function in Go language has the following characteristics:

  • The init function has no parameters and no return value. If parameters or return values ​​are added, an error will be reported when compiling.
  • Each .go source file under a package can have its own init function. When this package is imported, the init function under the package will be executed.
  • A .go source file can have one or more init functions, although the function signatures are exactly the same, but Go allows this.
  • The global constants and variables in the .go source file will be parsed by the compiler first, and then the init function will be executed.

Example 1

Let's look at the following code example:

package main

import "fmt"

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

func init() {
    
    
	fmt.Println(a)
}

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

var a = func() int {
    
    
	fmt.Println("var")
	return 0
}()

go run main.goThe result of executing this program is:

var
init
0
main

Although the definition of global variables ais placed at the end, it is first parsed by the compiler, then the init function is executed, and finally the main function is executed.

Example 2

There are 2 packages: mainand redis, mainthis package depends on redisthis package.

package main
 
import (
    "fmt"
 
    "redis"
)
 
func init() {
    
    
    // ...
}
 
func main() {
    
    
    err := redis.Store("foo", "bar")
    // ...
}
package redis
 
// imports
 
func init() {
    
    
    // ...
}
 
func Store(key, value string) error {
    
    
    // ...
}

Because mainit is imported redis, redisthe init function in this package is executed first, and then mainthe init function in this package is executed.

  • If there are multiple .go source files under a package, and each .go source file has its own init function, the init functions will be executed according to the dictionary order of the .go source file names. For example, there are two source files, a.go and b.go, both of which have init functions, so the init function in a.go is executed before the init function in b.go.
  • If there are multiple init functions in a .go source file, they are executed in the order of the code.

img

  • In our engineering practice, don't rely on the execution order of the init function. If you preset the execution order of init functions, it is usually very dangerous and not the best practice of Go language. Because the source file name is likely to be modified.

  • The init function cannot be called directly, otherwise an error will be reported when compiling.

    package main
     
    func init() {
          
          }
     
    func main() {
          
          
        init()
    }
    

    The above code compilation error is as follows:

    $ go build .
    ./main.go:6:2: undefined: init
    

By now, everyone should have a clearer understanding of the init function in the package. Next, let's look at the common mistakes and best practices of the init function.

Incorrect usage of init function

Let's first look at a common and bad usage of the init function.

var db *sql.DB
 
func init() {
    
    
    dataSourceName :=
        os.Getenv("MYSQL_DATA_SOURCE_NAME")
    d, err := sql.Open("mysql", dataSourceName)
    if err != nil {
    
    
        log.Panic(err)
    }
    err = d.Ping()
    if err != nil {
    
    
        log.Panic(err)
    }
    db = d
}

The above program does the following things:

  • Create a database connection instance.
  • Do a ping check on the database.
  • If both the connection to the database and the ping check pass, the database connection instance will be assigned to the global variable db.

You can first think about what problems this program will have.

  • First, the way to do error management in the init function is very limited. For example, the init function cannot return error, because the init function cannot have a return value. Then if an error occurs in the init function, if you want the outside world to perceive it, you must actively trigger a panic to stop the program. For the above sample program, although the init function encounters an error, it means that the database connection failed, it may be possible to stop the program from running. But to create a database connection in the init function, if it fails, it is not easy to do retry or fault tolerance. Just imagine, if you create a database connection in an ordinary function, then this ordinary function can return error information when creating a database connection fails, and then the caller of the function decides to retry or exit.

  • Second, it will affect the unit tests of the code. Because the init function will run before the test code is executed, if we just want to test a basic function in this package that does not require a database connection, the init function will still be executed during the test to create a database connection, which is obviously not It is not the effect we want, which increases the complexity of unit testing.

  • Third, this program assigns the database connection to a global variable. There are some potential risks in using global variables. For example, other functions in this package can modify the value of this global variable, resulting in incorrect modification; some unit tests that have nothing to do with database connections must also consider this global variable.

So how do we modify the above program to solve the above problems? Refer to the following code:

func createClient(dsn string) (*sql.DB, error) {
    
    
    db, err := sql.Open("mysql", dsn)
    if err != nil {
    
    
        return nil, err
    }
    if err = db.Ping(); err != nil {
    
    
        return nil, err
    }
    return db, nil
}

Using this function to create a database connection can solve the above three problems.

  • Error handling can be managed by the caller of the createClient function, and the caller can choose to exit the program or try again.
  • The unit test can not only test the basic functions that have nothing to do with the database, but also test the code implementation of createClient to check the database connection.
  • No global variables are exposed, and the database connection instance is created and returned in the createClient function.

when to use init function

The init function is not completely discouraged, and it can be considered in some scenarios. For example, the source code implementation of Go's official blog uses the init function.

func init() {
    
    
    redirect := func(w http.ResponseWriter, r *http.Request) {
    
    
        http.Redirect(w, r, "/", http.StatusFound)
    }
    http.HandleFunc("/blog", redirect)
    http.HandleFunc("/blog/", redirect)
 
    static := http.FileServer(http.Dir("static"))
    http.Handle("/favicon.ico", static)
    http.Handle("/fonts.css", static)
    http.Handle("/fonts/", static)
 
    http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/",
        http.HandlerFunc(staticHandler)))
}

In this source code, the init function cannot fail, because http.HandleFunc will only panic when the second handler parameter is nil. Obviously, the second handler parameter of http.HandleFunc in this program is a legal value, so The init function does not fail.

At the same time, there is no need to create global variables here, and this function will not affect unit tests.

So this is an example of a scenario where the init function is suitable.

Summarize

The init function should be used with caution. If it is used improperly, it may cause problems. Do not rely on the execution order of init in different .go files under the same package in the code.

Finally, review the precautions for the Go language init function:

  • The init function has no parameters and no return value. If parameters or return values ​​are added, an error will be reported when compiling.
  • Each .go source file under a package can have its own init function. When this package is imported, the init function under the package will be executed.
  • A .go source file can have one or more init functions, although the function signatures are exactly the same, but Go allows this.
  • The global constants and variables in the .go source file will be parsed by the compiler first, and then the init function will be executed.

recommended reading

open source address

Articles and sample code are open source on GitHub: Go Language Beginner, Intermediate, and Advanced Tutorials .

Official account: advanced coding. Follow the official account to get the latest Go interview questions and technology stack.

Personal website: Jincheng's Blog .

Zhihu: Wuji .

References

  • https://livebook.manning.com/book/100-go-mistakes-how-to-avoid-them/chapter-2/
  • https://github.com/jincheng9/go-tutorial/tree/main/workspace/lesson27

Guess you like

Origin blog.csdn.net/perfumekristy/article/details/127522655
Recommended