返回顶部
首页 > 资讯 > 后端开发 > GO >为什么不建议在go项目中使用init()
  • 329
分享到

为什么不建议在go项目中使用init()

GOinit 2022-06-07 20:06:17 329人浏览 薄情痞子
摘要

前言 Go的 init函数给人的感觉怪怪的,我想不明白聪明的 google团队为何要设计出这么一个“鸡肋“的机制。实际编码中,我主张尽量不要使用init函数。 首先来看看 in

前言

Go
init
函数给人的感觉怪怪的,我想不明白聪明的
google
团队为何要设计出这么一个“鸡肋“的机制。实际编码中,我主张尽量不要使用
init
函数。

首先来看看

init
函数的作用吧。

init() 介绍

init()
与包的初始化顺序息息相关,所以先介绍一个
go
中包的初始化顺序吧。(下面的内容部分摘自《The go programinng language》)

大体而言,顺序如下:

首先初始化包内声明的变量

之后调用

init
函数

最后调用

main
函数

变量的初始化顺序

变量的初始化顺序由他们的依赖关系决定

应该任何强类型语言都是这样子吧。

例如:


var a = b + c;
var b = f();// 需要调用 f() 函数
var c = 1
func f() int{return c + 1;}

a
依赖
b
c
b
依赖
f()
f()
依赖
c
。因此,他们的初始化顺序理所当然是
c -> b -> a

graph TB; b-->a c-->a f-->b c-->b

Ps:其实在这里可能引申出一个没用的小技巧。当你有一个函数需要在包被初始化的过程中被调用时,你可以把这个函数赋值给一个包级变量。这样,当包被初始化时就会自动调用这个函数了,这个函数甚至能够在

init()
之前被调用!不过话说回来,它既然比
init()
更早被调用,那它才是真正的
init()
才对;此外你也可以在
init()
中调用该函数,这样才更合理一些。


// 笨版
// 函数必须得有一个返回值才行
var _ = func() interface{} {
fmt.Println("hello")
return nil
}()
func init() {
fmt.Println("world")
}
func main() {
}
// Output:
// hello
// world

// 更合理的版本
func init() {
fmt.Println("hello")
fmt.Println("world")
}
func main() {
}
// Output:
// hello
// world
包内变量的初始化顺序

一个包内往往有多个

go
文件,这么
go
文件的初始化顺序由它们被提交给编译器的顺序决定,顺序和这些文件的名字有关。

init()

主角出场了。先来看看它的设计动机吧:

Each variable declared at package level starts life with the value of its initializer expression, if any, but for some variables, like tables of data,an initializer expression may not be the simplest way to set its initial value.In that case,the init function mechanism may be simpler. 《The go pragramming language P44》

这句话的意思是有的包级变量没办法用一条简单的表达式来初始化,这个 时候,

init
机制就派上用场了。

init()
不能被调用,也不能被 reference,它们会在程序启动时自动执行。

同一个 go 文件中 init 函数的调用顺序

一个包内,甚至

go
文件内可以包含多个
init()
,同一个
go
文件中的
init()
调用顺序由他们的声明顺序决定 。


func init() {
fmt.Print("a")
}
func init() {
fmt.Print("b")
}
func init() {
fmt.Print("c")
}
// Output
// abc

同一个包下面不同

go
文件中
init()
的调用顺序

依旧是由它们的声明顺序决定,同一个包下面的所有

go
文件在编译时会被编译器合并成一个“大的
go
文件“(并不是真正合并,仅仅是效果类似而已)。合并的顺序由编译器决定。

不要把程序是否能够正常工作寄托在

init()
能够按照你期待的顺序被调用上。

不过话说回来,正经人谁在一个包里写很多

init()
呀,而且还把这些
init()
放在不同文件里,更可恶的是每个文件里还有多个
init()
。要是看到这样的代码,我立马:@#$%^&*...balabala...

一个包里最多写一个init()(我甚至觉得最好连一个

init()
都不要有)

不同包内 init 函数的调用顺序

唯独这个顺序,我们程序员是绝对可控的。它们的调用顺序由包之间的依赖关系决定。假设

