前言

这是Go常见谬误系列的第13篇:init函数的常见谬误和最佳实际。

素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi。

本文波及的源代码全副开源在:Go常见谬误源代码,欢送大家关注公众号,及时获取本系列最新更新。

常见谬误和最佳实际

很多Go语言开发者会谬误地应用package里的init函数,导致代码难懂,保护艰难。

咱们先回顾下package里init函数的概念,而后解说init函数的常见谬误和最佳实际。

init基本概念

Go语言里的init函数有如下特点:

  • init函数没有参数,没有返回值。如果加了参数或返回值,会编译报错。
  • 一个package上面的每个.go源文件都能够有本人的init函数。当这个package被import时,就会执行该package下的init函数。
  • 一个.go源文件里能够有一个或者多个init函数,尽管函数签名齐全一样,然而Go容许这么做。
  • .go源文件里的全局常量和变量会先被编译器解析,而后再执行init函数。

示例1

咱们来看如下的代码示例:

package mainimport "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.go执行这段程序的后果是:

varinit0main

全局变量a的定义尽管放在了最初面,然而先被编译器解析,而后执行init函数,最初执行main函数。

示例2

有2个package: mainredismain这个package依赖了redis这个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 {    // ...}

因为main import了redis,所以redis这个package里的init函数先执行,而后再执行main这个package里的init函数。

  • 如果一个package上面有多个.go源文件,每个.go源文件里都有各自的init函数,那会依照.go源文件名的字典序执行init函数。比方有a.go和b.go这2个源文件,外面都有init函数,那a.go里的init函数比b.go里的init函数先执行。
  • 如果一个.go源文件里有多个init函数,那依照代码里的先后顺序执行。

  • 咱们在工程实际里,不要去依赖init函数的执行程序。如果预设了init函数的执行程序,通常是很危险的,也不是Go语言的最佳实际。因为源文件名是有可能被批改的。
  • init函数不能被间接调用,否则会编译报错。

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

    下面这段代码编译报错如下:

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

到当初为止,大家对package里的init函数应该有了一个比拟清晰的了解,接下来咱们看看init函数的常见谬误和最佳实际。

init函数的谬误用法

咱们先看看init函数一种常见的不太好的用法。

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}

下面的程序做了如下几个事件:

  • 创立一个数据库连贯实例。
  • 对数据库做ping查看。
  • 如果连贯数据库和ping查看都通过的话,会把数据库连贯实例赋值给全局变量db

大家能够先思考下这段程序会有哪些问题。

  • 第一,init函数外面做谬误治理的形式是很无限的。比方,init函数没法返回error,因为init函数是不能有返回值的。那如果init函数呈现了error要让外界感知的话,得被动触发panic,让程序进行。对于下面的示例程序,尽管init函数遇到谬误时,示意数据库连贯失败,去进行程序运行或者是能够的。然而在init函数里去创立数据库连贯,如果失败的话,就不好做重试或者容错解决。试想,如果是在一个一般函数里去创立数据库连贯,那这个一般函数能够在创立数据库连贯失败的时候返回error信息,而后函数的调用者来决定做重试或者退出的操作。
  • 第二,会影响代码的单元测试。因为init函数在测试代码执行之前就会运行,如果咱们仅仅是想测试这个package里某个不须要做数据库连贯的根底函数,那测试的时候还是会执行init函数,去创立数据库连贯,这显然并不是咱们想要的成果,减少了单元测试的复杂性。
  • 第三,这段程序把数据库连贯赋值给了全局变量。用全局变量会有一些潜在的危险,比方这个package里的其它函数能够批改这个全局变量的值,导致被误批改;一些和数据库连贯无关的单元测试也得思考这个全局变量。

那咱们如何对下面的程序做批改来解决以上问题呢?参考如下代码:

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}

通过这个函数来创立数据库连贯就能够解决以上3个问题了。

  • 错误处理能够交给createClient函数的调用者去治理,调用者能够抉择退出程序或者重试。
  • 单元测试既能够测试和数据库无关的根底函数,也能够测试createClient来查看数据库连贯的代码实现。
  • 没有裸露全局变量,数据库连贯实例在createClient函数外面创立和返回。

何时应用init函数

init函数也并不是齐全不倡议用,在有些场景下是能够思考应用的。比方Go的官网blog的源码实现就用到了init函数。

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)))}

这段源码里,init函数不可能失败,因为http.HandleFunc只有在第2个handler参数为nil的时候才会panic,显然这段程序里http.HandleFunc的第2个handler参数都是非法值,所以init函数不会失败。

同时,这里也无需创立全局变量,而且这个函数也不会影响单元测试。

因而这是一个适宜用init函数的场景示例。

总结

init函数要慎用,如果使用不当可能会带来问题,千万不要在代码里依赖同一package下不同.go文件init的执行程序。

最初回顾下Go语言init函数的注意事项:

  • init函数没有参数,没有返回值。如果加了参数或返回值,会编译报错。
  • 一个package上面的每个.go源文件都能够有本人的init函数。当这个package被import时,就会执行该package下的init函数。
  • 一个.go源文件里能够有一个或者多个init函数,尽管函数签名齐全一样,然而Go容许这么做。
  • .go源文件里的全局常量和变量会先被编译器解析,而后再执行init函数。

举荐浏览

  • Go面试题系列,看看你会几题?
  • Go常见谬误第1篇:未知枚举值
  • Go常见谬误第2篇:benchmark性能测试的坑
  • Go常见谬误第3篇:go指针的性能问题和内存逃逸
  • Go常见谬误第4篇:break操作的注意事项
  • Go常见谬误第5篇:Go语言Error治理
  • Go常见谬误第6篇:slice初始化常犯的谬误
  • Go常见谬误第7篇:不应用-race选项做并发竞争检测
  • Go常见谬误第8篇:并发编程中Context应用常见谬误
  • Go常见谬误第9篇:应用文件名称作为函数输出
  • Go常见谬误第10篇:Goroutine和循环变量一起应用的坑
  • Go常见谬误第11篇:意外的变量遮蔽(variable shadowing)
  • Go常见谬误第12篇:如何破解箭头型代码

开源地址

文章和示例代码开源在GitHub: Go语言高级、中级和高级教程。

公众号:coding进阶。关注公众号能够获取最新Go面试题和技术栈。

集体网站:Jincheng's Blog。

知乎:无忌。

福利

我为大家整顿了一份后端开发学习材料礼包,蕴含编程语言入门到进阶常识(Go、C++、Python)、后端开发技术栈、面试题等。

关注公众号「coding进阶」,发送音讯 backend 支付材料礼包,这份材料会不定期更新,退出我感觉有价值的材料。

发送音讯「进群」,和同行一起交流学习,答疑解惑。

References

  • https://livebook.manning.com/...
  • https://github.com/jincheng9/...