脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go 的入口函数和包初始化的使用

Go 的入口函数和包初始化的使用

2022-10-07 15:15小菠萝测试笔记 Golang

本文主要介绍了Go 的入口函数和包初始化的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

包 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想要引用别的包的代码,必须同样以包的方式进行引用
?
1
2
3
4
5
6
package main
 
func main() {   
    // 用户层执行逻辑
    ... ...
}

Go 语言要求:可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错

注意

main 包是不可以像标准库 fmt 包那样被导入(Import)的

其他包也可以拥有 main 函数或方法

按照 Go 的可见性规则(小写字母卡头的标识符为非导出标识符),非 main包中自定义的 main 函数仅限于包内使用

?
1
2
3
4
5
6
7
8
9
10
11
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 函数也是一个无参数无返回值的函数

?
1
2
3
4
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 函数)等

Go 的入口函数和包初始化的使用

三步走

  • 依赖包按“深度优先”的次序进行初始化
  • 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化
  • 包内的多个 init 函数按出现次序进行自动调用

init 函数的特点

  • 如上图所示,执行顺位排在包内其他语法元素(常量、变量)的后面
  • 每个 init 函数在整个 Go 程序生命周期内仅会被执行一次
  • init 函数是顺序执行的,只有当一个 init 函数执行完毕后,才会去执行下一个 init 函数

init 函数的用途

重置包级变量值

init 函数就好比 Go 包真正投入使用之前唯一的“质检员”,负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查

实现对包级变量的复杂初始化

有些包级变量需要一个比较复杂的初始化过程,有些时候,使用它的类型零值或通过简单初始化表达式不能满足业务逻辑要求,而 init 函数则非常适合完成此项工作,标准库 http 包中就有这样一个典型示例

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 数据库的代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 函数

?
1
2
3
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 源码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 的支持格式列表中了
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// $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)
}

到此这篇关于Go 的入口函数和包初始化的使用的文章就介绍到这了,更多相关Go 入口函数和包初始化内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7055106841545539592

延伸 · 阅读

精彩推荐
  • GolangGO常见的错误99%程序员会遇到(解决方法)

    GO常见的错误99%程序员会遇到(解决方法)

    这篇文章主要介绍了GO常见的错误99%程序员会遇到,本文给出了解决方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下 ...

    A_文艺钦年 ·4162020-06-02
  • GolangGo 的零值有什么用?看看这四个场景

    Go 的零值有什么用?看看这四个场景

    零值,在 Go 里虽然是一个缺省值。但它本质上不仅仅是一个 “缺省”,它还在 Go 程序里的许多应用场景提供了不少的便捷特性,甚至是隐晦的功能。...

    脑子进煎鱼了7682022-09-01
  • GolangC语言的10大基础算法

    C语言的10大基础算法

    算法是一个程序和软件的灵魂,作为一名优秀的程序员,只有对一些基础的算法有着全面的掌握,才会在设计程序和编写代码的过程中显得得心应手。这篇...

    翻斗街扛把子胡图图3492020-05-28
  • Golanggo语言区块链学习调用以太坊

    go语言区块链学习调用以太坊

    这篇文章主要为大家介绍了go语言区块链学习如何调用以太坊的示例实现过程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步...

    小生凡一12052021-11-26
  • Golang手把手教你Golang的协程池设计

    手把手教你Golang的协程池设计

    在很多公司都在陆续的搭建golang的语言栈,大家有没有想过为什么会出现这种情况?今天咱们就来一步一步的揭开协程池的面纱,如果你没有接触过go的协程...

    程序员小饭3822021-06-08
  • GolangGolang Cron 定时任务的实现示例

    Golang Cron 定时任务的实现示例

    这篇文章主要介绍了Golang Cron 定时任务的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下...

    jihite13092020-06-09
  • GolangGo语言中的闭包详解

    Go语言中的闭包详解

    本文详细讲解了Go语言中的闭包,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...

    奋斗的大橙子7892022-07-16
  • GolangGo 切片导致内存泄露,被坑两次了!

    Go 切片导致内存泄露,被坑两次了!

    在业务代码的编写上,我们经常会接受来自外部的接口数据,再把他插入到对应的数据结构中去,再进行下一步的业务聚合、裁剪、封装、处理。...

    脑子进煎鱼了7902021-10-09