a
包需要
import
b
包,
b
包需要
import
c
包,那么很显然他们的调用顺序是,
c
包的
init()
最先被调用,其次是
b
包,最后是
a
包。

graph LRc-->bb-->a

一个包的init函数最多会被调用一次

道理类似于一个变量最多会被初始化一次。

有的同学会问,一个变量明明可以多次赋值呀,可第二次对这个变量赋值那还能够叫初始化么?

例如有如下的包结构,

B
包和
C
包都分别
import
A
包,D包需要
import
B
包和
C
包。

graph TD; A-->B A-->C B-->D C-->D

A
包中有
init()


func init() {
fmt.Println("hello world")
}

D
包是
main
包,最终程序只输出了一句
hello world

我不喜欢 init 函数的原因

我不喜欢

init
函数的一个重要原因是,它会隐藏掉程序的一些细节,它会在没有经过你同意的情况下,偷偷干一些事情。
go
的函数王国里,所有的函数都需要程序员显示的调用(Call)才会被执行,只有它——
init()
,是个例如,你明明没 Call 它,它却偷偷执行了。

有的同学会说,

c++
里类的构造函数也是在对象被创建时就会默默执行呀。确实是这样,但在
c++
里,当你点进这个类的定义时,你就能立马看到它的构造函数和析构函数。在
go
里,当你点进某个包时,你能立马看到包内的
init()
么?这个包有没有
init()
以及有几个
init()
完全是个未知数,你需要在包内的所有文件中搜索
init()
这个关键字才能摸清包的
init()
情况,而大多数人包括我懒得费这个功夫。在
c++
中创建对象时,程序员能够很清楚的意识到这个操作会触发这个类的构造函数,这个构造函数的内容也能很快找到;但在
go
中,
import
包时,一切却没那么清晰了。

希望将来 goland 或者 vscode 能够分析包内的

init()
情况,这样我对
init()
的恶意会减半。

init()
项目维护带来的困难

当你看到这样的

import
代码时


import(
_ "pkg"
)

你立马能够知道,这个

import
的目的就是调用
pkg
包的
int()

当看到


import(
"pkg"
)

你却很难知道,

pkg
包里藏着一个
init()
,它被偷偷调用了。

但这还好,你起码知道如果

pkg
包有
init()
的话,它会在此处被调用。

但当

pkg
包,被多个包
import
时,
pkg
包内的
init()
何时被调用的,就是一个谜了。你得搞清楚这些包之间的
import
先后顺序关系,这是一场噩梦。

使用

init()
的时机

先说一下我的结论:我认为

init()
应该仅被用来初始化包内变量。

《The go programming language》提供了一个使用

init
函数的例子。


// pc[i] 是 i 中 bit = 1 的数量
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// 返回 x 中等于 1 的 bit 的数量
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}

PopCount
函数的作用数计算数字中等于
1
bit
的数量。例如 :


var i uint64 = 2

变量 i 的二进制表示形式为

0000000000000000000000000000000000000000000000000000000000000010

把它传入

PopCount
最终得到的结果将为
1
,因为它只有一个
bit
的值为
1

pc
是一个表,它的 index 为 x,其中 0 <= x <= 255,value 为 x 中等于 1 的 bit 的数量。

它的初始化思想是:

如果一个数x最后的 bit 为 1,那么这个数值为 1 的bit数 = x/2 的值为1的bit数 + 1;

如果一个数x最后的 bit 为 0,那么这个数值为 1 的bit数 = x/2 的值为1的bit数;

PopCount
中把一个 8byte 数拆成了 8 个单 byte 数,分别计算这8个单 byte 数中
bit
1
的数量,最后累加即可。

这里

pc
的初始化确实比较复杂,无法直接用


var pc = []byte{0, 1, 1,...}

这种形式给出。

一个可以替代

init()
的方法是:


var pc = generatePc()
func generatePc() [256]byte {
var localPc [256]byte
for i := range localPc {
localPc[i] = localPc[i/2] + byte(i&1)
}
return localPc
}

我觉得这样子初始化比利用

init()
初始化要更好,因为你可以立马知道
pc
是怎样得来的,而利用
init()
时,你需要利用 ide 来查找
pc
的 write reference,之后才能知道,哦,原来它(
pc
)来这里(
init()
) 被初始化了呀。

