Go 基础15-在init()函数中检查包级变量的初始状态

从程序逻辑结构角度来看,包(package)是Go程序逻辑封装的基本单元,每个包都可以理解为一个“自治”的、封装良好的、对外部暴露有限接口的基本单元。一个Go程序就是由一组包组成的。

在Go包这一基本单元中分布着常量、包级变量、函数、类型和类型方法、接口等,我们要保证包内部的这些元素在被使用之前处于合理有效的初始状态,尤其是包级变量。在Go语言中,我们一般通过包的init函数来完成这一工作。

认识init函数

Go语言中有两个特殊的函数:一个是main包中的main函数,它是所有Go可执行程序的入口函数;另一个就是包的init函数。

init函数是一个无参数、无返回值的函数:
func init() {

}
如果一个包定义了init函数,Go运行时会负责在该包初始化时调用它的init函数。在Go程序中我们不能显式调用init,否则会在编译期间报错:

package main
import "fmt"
func init() {
fmt.Println("init invoked")
}
func main() {
init()
}

运行结果:

undefined: init

一个Go包可以拥有多个init函数,每个组成Go包的Go源文件中可以定义多个init函数。在初始化Go包时,Go运行时会按照一定的次序逐一调用该包的init函数。Go运行时不会并发调用init函数,它会等待一个init函数执行完毕并返回后再执行下一个init函数,
每个init函数在整个Go程序生命周期内仅会被执行一次。因此,init函数极其适合做一些包级数据的初始化及初始状态的检查工作。

一个包内的、分布在多个文件中的多个init函数的执行次序是什么样的呢?一般来说,先被传递给Go编译器的源文件中的init函数先被执行,同一个源文件中的多个init函数按声明顺序依次执行。但Go语言的惯例告诉我们:不要依赖init函数的执行次序

程序初始化顺序

init函数为何适合做包级数据的初始化及初始状态检查工作呢?除了init函数是顺序执行并仅被执行一次之外,Go程序初始化顺序也给init函数提供了胜任该工作的前提条件。

Go程序由一组包组合而成,程序的初始化就是这些包的初始化。每个Go包都会有自己的依赖包,每个包还包含有常量、变量、init函数等(其中main包有main函数),这些元素在程序初始化过程中的初始化顺序是什么样的呢?我们用下图来说明一下。

在这里插入图片描述

● main包直接依赖pkg1、pkg4两个包;

● Go运行时会根据包导入的顺序,先去初始化main包的第一个依赖包pkg1;

● Go运行时遵循“深度优先”原则查看到pkg1依赖pkg2,于是Go运行时去初始化pkg2;

● pkg2依赖pkg3,Go运行时去初始化pkg3;

● pkg3没有依赖包,于是Go运行时在pkg3包中按照常量→变量→init函数的顺序进行初始化;

● pkg3初始化完毕后,Go运行时会回到pkg2并对pkg2进行初始化,之后再回到pkg1并对pkg1进行初始化;

● 在调用完pkg1的init函数后,Go运行时完成main包的第一个依赖包pkg1的初始化;

● Go运行时接下来会初始化main包的第二个依赖包pkg4;

● pkg4的初始化过程与pkg1类似,也是先初始化其依赖包pkg5,然后再初始化自身;

● 在Go运行时初始化完pkg4后,也就完成了对main包所有依赖包的初始化,接下来初始化main包自身;

● 在main包中,Go运行时会按照常量→变量→init函数的顺序进行初始化,执行完这些初始化工作后才正式进入程序的入口函数main函数

到这里,我们知道了init函数适合做包级数据的初始化及初始状态检查工作的前提条件是,init函数的执行顺位排在其所在包的包级变量之后。

使用init函数检查包级变量的初始状态

init函数就好比Go包真正投入使用之前的唯一“质检员”,负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查。在Go运行时和标准库中,我们能发现很多init检查包级变量的初始状态的例子。

  1. 重置包级变量值
func init() {CommandLine.Usage = commandLineUsage
}

CommandLine是flag包的一个导出包级变量,它也是默认情况下(如果你没有新创建一个FlagSet)代表命令行的变量,我们从其初始化表达式即可看出:

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

