河南网站建设服务,河南网站制作工作室,wordpress多功能博客,杭州做销售去哪个网站好从程序逻辑结构角度来看#xff0c;包#xff08;package#xff09;是Go程序逻辑封装的基本单元#xff0c;每个包都可以理解为一个“自治”的、封装良好的、对外部暴露有限接口的基本单元。一个Go程序就是由一组包组成的。 在Go包这一基本单元中分布着常量、包级变量、函… 从程序逻辑结构角度来看包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依赖pkg3Go运行时去初始化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检查包级变量的初始状态的例子。
重置包级变量值
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, http2debug1) {http2VerboseLogs true
}
if strings.Contains(e, http2debug2) {http2VerboseLogs truehttp2logFrameWrites truehttp2logFrameReads true}
}init函数中的注册模式
下面是使用lib/pq包 [1] 访问PostgreSQL数据库的一段代码示例
import (
database/sql
_ github.com/lib/pq
)
func main() {db, err : sql.Open(postgres, userpqgotest dbnamepqgotest sslmodeverify-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等函数记录异常日志然后让程序快速退出。