1. 程序启动
1.1 入口函数
Go程序的入口函数,是 main 包中的 main 函数,一般定义在项目根目录的 main.go 文件中,它是必须定义的,否则项目将无法编译运行。main 函数没有参数和返回值。
我们将代码逻辑通过 main 函数进行调用。
package main
import "fmt"
func main() {
fmt.Println("hello")
}
1.2 导入包
在程序中,我们通常会导入不同的包,来实现不同的功能。
包的搜索路径
对于 import 语句导入的包,会先去 GOROOT 搜索对应的包,没有则再去 GOPATH/src 中的每个路径搜索,还是没有则会编译错误。
以如下路径为例,编译器将先搜索 /usr/lib/golang,没有则搜索 /root/go/src。
go env GOROOT
# /usr/lib/golang
go env GOPATH
# /root/go
包的导入过程
在编译 Go 程序时,编译器从 main 包开始,找到 import 语句并依次导入对应的包,如果这些包本身又有导入的包,则会递归地继续导入它所 import 的包。这其中重复的包只会导入一次。导入顺序如下示意图:
导入一个包将会完成以下事情:完成导入当前包所 import 的其它包、初始化常量、初始化变量、调用包的 init 函数。而执行 main 函数则会完成以下事情:完成导入 main 包所 import 的包、初始化常量、初始化变量、调用 main 包的 init 函数,调用 main 函数。
1.3 init函数
init 函数是定义在一个包中的初始化函数,没有参数和返回值。从包的导入过程可以看出,程序的 init 函数将在 main 函数之前执行,而各个导入包的 init 函数将会按照导入顺序依次执行。
func init() {
// do sth.
}
我们不能显示地调用 init 函数,在一个包可以定义多个 init 函数,但它们之间的执行顺序是没有明确定义的。
func init() {
// do sth.
}
func init() {
// do sth.
}
例如在使用 pprof 进行程序分析时,需要空白导入 pprof 包,但是不调用它的任何函数,就是为了调用它的 init 函数进行初始化。
import _ "net/http/pprof"
2. 退出
2.1 正常退出
首先一种正常退出的方式就是执行完 main 函数,或者在 main 函数中 return。
另外可以通过 os.Exit 退出并指定返回码为0。
os.Exit(0)
2.2 异常退出
通过 os.Exit 退出并指定返回码为非0的值,指定程序返回码。
os.Exit(1)
通过调用 panic 也可以引发程序的异常退出。或者在一些其它情况如引用数组下标超过范围,也会引发 panic 异常退出程序。
panic("crash")
2.3 优雅退出
当我们开发服务器程序时,程序一般都会长期运行在服务器上,监听端口对请求作出相应,或者是从消息队列或其它外部数据源中获取消息进行处理,或者是需要定时地完成某些任务。这时候我们发送信号令服务直接中断,就有可能对正在响应的请求、处理的任务粗暴地中止了,影响请求的响应或是任务的完整执行。
因此,我们需要让服务实现优雅退出,也就是通过监听捕获对应信号,并且在收到信号时做出一些操作,比如完成现在正在处理的请求和任务,进行一些资源回收等收尾工作,然后再结束程序的运行。
常见终止程序的信号有:1(SIGHUP)、2(SIGINT)、3(SIGQUIT)、9(SIGKILL)、15(SIGTERM)、19(SIGSTOP),其中 SIGKILL 和 SIGSTOP 不能被捕获、阻塞、忽略。
这些信号的触发方式:
- SIGHUP:终端被关闭
- SIGINT:按下 CTRL + C
- SIGQUIT:按下 CTRL + \
- SIGKILL:kill -9 pid
- SIGTERM:kill pid
- SIGSTOP:kill -19 pid
2.3.1 捕获信号
如下示例代码,程序启动后开始监听几个信号,并开启新的协程来捕获信号并处理,然后执行业务逻辑。当捕获相应的信号后,进行程序退出前的清理工作,如数据库连接断开、关闭HTTP服务、打印退出日志等。
func main() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
select {
case sig := <-c:
switch sig {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Printf("catch signal %s\n", sig)
// 完成清理操作
os.Exit(1)
}
}
}()
// 业务逻辑
}
或者换一种写法,程序启动后先执行业务逻辑,开启新的协程进行端口监听或消息队列消费等操作,然后创建一个通道捕获程序终止信号,捕获后进行程序退出前的清理工作。
func main() {
// 业务逻辑
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
sig := <-c
switch sig {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
// 完成清理操作
os.Exit(1)
}
}
2.3.2 使用context通知协程
如果捕获退出信号后,需要在子协程内执行一些清理操作,可以使用context来通知子协程接收终止的消息并完成清理操作。
同时通过 sync.WaitGroup 实现主协程等待子协程处理完毕后再停止运行。
func main() {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
ctx, cancle := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
// 完成清理操作
return
}
}
}(ctx)
<-sig // 收到信号
cancle() // 取消context
wg.Wait() // 等待所有的子协程都优雅关闭
}