go言語でシグナルをきちんとエラーハンドリングする

関連

並行処理全般に関するメモは以下 go言語の並行処理 - ludwig125のブログ

go言語でsignalを適切に処理する方法を調べたので例をいくつか

シグナルを受け付けて関数を適切に終了させる例1

package main

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

func main() {

    sigs := make(chan os.Signal, 1)
    ctx, cancel := context.WithCancel(context.Background())

    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    defer func() {
        // シグナルの受付を終了する
        signal.Stop(sigs)
        cancel()
    }()

    go func() {
        select {
        case sig := <-sigs: // シグナルを受け取ったらここに入る
            fmt.Println("Got signal!", sig)
            cancel() // cancelを呼び出して全ての処理を終了させる
        }
    }()

    if err := doTask(ctx); err != nil {
        fmt.Printf("failed to doTask: %v", err)
        cancel() // 何らかのエラーが発生した場合、他の処理も全てcancelさせる
        return
    }
    fmt.Println("done successfully.")
}

func doTask(ctx context.Context) error {
    defer fmt.Println("done doTask")
    for i := 0; i < 5; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("received done")
            return ctx.Err()
        default:
        }
        // // エラー時の挙動が見たい場合はここのコメントアウトを外す
        // if i == 3 {
        //  return fmt.Errorf("error happened")
        // }

        // do something
        fmt.Println("sleep 1. count:", i)
        time.Sleep(1 * time.Second)
    }
    return nil
}

動作確認

順にそれぞれの挙動を見てみる

正常終了時

$go run signal3/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
sleep 1. count: 3
sleep 1. count: 4
done doTask
done successfully.

異常終了時(上のコメントアウトを外した時)

$go run signal3/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
done doTask
failed to doTask: error happened%                                                                                                               

Ctrl+Cをした時

$go run signal3/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
^CGot signal! interrupt
received done
done doTask
failed to doTask: context canceled%                                                                                                             
  • Ctrl+Cのとき、signalを受け取った後cancelされていることがわかる

シグナルを受け付けて関数を適切に終了させる例2(タスクの中身がgoroutine)

練習がてら、doTaskの中身をgoroutineにしてみた場合も考えてみた

package main

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

    "golang.org/x/net/context"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, os.Interrupt)
    defer func() {
        // シグナルの受付を終了する
        signal.Stop(sigs)
        cancel()
    }()
    go func() {
        select {
        case sig := <-sigs: // シグナルを受け取ったらここに入る
            fmt.Println("Got signal!", sig)
            cancel() // cancelを呼び出して全ての処理を終了させる
            return
        }
    }()

    res, err := doTask(ctx)
    for v := range res {
        fmt.Println("done successfully.", v)
    }
    for e := range err {
        fmt.Printf("failed to doTask: %v", e)
        cancel() // 何らかのエラーが発生した場合、他の処理も全てcancelさせる
        return
    }
}

func doTask(ctx context.Context) (<-chan string, <-chan error) {
    resCh := make(chan string)
    errCh := make(chan error, 5)
    go func() {
        defer fmt.Println("done doTask")
        defer close(resCh)
        defer close(errCh)
        for i := 0; i < 5; i++ {
            select {
            case <-ctx.Done():
                fmt.Println("received done")
                // Do something before terminated
                time.Sleep(500 * time.Millisecond)
                errCh <- ctx.Err()
                return
            default:
            }
            // // エラー時の挙動が見たい場合はここのコメントアウトを外す
            // if i == 3 {
            //  errCh <- fmt.Errorf("error happened")
            //  return
            // }

            // do something
            fmt.Println("sleep 1. count:", i)
            time.Sleep(time.Second)
        }
        resCh <- fmt.Sprintf("something")
    }()
    return resCh, errCh
}

動作確認

正常終了時

$go run signal5/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
sleep 1. count: 3
sleep 1. count: 4
done doTask
done successfully. something

異常終了時(上のコメントアウトを外した時)

$go run signal5/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
done doTask
failed to doTask: error happened%                                                                                                               

Ctrl+Cをした時

$go run signal5/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
^CGot signal! interrupt
received done
done doTask
failed to doTask: context canceled%                                                                                                             

問題なさそう

シグナルを受け付けて関数を適切に終了させる例3(独自のcontextを用意する)

こちらの記事で紹介されていたNewCtxを使ってみる Managing Groups of Goroutines in Go - The Startup - Medium

これは、signalを処理してcontextのcancelを送るctxを定義するもの

シグナルを受け付けるまで処理し続けるような場合(main側でエラー時にcancelを使う必要がない場合)では、上のコードがかなり減った

package main

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

    "golang.org/x/net/context"
)

func main() {
    res, err := doTask(newCtx())
    for v := range res {
        fmt.Println("done successfully.", v)
    }
    for e := range err {
        fmt.Printf("failed to doTask: %v", e)
        return
    }
}

func newCtx() context.Context {
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        sCh := make(chan os.Signal, 1)
        signal.Notify(sCh, syscall.SIGINT, syscall.SIGTERM)
        <-sCh
        fmt.Println("Got signal!")
        cancel()
    }()
    return ctx
}

func doTask(ctx context.Context) (<-chan string, <-chan error) {
    resCh := make(chan string)
    errCh := make(chan error, 5)
    go func() {
        defer fmt.Println("done doTask")
        defer close(resCh)
        defer close(errCh)
        for i := 0; i < 5; i++ {
            select {
            case <-ctx.Done():
                fmt.Println("received done")
                // Do something before terminated
                time.Sleep(500 * time.Millisecond)
                errCh <- ctx.Err()
                return
            default:
            }

            // do something
            fmt.Println("sleep 1. count:", i)
            time.Sleep(time.Second)
        }
        resCh <- fmt.Sprintf("something")
    }()
    return resCh, errCh
}

実行結果

シグナル送った時

$go run signal_pattern/signal6/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
^CGot signal!
received done
done doTask
failed to doTask: context canceled

正常終了時

$go run signal_pattern/signal6/signal.go
sleep 1. count: 0
sleep 1. count: 1
sleep 1. count: 2
sleep 1. count: 3
sleep 1. count: 4
done doTask
done successfully. something