当包内有多个变量的初始化流程比较复杂时,可能会写出如下代码。


var pc = generatePc()
var pc2 = generatePc2()
var pc3 = generatePc3()
// ...

有的同学可能不太喜欢这种写法,那么用上

init()
后,会写成这样


func init() {
initPc()
initPc2()
initPc3()
}

我觉得两种写法都说的过去吧,虽然我个人更倾向第一种写法。

使用

init()
的时机,仅仅有一个例外,后面说。

不使用 init 函数的时机

init()
除了初始化变量,不应该干其他任何事!

有两个原则:

一个包的

init()
不应该依赖包外的环境

一个包的

init()
不应该对包外的环境造成影响

设置这两个原则的理由是:任何对外部有依赖或者对外部有影响的代码都有义务显式的让程序员知晓,不应该自己悄咪咪地去做,最好是显式地让程序员自己去调用。

init()
的活动范围就应该仅仅被局限在包内,自己和自己玩,不要影响了其他小朋友的游戏体验。

如下几条行为就踩了红线:

读取配置(依赖于外部的配置文件,且一般读取配置得到的 obj 会被其他包访问,违反了第一条和第二条)

注册路由(因为修改了

Http
包中的 routeMap,会对
http
包造成影响,违反了第二条)

连接数据库(连接数据库后一般会得到一个 db 对象给业务层去curd吧?违反了第二条)

etc... 我暂时只能想到这么多了

一个反面教材 https://GitHub.com/go-sql-driver/Mysql

反面教材就是:https://github.com/go-sql-driver/mysql 这个大名鼎鼎的包

当使用这个包时,一个必不可少的语句是:


import (
_ "github.com/go-sql-driver/mysql"
)

原因是它里面有个

init
函数,会把自己注册到 sql 包里。


func init() {
sql.ReGISter("mysql", &MySQLDriver{})
}

按照之前的标准,此处明显不符合规范,因为它影响了标准库的

sql
包。

我认为一个更好的方法是,创建一个

export
的专门用来做初始化工作的方法:


// Package mysql
func Init() {
sql.Register("mysql", &MySQLDriver{})
}

然后在

main
包中显式的调用它:


// Package main
func main(){
    mysql.Init();
    // other logic
}

来比较一下两种方式吧。

使用

Init()

是否需要告诉开发者额外的信息?

需要。

需要告诉开发者:使用这个库时,记得一定要调用 Init() 哦,我在里面做了一些工作。

开发者,点进

Init()
,瞬间了然。

是否能够阻止开发者不正确的调用?

不能。

因为是

export
的,所以开发者可以想到哪儿调用就到哪儿调用,想调用多少次就调用多少次。

因此需要额外告诉开发者:请您务必只调用一次,之后就不要调用了。且必须在用到

sql
包之前调用,一般而言都是在
main()
的第一句调用。

使用

init()

是否需要告诉开发者额外的信息?

需要

依旧需要告诉开发者,一定要用

_ "github.com/go-sql-driver/mysql"
这个语句显式的导入包哦,因为我利用
init()
在里面做一些工作。

开发者:那你做了什么工作

库:亲,请您点进

mysql
包,在目录下搜索
init()
关键字,慢慢找哦。

开发者:......

是否能够阻止开发者不正确的调用?

勉强可以吧。

因为

init()
只会被调用一次,不可能被调用多次,这从根本上杜绝了开发者调用多次的可能性。

可你管不了开发者的

import
时机,假设开发者在其他地方
import
了,导致你在
sql.Open()
时,
mysql
driver
没有被正常注册,你还是拿开发者没有办法。只能哀叹一声:我累了,毁灭吧。

我觉得作为库的提供者,最主要的是提供完善的机制,在用户使用你的库时,能利用你提供的机制,写出无bug 的代码。而不是像保姆一样,想方设法避免用户出错。

所以可能使用

init()
为了的优势就是减少了代码量吧。

使用

Init()
时,需要两句代码


import (
"github.com/go-sql-driver/mysql"// 这句
)
func main(){
    mysql.Init()  // 这句
}

但是使用

init
时,却只需要一句代码


