乐趣区

关于go:Go常见错误系列的第13篇init函数的常见错误和最佳实践

前言

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

var
init
0
main

全局变量 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/…
退出移动版