CommandLine的Usage字段在NewFlagSet函数中被初始化为FlagSet实例(也就是CommandLine) 的 方 法 值 defaultUsage。 如 果 一 直 保 持 这 样, 那 么 使 用 Flag 默 认CommandLine的外部用户就无法自定义usage输出了。于是flag包在init函数中,将ComandLine的Usage字段设置为一个包内未导出函数commandLineUsage,后者则直接使用了
flag包的另一个导出包变量Usage。这样就通过init函数将CommandLine与包变量Usage关联在一起了。在用户将自定义usage赋值给Usage后,就相当于改变了CommandLine变量的Usage。

下面这个例子来自标准库的context包:

// closedchan是一个可重用的处于关闭状态的channel
var closedchan = make(chan struct{})
func init() {close(closedchan)
}

context包在cancelCtx的cancel方法中需要一个可复用的、处于关闭状态的channel,于是context包定义了一个未导出包级变量closedchan并对其进行了初始化。但初始化后的closedchan并不满足context包的要求,唯一能检查和更正其状态的地方就是context包的init函数,于是上面的代码在init函数中将closedchan关闭了。

对包级变量进行初始化,保证其后续可用

有些包级变量的初始化过程较为复杂,简单的初始化表达式不能满足要求,而init函数则非常适合完成此项工作。标准库regexp包的init函数就负责完成对内部特殊字节数组的初始化,这个特殊字节数组被包内的special函数使用,用于判断某个字符是否需要转义:

var specialBytes [16]byte
func special(b byte) bool {return b < utf8.RuneSelf && specialBytes[b%16]&(1<<(b/16)) != 0
}
func init() {for _, b := range []byte(`\.+*?()|[]{}^$`) {specialBytes[b%16] |= 1 << (b / 16)}
}

标准库net包在init函数中对rfc6724policyTable这个未导出包级变量进行反转排序:

func init() {sort.Sort(sort.Reverse(byMaskLength(rfc6724policyTable)))
}

标准库http包则在init函数中根据环境变量GODEBUG的值对一些包级开关变量进行赋值:

var (http2VerboseLogs boolhttp2logFrameWrites boolhttp2logFrameReads boolhttp2inTests bool
)
func init() {e := os.Getenv("GODEBUG")if strings.Contains(e, "http2debug=1") {http2VerboseLogs = true
}
if strings.Contains(e, "http2debug=2") {http2VerboseLogs = truehttp2logFrameWrites = truehttp2logFrameReads = true}
}
  1. init函数中的注册模式

下面是使用lib/pq包 [1] 访问PostgreSQL数据库的一段代码示例:

import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
if err != nil {log.Fatal(err)
}
age := 21
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
...
}

对于初学Go的Gopher来说,这是一段神奇的代码,因为在以空别名方式导入lib/pq包后,main函数中似乎并没有使用pq的任何变量、函数或方法。这段代码的奥秘全在pq包的init函数中:

// github.com/lib/pq/conn.go
...
func init() {sql.Register("postgres", &Driver{})
}
...

空别名方式导入lib/pq的副作用就是Go运行时会将lib/pq作为main包的依赖包并会初始化pq包,于是pq包的init函数得以执行。我们看到在pq包的init函数中,pq包将自己实现的SQL驱动(driver)注册到sql包中。这样,只要应用层代码在打开数据库的时候传入驱动的名字(这里是postgres),通过sql.Open函数返回的数据库实例句柄对应的就是pq这个驱动的相应实现。

这种在init函数中注册自己的实现的模式降低了Go包对外的直接暴露,尤其是包级变量的暴露,避免了外部通过包级变量对包状态的改动。从database/sql的角度来看,这种注册模式实质是一种工厂设计模式的实现,sql.Open函数就是该模式中的工厂方法,它根据外部传入的驱动名称生产出不同类别的数据库实例句柄。

这种注册模式在标准库的其他包中亦有广泛应用,比如,使用标准库image包获取各种格式的图片的宽和高。

package main
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
)
func main() {// 支持PNG、JPEG、GIFwidth, height, err := imageSize(os.Args[1])if err != nil {fmt.Println("get image size error:", err)return}fmt.Printf("image size: [%d, %d]\n", width, height)
}
func imageSize(imageFile string) (int, int, error) {f, _ := os.Open(imageFile)defer f.Close()img, _, err := image.Decode(f)if err != nil {return 0, 0, err
}
b := img.Bounds()
return b.Max.X, b.Max.Y, nil
}