import (
_ "github.com/go-sql-driver/mysql"// 这句
)

oh yeah,足足少写了一句代码!

一个例外 单元测试

可能使用

init
的唯一例外就是写单元测试的时候了吧。

假设我现在需要需要对

dao
层的增删改查逻辑的写一个单元测试。


func TestCURDPlayer(t *testing.T) {
// 测试 curd 玩家信息
}
func TestCURDStore(t *testing.T) {
// 测试 curd 商店信息
}
func TestCURDMail(t *testing.T) {
// 测试 curd 邮件信息
}

很显然,这些测试都是依赖数据库的,因此为了正常的测试,必须初始化数据库


func TestCURDPlayer(t *testing.T) {
// 测试 curd 玩家信息
    initdb()
    // balabala
}
func TestCURDStore(t *testing.T) {
// 测试 curd 商店信息
    initdb()
    // balabala
}
func TestCURDMail(t *testing.T) {
// 测试 curd 邮件信息
    initdb()
    // balabala
}
func initdb(){
    // sql.Open()...
}

难道我每次新增一个单元测试,都要在单元测试的代码中加一个

initdb()
么,这也太麻烦了吧。

这个时候

init()
就派上用场了。可以这样


func TestCURDPlayer(t *testing.T) {
// 测试 curd 玩家信息
    // balabala
}
func TestCURDStore(t *testing.T) {
// 测试 curd 商店信息
    // balabala
}
func TestCURDMail(t *testing.T) {
// 测试 curd 邮件信息
    // balabala
}
func init(){
    initdb()
}
func initdb(){
    // sql.Open()...
}

这样,当对这个文件进行单元测试时,可以确保在执行每个

TestXXX
函数时,
db
肯定是被正确初始化了的。

那为什么这个地方可以利用

init()
来初始化数据库呢?

理由之一是它的影响范围很小,仅仅在

xxx_test.go
文件中生效,在
go run
时不会起作用,在
go test
时才会起作用。

理由之二是我懒。。。

总结

init
更像是一个语法糖,它会让开发者对代码的追踪能力变弱,所以能不用就最好不用。

到此这篇关于为什么不建议在go项目中使用用init()的文章就介绍到这了,更多相关go init内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!


您可能感兴趣的文档:

--结束END--

本文标题: 为什么不建议在go项目中使用init()

