前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go常用错误集锦之误用init初始化函数

Go常用错误集锦之误用init初始化函数

作者头像
Go学堂
发布2023-01-31 15:30:52
5950
发布2023-01-31 15:30:52
举报
文章被收录于专栏:Go工具箱

init函数有时候会在Go应用程序中被误用。潜在的后果可能是错误管理不善或代码逻辑难以理解。

首先,我们将重新认识一下什么是init函数。然后,我们看看什么时候该使用init函数,什么时候不推荐使用。

1 概念

一个init函数是一个没有任何参数和返回值的函数(一个func()函数)。当一个包被初始化时,在包中所有声明的常量和变量都被初始化。然后,该init函数被执行。下面是一个main包的例子:

代码语言:javascript
复制
package main

import "fmt"

var a = func() int {
   fmt.Println("var") ①
   return 0
}()
func init() {
   fmt.Println("init") ②
}
func main() {
   fmt.Println("main") ③
}

① 首先被执行

② 第二执行

③ 最后执行

执行该例子将会有如下输出:

代码语言:javascript
复制
var
init
main

当一个包被初始化的时候,init函数就会被执行。在下面的例子中,我们定义了两个包,main和redis,其中main包依赖redis包。

代码语言:javascript
复制
package main

import (
  "fmt"
  "redis"
)

func init() {
  // ...
}

func main() {
  err := redis.Store("foo", "bar") ①
  // ...
}

① 依赖于redis包

代码语言:javascript
复制
package redis

import ...

func init() {
  // ...
}

func Store(key, value string) error {
  // ...
}

因为 main依赖于redis,所以会首先执行redis包的init函数,然后是main包的init函数,然后是main函数自身,如下图

我们在一个包中也可以定义很多init函数。在这种场景中,在同一个包里的init函数的执行顺序是依赖于源码里按字母顺序执行的。例如,如果一个包里包含一个a.go和一个b.go文件,两个文件里都有init函数,a.go中的init函数将先被执行。我们不应该依赖于同一个包中的init函数的执行顺序。实际上,如果源文件被重命名会影响init的执行顺序,这是会很危险的

我们也能在同一个文件中定义多个init函数。例如,下面的代码是非常合法的:

代码语言:javascript
复制
package main
import "fmt"

func init() { ①
   fmt.Println("init 1")
}
func init() { ②
   fmt.Println("init 2")
}
func main() {
}

① 该init会先执行

② 该init会后执行

首先定义的第一个init函数会被优先执行。

代码语言:javascript
复制
init 1
init 2

我们也可以使用init函数只对包进行初始化,但在main包中不使用该包。在下面的这个例子中,我们定义了一个main包,该包间接依赖于一个foo包(例如,一个公开函数的非直接调用)。然而,它包含foo包的初始化。我们可以使用 _ 操作符来进行初始化:

代码语言:javascript
复制
package main

import (
  "fmt"
  _ "foo" ①
)
func main() {
  //...
}

① 导入foo包以初始化该包,但不使用该包

在这个案例中,foo包将会在main之前进行初始化。因此,foo的init函数将会被执行。

需要注意的是,init函数是不能直接被调用的

代码语言:javascript
复制
package main

func init() {}

func main() {
  init() ①
}

① 不合法的引用

该代码将会产生如下编译错误:

代码语言:javascript
复制
$ go build .
./main.go:6:2:undefined:init

至此,我们回顾了init是如何工作的。接下来让我们看看我们该何时使用它,何时不该使用。

2 何时使用init函数

在下面的例子中,我们会创建一个SQL连接。我们将使用一个init函数并构造一个可用的连接作为全局变量以供后续使用。

代码语言:javascript
复制
var connection *sql.DB
func init() {
   dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME") ①
   c, err := sql.Open("mysql", dataSourceName)
   if err != nil {
       log.Panic(err)
  }
   err = connection.Ping()
   if err != nil {
       log.Panic(err)
  }
   connection = c ②
}

① 环境变量

② 将DB连接赋值给全局connection变量

在这段代码中,有三个主要的缺点。

第一,在init函数中的错误管理是非常受局限的。事实上,因为init函数不会有返回值,所以,如果遇到一些错误时我们才决定使用panic。如果在init函数中发生了panic,是不可能从错误中恢复的,同时该应用程序将会停止。在我们的例子中,如果创建一个连接是绝对必须的,那么遇到panic就停止是可以接受的。但是,是否停止应用程序不一定要由包本身来决定。也许,调用者更希望使用重试机制或使用回调技术。在init函数中进行错误处理阻止了客户端实现错误管理的逻辑处理。

第二,会使单元测试更复杂。如果我们在这个文件中加入了测试,init函数将会在执行测试用例之前执行,这不是我们所期望的。例如,我们可能希望在不需要创建此连接的映射函数上添加单元测试。所以,编写单元测试的方法会很复杂。

第三,是我们创建连接的方法需要一个全局变量。全局变量有一些严重的缺点,例如:

  • 它可以被包中的任何函数更改
  • 它会使单元测试变得更复杂,因为依赖于共享全局状态的函数不是纯函数。

在大多数场景中,我们更喜欢封装一个变量,而不是全局变量。

这是和init函数相关的主要缺点。那么,我们是不是就不使用它了呢?当然不是。也有一些场景是适合使用init函数的。例如,官方博客中所说的使用init函数来配置静态http的配置文件:

代码语言:javascript
复制
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会引发panic,但也只有在handler是nil,这里不是这种情况),也没有创建任何全局变量的需要,并且也不会影响单元测试。因此,这个就是一个非常适合用init函数的例子。

总之,我们已经知道init函数可能会导致一些缺点:

  • 错误管理是有局限性的
  • 对实现单元测试会很复杂(例如,外部依赖设置,对于单元测试来说这不是必须的)
  • 如果初始化需要设置一个状态,必须通过全局变量完成

我们必须小心使用init函数。它在一些场景下会很有用,例如定义静态配置;在大多数情况下,我们应该将初始化处理为特殊函数,使代码流更加明确。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 概念
  • 2 何时使用init函数
相关产品与服务
云数据库 Redis®
腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档