从程序逻辑角度来看.包(package)是Go程序逻辑封装的基本单元.每个包都可以理解为一个自治的 封装良好的 对外暴露有限接口的基本单元.一个Go程序就是由一组包组成的.在Go中这一基本单元中分布这常量 包级变量 函数 类型和类型方法 接口等.要保证包内部的这些元素在被使用之前处于合理有效的初始状态.尤其是包级变量.在Go语言中一般通过init函数完成这一工作.
1.init函数:
在Go语言中有两个特殊的函数.一个是main包中的mian函数.它是所有Go可执行程序的入口函数.另一个就是init函数.
init函数是一个无参数 无返回值的函数.
func init(){
...
}
如果一个包定义了init函数.Go运行时会负责在该包初始化时调用它的init函数.在Go程序中不能显示调用init.否则会在编译期间报错.
func main() {
init()
}
func init() {
fmt.Println("init invoked")
}
一个包可以拥有多会init函数.每个组成Go包的Go源码文件中可以定义多个init函数.在初始化Go包时.Go运行会按照一定的次序逐一调用该包的init函数.Go运行时不会并发调用init函数.它会等待一个init函数执行完毕并返回后在执行下一个init函数.且每个init函数在整个Go程序生命周期内仅会被执行一次.因此.init函数极其适合做一些包级数据结构的初始化及初始化检查工作.
先被传递给Go编译器的源文件中的init函数先被执行.同一个源文件中的多个init函数按声明顺序依次被执行.但Go程序的管理告诉我们.不要依赖init函数的执行顺序.
2.程序初始化顺序:
init函数是顺序执行并仅被执行一次之外.Go程序初始化也给init函数提供了胜任该工作的前提条件.
Go程序是由一组包组合而成.程序的初始化就是这些包的初始化.每个Go包都会有自己的依赖包.每个包还包含有常量 变量 init函数等(其中main包有main函数).
1).main包直接依赖pkg1 pkg4两个包.
2).Go运行时会根据包导入的顺序.先去初始化main包的第一个依赖包pkg1.
3).Go会遵循深度优先原则查看pkg1依赖pkg2.于是Go运行时去初始化pkg2.
4).pkg2依赖pkg3.Go运行时去初始化pkg3.
5).pkg3没有依赖包.于是Go运行时在pkg3包中按照常量->变量->init函数的顺序进行初始化.
6).pkg3初始化完毕后.Go运行时会回到pkg2并对pkg2进行初始化.之后在回到pkg1并对pkg1进行初始化.
7).调用完pkg1的init函数后.Go运行时完成main包的第一个依赖包pkg1的初始化.
8).Go运行时接下来会初始化main包的第二个依赖包pkg4.
9).pkg4的初始化过程与pkg1类似.也是先初始化其依赖包pkg5.然后在初始化自身.
10).在Go运行时初始化完成pkg4后.也就完成了对main包的所有依赖包的初始化.接下来初始化main包自身.
11).在main包中.Go运行时会按照常量->变量->init函数的顺序进行初始化.执行完成这些初始化工作后才正式进入程序的入口函数main函数.
通过上面的流程可以知道init函数适合做包级数据的初始化及初始状态检查工作的前提是.init函数的执行顺位排在其所在包的包集变量之后.
示例:
main包:
import (
_ "gomodule/pkg1"
_ "gomodule/pkg3"
)
var (
_ = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)
const (
c1 = "c1"
c2 = "c2"
)
func constInitCheck() string {
if c1 != "" {
fmt.Println("main: const c1 init")
}
if c2 != "" {
fmt.Println("main: const c2 init")
}
return ""
}
func variableInit(name string) string {
fmt.Printf("main: variable %s init\n", name)
return name
}
func main() {
fmt.Println(v1)
fmt.Println(v2)
}
func init() {
fmt.Println("main init")
}
pkg1:
package pkg1
import "fmt"
var (
_ = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)
const (
c1 = "c1"
c2 = "c2"
)
func constInitCheck() string {
if c1 != "" {
fmt.Println("pkg1: const c1 init")
}
if c2 != "" {
fmt.Println("pkg1: const c2 init")
}
return ""
}
func variableInit(name string) string {
fmt.Printf("pkg1: variable %s init\n", name)
return name
}
func init() {
fmt.Println("pkg1 init")
}
pkg3:
package pkg3
import "fmt"
var (
_ = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)
const (
c1 = "c1"
c2 = "c2"
)
func constInitCheck() string {
if c1 != "" {
fmt.Println("pkg3: const c1 init")
}
if c2 != "" {
fmt.Println("pkg3: const c2 init")
}
return ""
}
func variableInit(name string) string {
fmt.Printf("pkg3: variable %s init\n", name)
return name
}
func init() {
fmt.Println("pkg3 init")
}
执行结果:
3.使用init函数检查包级变量的初始状态:
init函数就好比Go包真正投入使用之前的唯一"质检员".负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始化状态进行检查.
1).重置包级变量值:
flag包的init函数.源码位置:src/flag.flag.go
func init() {
// It's possible for execl to hand us an empty os.Args.
if len(os.Args) == 0 {
CommandLine = NewFlagSet("", ExitOnError)
} else {
CommandLine = NewFlagSet(os.Args[0], ExitOnError)
}
// Override generic FlagSet default Usage with call to global Usage.
// Note: This is not CommandLine.Usage = Usage,
// because we want any eventual call to use any updated value of Usage,
// not the value it has when this line is run.
CommandLine.Usage = commandLineUsage
}
CommandLine是flag包的一个导出包级变量.它也是默认情况下(如果没有新创建一个FlagSet)代表命令行的变量.
从初始化表达式可以看出.
如果参数值为空的话.就会创建一个默认的defaultUsage.如果不为空的话.就会把name值进行赋值.改变了输出.
源码位置:src/context/context.go
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
context包在cancelCtx的cancel方法中需要一个可复用的 处于关闭状态的channel.于是context包定义了一个未导出包级变量closedchan并对其进行了初始化.但初始化后的closedchan并不满足context包的要求.唯一能检查和更正其状态的地方就是context包的init函数.于是上面的代码在init函数中将closedchan关闭了.
2).对包级变量进行初始化.保证其后续可用.
有些包级变量的初始化过程较为复杂.简单的初始化表达式不能满足要求.而init函数则非常适合完成此项工作.
标准库regexp包的init函数就负责完成对内部特殊字节数组的初始化.这个特殊字节数组被包内的special函数使用.用于判断某个字符是否需要转义.
源码位置:src/regexp.regexp.go
var specialBytes [16]byte
// special reports whether byte b needs to be escaped by QuoteMeta.
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)
}
}
标准库http包在init函数中根据环境变量GODEBUG的值对一些包级开关变量进行赋值.
源码位置:src/net/http/h2_bundle.go
var (
http2VerboseLogs bool
http2logFrameWrites bool
http2logFrameReads bool
http2inTests bool
// Enabling extended CONNECT by causes browsers to attempt to use
// WebSockets-over-HTTP/2. This results in problems when the server's websocket
// package doesn't support extended CONNECT.
//
// Disable extended CONNECT by default for now.
//
// Issue #71128.
http2disableExtendedConnectProtocol = true
)
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
}
if strings.Contains(e, "http2xconnect=1") {
http2disableExtendedConnectProtocol = false
}
}
3).init函数中的注册模式:
package main
import (
"database/sql"
_ "github.com/lib/pq"
"log"
)
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 id = $1", age)
...
}
在以空别名方式导入lib/pg包后.main函数中似乎没有使用pq的任何变量 函数或方法.奥秘全在pg包的init函数中.
package pq
import (
"bufio"
"context"
"crypto/md5"
"crypto/sha256"
"database/sql"
"database/sql/driver"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"net"
"os"
"reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/lib/pq/internal/pgpass"
"github.com/lib/pq/internal/pqsql"
"github.com/lib/pq/internal/pqutil"
"github.com/lib/pq/internal/proto"
"github.com/lib/pq/oid"
"github.com/lib/pq/scram"
)
func init() {
sql.Register("postgres", &Driver{})
}
空别名导入方式的副作用就是Go运行时会将lib/pg作为main包的依赖包并会初始化pg包.于是pg包的init函数得以执行.在pg包的init函数中.pg包将自己实现的SQL驱动注册到sql包中.只要应用层代码在打开数据库的时候传入驱动的名字(这里是postgres).通过sql.Open函数返回的数据库实例句柄对应的就是pg这个驱动的相应实现.
这种在init函数中注册自己的实现的模式降低了Go包对外的直接暴露.尤其是包级别变量的暴露.避免了外部通过包级变量对包状态的改动.从database/sql的角度看.这种注册模式实质是一种工厂设计模式的实现.sql.Open函数就是该模式的工厂方法.它根据外部传入的驱动名称生产出不同类别的数据库实例句柄.
示例:
package main
import (
"errors"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
)
func main() {
width, height, err := imageSize("D:\\ChromeCoreDownloads\\csdn榜35.png")
if err != nil {
fmt.Println("get image size:", err)
return
}
fmt.Printf("image size: %dx%d\n", width, height)
}
func imageSize(imageFile string) (int, int, error) {
f, err := os.Open(imageFile)
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return 0, 0, err
}
bounds := img.Bounds()
return bounds.Max.X, bounds.Max.Y, nil
}
执行结果:
这个程序支持PNG JPEG GIF三种格式的图片.达成这一目标正是因为image/png image/jpeg和image/gif包在各自的init函数中将自己注册到image格式列表中.
源码位置:src/image/png/reader.go
func init() {
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
源码位置:src/image/gif/reader.go
func init() {
image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}
源码位置:src/image/jpeg/reader.go
func init() {
image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
4).init函数中检查失败的处理方法.
init函数是一个无参数 无返回值的函数.它的主要目的是保证其所在包被正式使用之前的初始状态是有效的.一旦init函数在检查包数据初始状态时遇到失败或错误的情况(尽管极少出现).则说明对包的质检亮了红灯.如果让包出厂.那么只会导致更为严重的影响.因此这种情况下.快速失败是最佳的选择.一般建议直接调用panic或者通过log.Fatal等函数记录日常日志.然后让程序快速退出.
白狼河北秋偏早,星桥又迎河鼓。清漏频移,微云欲湿,正是金风玉露。两眉愁聚。待归踏榆花,那里才诉。只恐重逢,明明相视更无语。
人间别离无数。向瓜果筵前,碧天凝伫。连理千花,相思一叶,毕竟随风何处。羁栖良苦,算未抵空房,冷香啼曙。今夜天孙,笑人愁似许。 纳兰
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路