Go 的入口函数和包初始化的使用
作者:小菠萝测试笔记 发布时间:2024-05-10 13:57:12
包 package
Go 包是 Go 语言的基本组成单元,一个 Go 程序就是一组包的集合,所有 Go 代码都位于包中
Go 源码可以导入其他 Go 包,并使用其中的导出语法元素,包括类型、变量、函数、方法等,而且 main 函数是整个 Go 应用的入口函数
Go 语言提供了很多内置包,如 fmt、os、io 等
任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是
package 包名
语句,通过该语句声明源码文件所在的包
main.main 函数:Go 应用的入口函数
Go 语言中有一个特殊的函数:main 包中的 main 函数,也就是
main.main
,它是所有 Go 可执行程序的用户层执行逻辑的入口函数Go 程序在用户层面的执行逻辑,会在这个函数内按照它的调用顺序展开
package main
整个 Go 可执行程序中仅允许存在一个名为 main 的包
package main
想要引用别的包的代码,必须同样以包的方式进行引用
package main
func main() {
// 用户层执行逻辑
... ...
}
Go 语言要求:可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错
注意
main 包是不可以像标准库 fmt 包那样被导入(Import)的
其他包也可以拥有 main 函数或方法
按照 Go 的可见性规则(小写字母卡头的标识符为非导出标识符),非 main包中自定义的 main 函数仅限于包内使用
package pkg1
import "fmt"
func Main () {
main()
}
func main(){
fmt.Println("main func for pkg1")
}
重点
一个文件夹下的所有源码文件只能属于同一个包,不要求同名,但还是建议包名和所在目录同名,这样结构更清晰,包名中不能包含特殊符号
给结构定义的方法必须放在同一个包内,可以是不同文件
包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
一个文件夹下的所有源码文件只能属于同一个包,属于同一个包的源码文件不能放在多个文件夹下
引子
不过对于 main 包的main 函数来说,还需要明确一点,就是它虽然是用户层逻辑的入口函数,但它却不一定是用户层第一个被执行的函数。这是为什么呢?这跟 Go 语言的另一个函数 init 有关
init 函数:Go 包的初始化函数
和 main.main 函数一样,init 函数也是一个无参数无返回值的函数
func init() {
// 包初始化逻辑
... ...
}
Go 程序会在这个包初始化的时候,自动调用它的 init 函数,所以 init 函数的执行会发生在 main 函数之前
在 Go 程序中不能手工显式地调用 init,否则会收到编译错误
和 main 函数不一样
init 函数在一个包中可以有多个,每个 Go 源文件都可以定义多个 init 函数
init 函数的执行顺序
在初始化 Go 包时,Go 会按照一定的顺序,逐一、顺序地调用这个包的 init 函数
一般来说,先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文件中的多个 init 函数,会按声明顺序依次执行
Go 包的初始化次序
从程序逻辑结构角度来看,Go 包是程序逻辑封装的基本单元
每个包都可以理解为是一个“自治”的、封装良好的、对外部暴露有限接口的基本单元
一个 Go 程序就是由一组包组成的,程序的初始化就是这些包的初始化
每个 Go 包还会有自己的依赖包、常量、变量、init 函数(其中 main 包有 main 函数)等
三步走
依赖包按“深度优先”的次序进行初始化
每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化
包内的多个 init 函数按出现次序进行自动调用
init 函数的特点
如上图所示,执行顺位排在包内其他语法元素(常量、变量)的后面
每个 init 函数在整个 Go 程序生命周期内仅会被执行一次
init 函数是顺序执行的,只有当一个 init 函数执行完毕后,才会去执行下一个 init 函数
init 函数的用途
重置包级变量值
init 函数就好比 Go 包真正投入使用之前唯一的“质检员”,负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查
实现对包级变量的复杂初始化
有些包级变量需要一个比较复杂的初始化过程,有些时候,使用它的类型零值或通过简单初始化表达式不能满足业务逻辑要求,而 init 函数则非常适合完成此项工作,标准库 http 包中就有这样一个典型示例
package main
import (
"os"
"strings"
)
var (
http2VerboseLogs bool // 初始化默认值 false
http2logFrameWrites bool
http2logFrameReads bool
http2inTests bool
)
func init() {
e := os.Getenv("GODEBUG")
if strings.Contains(e, "http2debug=1") {
http2VerboseLogs = true // 在 init 中对 http2VerboseLogs 的值进行重置
}
if strings.Contains(e, "http2debug=2") {
http2logFrameWrites = true
http2logFrameReads = true
http2inTests = true
}
}
http 包在init 函数中,就根据环境变量 GODEBUG 的值,对这些包级开关变量进行了复杂的初始化,从而保证了这些开关变量在 http 包完成初始化后,可以处于合理状态
在 init 函数中实现“注册模式”
来看一段使用 lib/pq 包访问 PostgreSQL 数据库的代码 ??
package main
import (
"database/sql"
"log"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=ver)
if err != nil {
log.Fatal(err)
}
age := 21
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
}
复制代码
这里是以空导入_
的方式导入 lib/pq 包的,main 函数中没有使用 pq 包的任何变量、函数或方法,这样就实现了对 PostgreSQL数据库的访问
实际原因
在 pq 包的 conn.go 源码文件中的 init 函数
func init() {
sql.Register("postgres", &Driver{})
}
利用了空导入的特性,将 lib/pq 包作为 main 包的依赖包,在包初始化时,会先执行 lib/pq 包里面的 init 函数
pq 包的 init 函数将自己实现的 sql 驱动注册到了 sql 包中
这样在实际应用代码中,Open 数据库时,传入驱动名字(这里是 postgres),就能得到数据库实例,然后对数据库进行操作,实际上是因为调用了 pq 包中相应的驱动实现的
好处:这种通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动
工厂设计模式
从标准库 database/sql 包的角度来看,这种“注册模式”实质是一种工厂设计模式的实现,sql.Open
函数就是这个模式中的工厂方法,它根据外部传入的驱动名称“生产”出不同类别的数据库实例句柄
通过注册模式实现获取各种格式图片的宽、高
Go 源码
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)
}
来源:https://juejin.cn/post/7055106841545539592


