无忌
2022/10/23阅读:67主题:默认主题
Go常见错误第13篇:init函数的常见错误和最佳实践
前言
这是Go常见错误系列的第13篇:init函数的常见错误和最佳实践。
素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi[1]。
本文涉及的源代码全部开源在:Go常见错误源代码[2],欢迎大家关注公众号,及时获取本系列最新更新。
常见错误和最佳实践
很多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: main
和redis
,main
这个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的源码实现[3]就用到了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函数。
推荐阅读
开源地址
文章和示例代码开源在GitHub: Go语言初级、中级和高级教程[4]。
公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。
个人网站:Jincheng's Blog[5]。
知乎:无忌[6]。
福利
我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。
关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。
发送消息「进群」,和同行一起交流学习,答疑解惑。
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
参考资料
Teiva Harsanyi: https://teivah.github.io/
[2]Go常见错误源代码: https://github.com/jincheng9/go-tutorial/tree/main/workspace/senior/p28
[3]源码实现: https://cs.opensource.google/go/x/website/+/e0d934b4:blog/blog.go;l=32
[4]Go语言初级、中级和高级教程: https://github.com/jincheng9/go-tutorial
[5]Jincheng's Blog: https://jincheng9.github.io/
[6]无忌: https://www.zhihu.com/people/thucuhkwuji
作者介绍