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

news2024/11/20 10:20:32

从程序逻辑结构角度来看,包(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 bool
	http2logFrameWrites bool
	http2logFrameReads bool
	http2inTests bool
)
func init() {
	e := os.Getenv("GODEBUG")
	if strings.Contains(e, "http2debug=1") {
	http2VerboseLogs = true
}
if strings.Contains(e, "http2debug=2") {
	http2VerboseLogs = true
	http2logFrameWrites = true
	http2logFrameReads = 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、GIF
	width, 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.coloradmin.cn/o/1000810.html

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

相关文章

胡焕庸线,我国东西地级市分布密度分界线

背景 黑河—腾冲线&#xff0c;又名胡焕庸线&#xff0c;是我国人口密度分布的东西近似分界线。今天把地级市坐标分布密度做成热力图&#xff0c;并与胡焕庸线一起展示时&#xff0c;惊奇的发现&#xff0c;胡焕庸线貌似也是我车东西地级市分布密度的分界线。 生成folium地图…

【17 > 分布式接口幂等性】2. Update的幂等性原理解析

一、 根据 唯一业务号去更新 数据的情况 1.1 原理 1.2 操作 1.3 实战 Stage 1&#xff1a;表添加 version 字段 Stage 2&#xff1a;前端 > 版本号放入隐藏域 Stage 3&#xff1a;后台 > 使用版本号作为更新条件 二、更新操作没有唯一业务号&#xff0c;可使用Tok…

FL Studio Producer Edition 21.0.3.3713中文完整破解版功能特点及安装激活教程

FL Studio Producer Edition 21.0.3.3713中文完整破解版是一款由Image Line公司研发几近完美的虚拟音乐工作站,同时也是知名的音乐制作软件。它让你的计算机就像是全功能的录音室&#xff0c;漂亮的大混音盘&#xff0c;先进的创作工具&#xff0c;让你的音乐突破想象力的限制。…

4.2 Ioc容器加载过程-Bean的生命周期深度剖析

Bean生命周期详解 第一步拿到父类BeanFactory子类 第二步&#xff0c;读取配置类 AnnotatedBeanDefinitionReader 用来读取配置类之外和还做了 第一个是解析类的处理器&#xff0c;没有的话我们的配置类就无法解析 总结this()无参构造函数里面实现了【三大步】 实例…

九安监控初始化后恢复案例

九安监控是国内一个十六线小安防品牌&#xff0c;目前CHS零壹视频恢复程序监控版、专业版、高级版是支持这个安防品牌的&#xff0c;不过下边这个案例比较特殊&#xff0c;具体情况如下。 故障存储:希捷4T监控专用硬盘 故障现象: 客户描述是使用了初始化操作&#xff0c;正常…

Linux中防火墙的简单使用方法

目录 前言 一、概念 1、防火墙的分类&#xff1a; 2、防火墙性能 3、硬件防火墙的品牌、软件防火墙的品牌 4、硬件防火墙与软件防火墙比较 二、linux中的防火墙 1、iptables 2.netfilter/iptables功能 3、四表 iptables中表的优先级 4、五链 三、iptables 1、ipt…

YOLO DNF辅助教程完结

课程完结&#xff01;撒花、撒花、撒花 课程完结&#xff01;撒花、撒花、撒花 课程完结&#xff01;撒花、撒花、撒花 ​呕心沥血三个月&#xff0c;《利用人工智能做DNF游戏辅助》系列实战课程已完结&#xff0c;技术路线贯穿串口通信、目标检测、opencv特征匹配等前沿技术…

计算机组装与维护实训室解决方案

一、产业背景 随着信息技术的不断发展&#xff0c;信息化已经深入到社会中各个层面&#xff0c;它能有效地帮助各个行业提高运营及管理效率&#xff0c;进而帮助企业成长。同时&#xff0c;作为企业信息化建设的基础部分&#xff0c;计算机已经融入了我们的生活&#xff0c;并逐…

【算法】一文带你从浅至深入门dp动态规划

文章目录 一、前言二、动态规划理论基础1、基本概念2、动态规划五部曲【✔】3、出错了如何排查&#xff1f; 三、实战演练&#x1f5e1;0x00 斐波那契数0x01 第N个泰波那契数0x02 爬楼梯0x03 三步问题0x04 使用最小花费爬楼梯⭐解法一解法二 0x05 解码方法* 四、总结与提炼 一、…

查看Android App包名,查看keystore的信息,导出公钥

查看Android App包名的三种方法 cmd中输入命令&#xff1a;adb shell am monitor 然后启动需要获取包名的应用cmd中输入命令&#xff1a;adb shell pm list packages -3 查看自己安装的app包名安卓系统中&#xff1a;设置——应用——正在运行 查看…

SQLI-labs-第七关

知识点&#xff1a;单引号&#xff08;&#xff09;加括号闭合错误的布尔盲注 思路&#xff1a; 寻找注入点 我们首先看一下正常的回显&#xff0c;并没有显示出什么明显的信息 输入?id1 发现报错 输入?id1 -- 还是报错&#xff0c;说明SQL语句的语法错误可能不是单引号闭合…

微信/QQ/TIM防撤回补丁

适用于 Windows 下 PC 版微信/QQ/TIM的防撤回补丁。支持最新版微信/QQ/TIM&#xff0c;其中微信能够选择安装多开功能。 下载地址&#xff1a; ⚡️点我下载最新版本 相关文档&#xff1a; ✔支持哪些版本 | ❓常见问题 | &#x1f4d6;查看完整文档 原理与方法&#xff1a; …

视频怎么转成漫画?分享些视频转漫画方法

在现代社会中&#xff0c;视频和漫画都是非常流行的媒介形式。而有时候&#xff0c;我们可能想将一些有趣的视频转化为漫画&#xff0c;以便在社交媒体上分享或进行其他用途。这篇文章将介绍几种视频转漫画的方法和注意事项。 使用转换工具 现在手机上有很多好用的工具&#x…

根据站点经纬度画全球分布图python

已知几个站点的经纬度,要求画出在全球的分布。 import pandas as pd import matplotlib.pyplot as plt import matplotlib as mpl import cartopy.crs as ccrs import cartopy.feature as cfeature from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter f…

Python练习分割字符串

str"itheima itcast boxuegu" # 统计字符串类有多少个“it”字符 count str.count("it") print(f"字符串类有{count}个“it”字符") # 将字符串内的空格全部替换为字符&#xff1a;“|” str_replace str.replace(" ", "|"…

正儿八经的雅思口语盘丝洞大法学习总结(长期修改更新)针对23.9月考生

目录 开篇语 李仙童口语大法 具体体系内容 说道科技产品或者说非传统物品 part2回答八大准则 【part2回答八大准则】&#xff08;一&#xff09; 【part2回答八大准则】&#xff08;二&#xff09; 【part3回答七大准则】&#xff08;一&#xff09; Part 1 核心体系 …

一文看懂Java的类加载机制

前言 当我们运行Java程序时&#xff0c;Java虚拟机&#xff08;JVM&#xff09;需要加载各种类文件&#xff0c;以执行程序中的代码。Java的类加载机制是Java语言的一个关键特性&#xff0c;它负责在运行时将类加载到内存中&#xff0c;并确保类的正确性。 类是在运行期间第一…

算法竞赛入门【码蹄集新手村600题】(MT1260-1280)C语言

算法竞赛入门【码蹄集新手村600题】(MT1260-1280&#xff09;C语言 目录MT1260 袋鼠躲猫猫MT1261 留下来的才是幸运数MT1262 约数MT1263 最大的三位约数MT1264 完数MT1265 区间完数MT1266 完数与因子MT1267 亏数MT1268 因数的因数MT1269 区间素数MT1270 素数计算MT1271 三生质数…

开开心心带你学习MySQL数据库之节尾篇

Java的JDBC编程 各种数据库,MySQL, Oracle, SQL Server在开发的时候,就会提供一组编程接口(API) API ~~ Application Programming Interface ~~ 应用程序编程接口 计算机领域里面的一个非常常见的概念, 给你个软件,你能对他干啥(从代码层次上的) 基于它提供的这些功能,就可以写…