使用 Golang 构建服务

147 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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")
}