猜你喜欢
- Golang中Array是值类型而slice是引用类型。因此两者之间的赋值或拷贝有些差异,本文带你了解各自的差异。1. 拷贝array前面提
- Matplotlib实现单画布绘制多个子图最近研究Python数据分析,需要利用Matplotlib绘制图表,并将多个图表绘制在一张图中,经
- <html><head>//搜索暂时没做,数据是出来了,但是却没法显示<link rel="styl
- 通过对 26 个字母的设定,设置自己要输出的字体。name = "RUNOOB"# 接收用户输入# name = inp
- 介绍一个利用Python监控当前联网状态情况的python代码,它可以清楚地知道,你的电脑网络是否是链接成功或失败,通俗的说,就是查看你的电
- 定义流的作用是使用统一的方式处理文件、网络和数据压缩等共用同一套函数和用法的操作。简单而言,流是具有流式行为的资源对象。因此,流可以线性读写
- 本文只是几年前学习的tkinter的时候写的测试程序,十分之简陋,只是学习用,没什么其他用处。学习一下莫烦Python的tkinter教程,
- #!/bin/sh#code by scpman#功能:检查并修复mysql数据库表#将此脚本加到定时中,脚本执行时,等会读库,列出要修复的
- 以下代码是基于python3.5.0编写的import pandasfood_info = pandas.read_csv("fo
- 一、前言在上一小节,我们介绍了文件的基本操作以及数据交换的格式,为了巩固我们上一节文件操作的知识。在这里我们做一个最基本的案例:用户登录二、
- 代码优化 for($i=0;$i<8;$i++){ array_push($week,$arr); } for($i=0;$i<
- 本文介绍我使用QQ得到服务器上回传的python代码的探索历程,面向的对象是对计算机网络有一定了解的读者。期待有兴趣的人和我一起探讨!需求来
- Math.min()和Math.max()用法相似。两个方法用来获取给定的一组数值中的最大值或最小值,但是却不接受数组作为参数。当然可以写个
- 本文实例为大家分享了javascript实现双端队列的具体代码,供大家参考,具体内容如下1.双端队列双端队列是一种允许我们同时从前端和后端添
- 一、首先从SQLServer中Error讲起,SQL中错误处理有些怪辟 错误级别同是16但结果都不同。select *
- 一、Requests库的安装利用 pip 安装,如果你安装了pip包(一款Python包管理工具,不知道可以百度哟),或者集成环境,比如Py
- 本文实例讲述了Python递归及尾递归优化操作。分享给大家供大家参考,具体如下:1、递归介绍递归简而言之就是自己调用自己。使用递归解决问题的
- 前言相信在日常生活中,平常大家聚在一起总会聊聊天,特别是女生(有冒犯到doge)非常喜欢聊星座,这个男生什么星座呀,那个男生什么星座呀…今天
- 本文实例讲述了PHP接口多继承及tarits实现多继承效果的方法。分享给大家供大家参考,具体如下:接口多继承在PHP的面向对象中,接口可以继
- 本文介绍了vue下history模式刷新后404错误解决方法,分享给大家,具体如下:官方说明文档:https://router.vuejs.