Go のラベル


久しぶりに「『プログラミング言語Go』オンライン読書会」からの小ネタ。

https://gpl-reading.connpass.com/event/241602/

まずは簡単なカウントダウンプログラムを書いてみる。

sample1.go
//go:build run
// +build run

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Commencing countdown.")
    tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        <-tick
    }
}

(『プログラミング言語Go』8.7章より)

これを実行すると1秒毎にカウントダウンを表示する。

$ go run sample1.go 
Commencing countdown.
10
9
8
7
6
5
4
3
2
1

ここまでは簡単。

次に,このカウントダウンプログラムを中断させることを考える。『プログラミング言語Go』8.7章では任意の abort チャネルを作ってリターンキー押下でイベントを発火させていたが,この記事では横着して os/signal パッケージを使うことにする。こんな感じ[1]

sample2.go
//go:build run
// +build run

package main

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

func main() {
    abort := make(chan os.Signal, 1)
    defer func() {
        close(abort)
        fmt.Println("Close abort channel.")
    }()
    signal.Notify(abort, os.Interrupt)

    fmt.Println("Commencing countdown.  Press Ctrl+C to abort.")
    tick := time.Tick(1 * time.Second)
LOOP:
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        select {
        case <-tick:
            //何もしない
        case <-abort:
            fmt.Println("Countdown aborted!")
            break LOOP
        }
    }
}

このプログラムを起動し,途中 [Ctrl+C] キーで止めてみる。

$ go run sample2.go 
Commencing countdown.  Press Ctrl+C to abort.
10
9
8
^CCountdown aborted!
Close abort channel.

うんうん。ちゃんとチャネルのクローズまで行ってるね。

で,ここで登場するのが LOOP ラベルなのだが,私のようなロートル・エンジニアな方々はこう思わなかっただろうか。

break で LOOP ラベルまで戻ったら for 文のやり直しで結局止められんのんちゃうん?

私は最初そう思ったし,今回の読書会でも実際に質問があった(私だけじゃなかった)。

プログラミング言語Go』にはラベルに関する説明がほとんどない。わずかに「2.7 スコープ」の章に

break 文、continue 文、 goto 文で使われる制御フローラベルのスコープはそれを含んでいる関数全体です。
(『プログラミング言語Go』2.7章より)

と説明があるだけで,あとは8.7章のサンプルコードで実際にラベルの記述が登場するまで一切言及がない。これは分からんわ(笑)

実は Go のラベルは処理の位置(address)を示すものではなく,それに続く文(statement)を指示する文なのだ。

A labeled statement may be the target of a goto, break or continue statement.

LabeledStmt = Label ":" Statement .
Label = identifier .
(via “The Go Programming Language Specification”)

先程のコードで言うなら LOOP ラベルは直後の for 文を指していて, for 文の中の break LOOP は,内側にある select 文ではなく, LOOP でラベル付けされた for 文内の処理を抜けることを意味している。

ちなみに goto, break, continue の対象ラベルのスコープは

The scope of a label is the body of the function in which it is declared and excludes the body of any nested function.
(via “The Go Programming Language Specification”)

とかなり限定されていて,特に goto 文では, goto 文実行時点で未宣言の変数を含んでたり

    goto L  // BAD
    v := 3
L:
    ...

ブロックの外から中に入るような

if n%2 == 1 {
    goto L1 // BAD
}
for n > 0 {
    f()
    n--
L1:
    f()
    n--
}

書き方もアカンらしい。なかなか使いどころが難しい,というか(余程のことがない限り)使う気にならない。

このようなラベルの書き方や作用は Go 特有というわけではないし(Java とかも確かこうなっている)今の若い方はむしろ今回のようなラベルの使い方が普通と思われるだろうが,若い頃にアセンブラや BASIC なんかで書いてたおぢさんにとっては微妙に違和感があって,慣れるまでしばらくかかったのですよ。

というわけで最後は年寄りの昔語りみたいになってしまったがご容赦を。

https://www.amazon.co.jp/dp/B099928SJD

脚注
  1. 最近の os/signal パッケージでは context を使った NotifyContext() というイケてる関数が用意されているのだが,この記事の主題はキャンセル処理ではないので,古臭いコードになっている(笑) ↩︎