持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
如果不使用其他的框架,自己使用 Golang 搭建一个服务需要做哪些操作呢?搭建一个服务不仅要监听服务,还要对启动的服务或者进程进行监控,还需要保持服务一直运行等工作。本文主要实践下完成构建一个服务的基本结构。
什么是服务
计算机系统中,服务是响应事件或请求执行特定任务的程序,比如:
- Http 请求
- 消息节点的消息或流
- 时间事件,如定时任务
服务是永久保持运行的,当然也需要准备好在一定时机停止。当然在服务停止时需要注意所有任务都必须完成,所有的连接都需要关闭或者没有其他协程正在运行。
创建服务
首先,构建一个能够响应Http 请求的服务,能够支持启动和关闭 Web 服务的功能。
var srv *http.Server
func Start(){
createServer()
go func(){
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
}
func Shutdown(){
done = make(chan struct{})
go func() {
defer close(done)
if err := srv.Shutdown(ctx); err != nil {
log.Printf("couldnt shutdown server error [%s]\n", err)
}
}()
return
}
func createServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", helloWorld)
srv = &http.Server{
Addr: ":8000",
Handler: mux,
}
}
Start() 函数主要启动一个协程去接收和处理其他的请求,这个包主要服务则创建和这个协程。Shutdown() 函数是以同步方式优雅的结束服务,它允许在等待所有的请求都结束后再结束其他的所有的程序。
启动服务
在Go 中的 main.go文件,可以开始创建一个 start 方法。这里我们需要也启动相关服务需要的中间件,比如打开数据库连接,监听消息节点的流事件,缓存等等。
func start(ctx context.Context, cancel contetx,CancelFunc){
ctx,cancel = context.withCancel(context.Background()
httpserver.Start()
return
}
以上代码中也创建了取消 context 的参数,主要是因为所有的进程都在这个方法中启动后应该是共享同一个 Context,所以当 cancel 方法被调用时所有的协程都应该能够被通知到,且能够停止它们正在处理的事情。而且没有方法能够阻塞线程,还有内部的程序都应该为自己协程负责。
服务不可避免宕机
服务启动后,就不能让主线程挂了,我们可以创建一个 channel然后等待那个消息通知。
func main(){
_, cancel := start()
defer cancel()
neverEnd := make(chan struct{})
<-neverEnd
}
func start() (ctx context.Context, cancel context.CancelFunc) {
ctx, cancel = context.WithCancel(context.Background())
httpserver.Start()
return
}
以上程序将会一直执行直到操作系统将进程杀死。这种方式可能会引起很多问题。比如导致数据库数据不一致,或和消息节点就会一直处理连接状态,这样就可能导致发生问题时很难找到该 bug。
所以需要为系统突然关掉或停止所有进程做好准备,可以创建一个等待关闭的处理方式。
func WaitShutDown(){
signc := make(chan os.Signal,1)
signal,Notify(signc,syscall.SIGHUP)
s := <-sigc
}
signal,Notify 方法将会发送所有从操作系统到 signc 通道的所有结束信号。现在就可以的等待 channel 接收到信息,然后阻塞主线程。
这样,在 main.go 中就可以启动所有的内容,然后直到操作系统发送结束信息就会结束服务。
关闭服务
现在我们知道服务将会宕机,那可以通过创建 shutdown 方法停止内部所有的进程。
func shutdown(){
cancel()
ctx := context.Background()
doneHTTP := httpserver.Shutdown(ctx)
<-doneHTTP
log.Println("bye bye")
}
在中断服务之前,主线程使用 <-doneHttp 信号保持持续等待直到所有的请求都结束。但是操作系统可能不会一直等待,它可能短暂时间内直接杀死进程,停止所有的事情。所以最佳实践是等待某段时间后超时了就直接强制停止所有。
func waitUntilIsDoneOrCancel(){
select{
case <-done:
log.Println("all done")
case <- ctx.Done():
err = ErrServiceCanceled
log.Println("canceled")
}
return
}
这样,我们的 shutdown 方法就完成了。
func shutdown(){
cancel()
ctx, cancelTimeout := context.WithTimeout(context.Background(), time.Second*30)
defer cancelTimeout()
doneHTTP := httpserver.Shutdown(ctx)
err := servicemanager.WaitUntilIsDoneOrCanceled(ctx, doneHTTP)
if err != nil {
log.Printf("service stopped by timeout %s\n", err)
}
log.Println("bye bye")
}