Go常见错误第13篇:init函数的常见错误和最佳实践
Contents
前言
这是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
我们来看如下的代码示例:
|
|
go run main.go
执行这段程序的结果是:
|
|
全局变量a
的定义虽然放在了最后面,但是先被编译器解析,然后执行init函数,最后执行main函数。
示例2
有2个package: main
和redis
,main
这个package依赖了redis
这个package。
|
|
|
|
因为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函数不能被直接调用,否则会编译报错。
1 2 3 4 5 6 7
package main func init() {} func main() { init() }
上面这段代码编译报错如下:
1 2
$ go build . ./main.go:6:2: undefined: init
到现在为止,大家对package里的init函数应该有了一个比较清晰的理解,接下来我们看看init函数的常见错误和最佳实践。
init函数的错误用法
我们先看看init函数一种常见的不太好的用法。
|
|
上面的程序做了如下几个事情:
- 创建一个数据库连接实例。
- 对数据库做ping检查。
- 如果连接数据库和ping检查都通过的话,会把数据库连接实例赋值给全局变量
db
。
大家可以先思考下这段程序会有哪些问题。
-
第一,init函数里面做错误管理的方式是很有限的。比如,init函数没法返回error,因为init函数是不能有返回值的。那如果init函数出现了error要让外界感知的话,得主动触发panic,让程序停止。对于上面的示例程序,虽然init函数遇到错误时,表示数据库连接失败,去停止程序运行或许是可以的。但是在init函数里去创建数据库连接,如果失败的话,就不好做重试或者容错处理。试想,如果是在一个普通函数里去创建数据库连接,那这个普通函数可以在创建数据库连接失败的时候返回error信息,然后函数的调用者来决定做重试或者退出的操作。
-
第二,会影响代码的单元测试。因为init函数在测试代码执行之前就会运行,如果我们仅仅是想测试这个package里某个不需要做数据库连接的基础函数,那测试的时候还是会执行init函数、去创建数据库连接,这显然并不是我们想要的效果,增加了单元测试的复杂性。
-
第三,这段程序把数据库连接赋值给了全局变量。用全局变量会有一些潜在的风险,比如这个package里的其它函数可以修改这个全局变量的值,导致被误修改;一些和数据库连接无关的单元测试也得考虑这个全局变量。
那我们如何对上面的程序做修改来解决以上问题呢?参考如下代码:
|
|
通过这个函数来创建数据库连接就可以解决以上3个问题了。
- 错误处理可以交给createClient函数的调用者去管理,调用者可以选择退出程序或者重试。
- 单元测试既可以测试和数据库无关的基础函数,也可以测试createClient来检查数据库连接的代码实现。
- 没有暴露全局变量,数据库连接实例在createClient函数里面创建和返回。
何时使用init函数
init函数也并不是完全不建议用,在有些场景下是可以考虑使用的。比如Go的官方blog的源码实现就用到了init函数。
|
|
这段源码里,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语言初级、中级和高级教程。
公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。
个人网站:Jincheng’s Blog。
知乎:无忌。
福利
我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。
关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。
发送消息「进群」,和同行一起交流学习,答疑解惑。