本文链接: https://lsjlt.com/news/30672.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 为什么不建议在go项目中使用init()
    前言 go的 init函数给人的感觉怪怪的,我想不明白聪明的 google团队为何要设计出这么一个“鸡肋“的机制。实际编码中,我主张尽量不要使用init函数。 首先来看看 in...
    99+
    2022-06-07
    GO init
  • 为什么在MySQL中不建议使用UTF-8
    最近我遇到了一个 bug,我试着通过 Rails 在以“utf8”编码的 MariaDB 中保存一个 UTF-8 字符串,然后出现了一个离奇的错误: Incorrect string value: ‘\xF0\x9...
    99+
    2022-05-16
    MySQL UTF-8
  • 为什么不建议在 MySQL 中使用 UTF-8?
    最近我遇到了一个bug,我试着通过Rails在以“utf8”编码的MariaDB中保存一个UTF-8字符串,然后出现了一个离奇的错误:Incorrect string value:&nb...
    99+
    2024-04-02
  • 在MySQL中为何不建议使用utf8
    目录何为字符集?有哪些常见的字符集?ASCIIGB2312GBKGB18030BIG5Unicode & UTF-8 编码mysql 字符集MySQL 字符编码集中有两套 UTF-8 编码实现:utf8 和 ut...
    99+
    2024-04-02
  • 为什么MySQL不建议使用SELECT *
    目录1. 不必要的磁盘I/O2. 加重网络时延3. 无法使用覆盖索引4. 可能拖慢JOIN连接查询“不要使用SELECT *”几乎已经成为了MySQL...
    99+
    2024-04-02
  • 为什么不建议使用DiskFileUpload类型
    不建议使用DiskFileUpload类型的主要原因是它会将文件保存到临时目录中,而临时目录可能会被清理或定期清除。这就意味着在某些...
    99+
    2023-08-08
    DiskFileUpload
  • 怎么在Goland中使用Go Modules创建项目
    这篇文章将为大家详细讲解有关怎么在Goland中使用Go Modules创建项目,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。创建项目Location:新项目文件夹GOROOT:go 安装根...
    99+
    2023-06-14
  • 为什么vue中不建议使用空字符串作为className
    目录比较空字符串''和null情况1:使用空字符串''情况2:使用null情况3:使用undefined使用对象的形式绑定class使用 &&绑定class案例1:i...
    99+
    2024-04-02
  • 为什么MySQL不建议使用NULL作为列默认值?
    1 前言 NULL值是一种对列的特殊约束,我们创建一个新列时,如果没有明确的使用关键字not null声明该数据列,Mysql会默认的为我们添加上NULL约束. 有些开发人员在创建数据表时,由于懒惰直...
    99+
    2023-09-10
    mysql 数据库
  • 为什么建议使用Java枚举
    这篇文章主要介绍“为什么建议使用Java枚举”,在日常操作中,相信很多人在为什么建议使用Java枚举问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”为什么建议使用Java枚举”的疑惑有所帮助!接下来,请跟着小编...
    99+
    2023-06-16
  • 如何使用 Spring 框架在 Go 项目中建立索引?
    Spring框架是一个非常流行的Java框架,它提供了很多强大的工具和功能,可以帮助开发者快速构建高效的Java应用程序。但是,在一些Go项目中,我们也想使用Spring框架来建立索引,这该怎么做呢?下面,我将介绍如何使用Spring框架在...
    99+
    2023-10-19
    索引 教程 spring
  • 为什么不建议使用Java自定义Object作为HashMap的key
    目录前言踩坑历程回顾hashCode覆写的讲究为什么hashCode和equals要同时覆写数据退出机制的兜底总结前言 此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满...
    99+
    2024-04-02
  • MySQL为什么不建议用UUID做innodb主键
    这篇文章将为大家详细讲解有关MySQL为什么不建议用UUID做innodb主键,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。 1).UUID...
    99+
    2024-04-02
  • MySQL中不建议使用SELECT *的原因是什么
    本篇内容介绍了“MySQL中不建议使用SELECT *的原因是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!“不要使用...
    99+
    2023-06-29
  • 为什么建议大家使用Linux开发
    为什么建议大家使用Linux开发,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。 Linux 能用吗?我身边还有些朋友对 linux 的印象似乎还停留在黑乎乎的命令行界...
    99+
    2023-06-15
  • 为什么要在Java项目中使用Numy作为日志记录工具?
    在Java项目中使用Numy作为日志记录工具有很多好处。Numy是一种基于日志的分析工具,可以帮助开发人员更好地监控和分析应用程序的行为和性能。 首先,Numy可以帮助开发人员更好地了解应用程序的运行情况。通过记录应用程序的日志信息,Num...
    99+
    2023-10-07
    日志 npm numy
  • 美团面试:为什么MySQL不建议使用NULL作为列默认值?
    今天给大家分享一道美团高频面试题,“为什么 MySQL 不建议使用 NULL 作为列默认值?”。 对于这个问题,通常能听到的答案是 使用了 NULL 值的列将会使索引失效,但是如果实际测试过一下,你就...
    99+
    2023-09-11
    面试 mysql adb
  • 在Go语言项目中怎么使用Zap日志库
    本篇内容介绍了“在Go语言项目中怎么使用Zap日志库”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在Go语言项目中使用Zap日志库介绍在许多...
    99+
    2023-06-30
  • Git和Go语言:如何在项目中使用?
    Git是目前最流行的版本控制工具之一,而Go语言则是一种快速、高效、可靠的编程语言。在实际项目开发中,使用Git和Go语言可以大大提高开发效率和代码质量。那么如何在项目中使用Git和Go语言呢?本文将为您详细介绍。 一、Git入门 Git...
    99+
    2023-10-14
    对象 http git
  • web worker在项目中的使用学习为项目增加亮点
    目录引言为什么JavaScript是单线程?什么是Web Worker?小试牛刀在单页面应用中使用注意事项小结引言 平时小伙伴们不是说日常的项目开发中,都是单纯的搬砖,没啥亮点嘛,那...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作