An easy and graceful way to start the main function and organize the entry point and closing method of your service in Go.

Introduction

  1. signal.Notify

    After running our service in main function as a new goroutine, we use signal.Notify to listen to the system signal to terminate the process. (e.g. Ctrl+C)

    This function relays incoming signals to the channel quitSignal provided by us, which blocks the main goroutine.

    func signal.Notify(c chan<- os.Signal, sig ...os.Signal)

    Notify causes package signal to relay incoming signals to c. If no signals are provided, all incoming signals will be relayed to c. Otherwise, just the provided signals will.

    Package signal will not block sending to c: the caller must ensure that c has sufficient buffer space to keep up with the expected signal rate. For a channel used for notification of just one signal value, a buffer of size 1 is sufficient

  2. context.WithTimeout

    After receiving the quitSignal, we call the Stop method of our service to do the cleanup work.

    It’s good to use context.WithTimeout to set a time limit for this quitting cleanup work, ensuring that the program will exit anyway after the time limit.

Template Code

File Structure

1
2
3
4
5
foo/
├─ go.mod
├─ myapp/
│  ├─ service.go
├─ main.go

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"foo/myapp"
)

func main() {
	// Load your configs and init your loggers and other necessary modules, etc.
	if err := InitModules(); err != nil {
		panic(err)
	}

	// Entry point of your service
	s := myapp.NewService()
	s.Run()

	// main goroutine waiting for shutdown
	quitSignal := make(chan os.Signal, 1)
	signal.Notify(quitSignal, os.Interrupt, syscall.SIGHUP, syscall.SIGINT,
		syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGTSTP)
	<-quitSignal

	// Cleanup task with time limit before quitting
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := s.Stop(ctx); err != nil {
		fmt.Println(err)
	}
}

// Example: init your modules
func InitModules() error {
	fmt.Println("preconditional modules init successfully")
	return nil
}

service.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package myapp

import (
	"context"
	"fmt"
)

type Service struct{}

func NewService() *Service {
	return &Service{}
}

func (s *Service) Run() {
	go func() {
    // Your service logic
		fmt.Println("Service running...")
	}()
}

func (s *Service) Stop(ctx context.Context) error {
	done := make(chan struct{})
	go func(done chan<- struct{}) {
		// Your quitting procedures to do some cleanup work
		fmt.Println("Goodbye :)")
		close(done)
	}(done)
	select {
	case <-done:
		return nil
	case <-ctx.Done():
		return fmt.Errorf("forced termination: %v", ctx.Err())
	}
}

Creative Commons License Licensed under CC BY-SA 4.0

Comments