这个程序支持PNG、JPEG和GIF三种格式的图片,而达成这一目标正是因为image/png、image/jpeg和image/gif包在各自的init函数中将自己注册到image的支持格式列表中了:

// $GOROOT/src/image/png/reader.go
func init() {image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
// $GOROOT/src/image/jpeg/reader.go
func init() {image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
// $GOROOT/src/image/gif/reader.go
func init() {image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}

4. init函数中检查失败的处理方法

init函数是一个无参数、无返回值的函数,它的主要目的是保证其所在包在被正式使用之前的初始状态是有效的。一旦init函数在检查包数据初始状态时遇到失败或错误的情况(尽管极少出现),则说明对包的“质检”亮了红灯,如果让包“出厂”,那么只会导致更为严重的影响。

因此,在这种情况下,快速失败是最佳选择。我们一般建议直接调用
panic或者通过log.Fatal等函数记录异常日志,然后让程序快速退出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/75322.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探究Vue3中的Composition API:优化组件逻辑的新利器

一、toRef函数 在 Vue 3.0 中&#xff0c;引入了一种新的响应式 API,即 toRef。toRef 函数可以将一个普通值转换为响应式引用类型&#xff0c;这样就可以在模板中直接使用这个响应式引用类型的属性&#xff0c;并且当该属性发生变化时&#xff0c;视图会自动更新。 <templat…

兵者多诡(HCTF2016)

环境:https://github.com/MartinxMax/CTFer_Zero_one 题目简介 解题过程 登录首页 提交png图片上传抓包&#xff0c;可以看到是向upload文件提交数据 在fp参数中尝试伪协议读取home.php文件 http://127.0.0.1:88/HCTF2016-LFI/home.php?fpphp://filter/readconvert.base64…

Mybatis的关系关联配置

前言 MyBatis是一个流行的Java持久化框架&#xff0c;它提供了一种简单而强大的方式来映射Java对象和关系数据库之间的数据。在MyBatis中&#xff0c;关系关联配置是一种用于定义对象之间关系的方式&#xff0c;它允许我们在查询数据库时同时获取相关联的对象。 在MyBatis中&…

第17章 站点构建

mini商城第17章 站点构建 一、课题 站点构建 二、回顾 1、Gateway限流 2、Nginx限流 3、Redis集群应用 4、缓存灾难处理 三、目标 1、Sentinel Sentinel介绍 Sentinel核心功能 Sentinel集成Gateway Sentinel控制台 2、Lvs+Nginx集群 Lvs负载均衡模式 NAT模式 TUN模式 …

实现在外网SSH远程访问内网树莓派的详细教程

文章目录 如何在局域网外SSH远程访问连接到家里的树莓派&#xff1f;如何通过 SSH 连接到树莓派步骤1. 在 Raspberry Pi 上启用 SSH步骤2. 查找树莓派的 IP 地址步骤3. SSH 到你的树莓派步骤 4. 在任何地点访问家中的树莓派4.1 安装 Cpolar4.2 cpolar进行token认证4.3 配置cpol…

HCIA自学笔记01-冲突域

共享式网络&#xff08;用同一根同轴电缆通信&#xff09;中可能会出现信号冲突现象。 如图是一个10BASE5以太网&#xff0c;每个主机都是用同一根同轴电缆来与其它主机进行通信&#xff0c;因此&#xff0c;这里的同轴电缆又被称为共享介质&#xff0c;相应的网络被称为共享介…

15:00面试,15:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到8月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%,…

算法通关村第12关【白银】| 字符串经典问题

一、反转问题 1.反转字符串 思路&#xff1a;双指针&#xff0c;反转数组一个套路 class Solution {public void reverseString(char[] s) {int l 0;int r s.length -1;while(l<r){char c s[l];s[l] s[r];s[r] c;l;r--;}} } 2.k个一组反转 思路&#xff1a;每k个进行…

第14章_瑞萨MCU零基础入门系列教程之QSPI

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

【leetcode 力扣刷题】删除字符串中的子串or字符以满足要求

删除字符串中的子串或者字符以满足题意要求 1234. 替换子串得到平衡字符串680. 验证回文串917. 仅仅反转字母 1234. 替换子串得到平衡字符串 题目链接&#xff1a;1234. 替换子串得到平衡字符串 题目内容&#xff1a; 题目中给出了平衡字符串的定义——只有’Q’&#xff0c;…

彻底掌握Protobuf编码原理与实战

目录 1.类型2.VARINT 2.1 无符号数2.2 有符号数3.定长 3.1 I64类型3.2 I32类型4.LEN5.代码 学习这些有什么用&#xff1f; - 如果你是后端开发者&#xff0c;掌握这个对工作非常有用 - 如果你是求职者&#xff0c;面试时可以临危不惧 1.类型 最近看到有直接操作wire type相关的…

React+antd实现可编辑单元格,非官网写法,不使用可编辑行和form验证

antd3以上的写法乍一看还挺复杂&#xff0c;自己写了个精简版 没用EditableRowCell的结构&#xff0c;也不使用Context、高阶组件等&#xff0c;不使用form验证 最终效果&#xff1a; class EditableCell extends React.Component {state {editing: false};toggleEdit () &…

系统软件启动过程

实验一&#xff1a;系统软件启动过程 参考 重要文件 调用顺序 1. boot/bootasm.S | bootasm.asm&#xff08;修改了名字&#xff0c;以便于彩色显示&#xff09;a. 开启A20 16位地址线 实现 20位地址访问 芯片版本兼容通过写 键盘控制器8042 的 64h端口 与 60h端口。b.…

Selenium自动化测试框架常见异常分析及解决方法

01 pycharm中导入selenium报错 现象: pycharm中输入from selenium import webdriver, selenium标红 原因1: pycharm使用的虚拟环境中没有安装selenium, 解决方法: 在pycharm中通过设置或terminal面板重新安装selenium 原因2: 当前项目下有selenium.py,和系统包名冲突导致, …

Amazon Aurora MySQL 和 Amazon RDS for MySQL 集群故障转移和只读实例扩容时间测试

01 测试背景 Amazon Aurora MySQL 是与 MySQL 兼容的关系数据库&#xff0c;专为云而打造&#xff0c;性能和可用性与商用数据库相当&#xff0c;成本只有其 1/10。 Amazon RDS for MySQL 让您能够在云中更轻松设置、操作和扩展 MySQL 部署。借助 Amazon RDS&#xff0c;您可以…

SpringBoot环境MongoDB分页+去重+获取去重后的原始数据

最近有个比较复杂的MongoDB查询需求&#xff0c; 要求1&#xff1a;获取最近订单表中的请求参数信息&#xff0c;并需要按照请求参数中的账号进行去重 要求2&#xff1a;数据量可能比较大&#xff0c;因此需要做分页查询 研究了大半天&#xff0c;终于搞出了解决方案&#xff0…

MySQL触发器详解保证入土

文章目录 简介一、MySQL触发器基础触发器分类基础常用关键字1. 定义触发器2. 创建和删除触发器3. 执行时机和条件 二、MySQL触发器的使用场景1. 数据完整性约束插入触发器更新触发器删除触发器 2. 数据变更日志的记录与追踪3. 触发器与存储过程的对比与选择 三、触发器的性能和…

C++学习笔记(重载、类)

C 1、函数重载2、类2.1、类的方法和属性2.2、类的方法的定义2.3、构造器和析构器2.4、基类与子类2.5、类的public、protected、private继承2.6、类的方法的重载2.7、子类方法的覆盖2.8、继承中的构造函数和析构函数 1、函数重载 函数重载大概可以理解为&#xff0c;定义两个名…

C语言实现三字棋

实现以下&#xff1a; 1游戏不退出&#xff0c;继续玩下一把&#xff08;循环&#xff09; 2应用多文件的形式完成 test.c. --测试游戏 game.c -游戏函数的实现 game.h -游戏函数的声明 (2)游戏再走的过程中要进行数据的存储&#xff0c;可以使用3*3的二维数组 char bor…

idea VCS配置多个远程仓库

Idea VCS配置多个远程仓库 首先要有两个或多个不同远程仓库地址 idea 添加数据源 查看推送记录 添加数据源 ok之后填写账号密码 推送本地项目 选择不同远程地址 push 查看不同远程地址的 不同分支的 推送记录 不期而遇的温柔&#xff1a; 应用开源架构进行项目开发&#xff0…