前言
这是 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: 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 的源码实现就用到了 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/…