GoTo文とは
プログラミング界のヴォルデモート卿。
記述してはいけないあの構文。
かつて、エドガー・ダイクストラは言った。Goto文は危険である。
Edgar Dijkstra : Go To Statement Considered Harmful
50年前以上のことであるが、プログラマなら誰もが聞いたことがあり、
物心ついたときから、Goto文を避けてきたとのではないかと思います。
ですが、比較的新しい(?) Go言語にもGoto文は存在し、標準ライブラリにも使われてますので、
Gotoキャンペーンにかこつけて、様々な利用例を探っていきたいと思います。
「構造化プログラミングの観点からgoto文を使うのは望ましくない」ものの「単にgoto文を使わなければ見通しが良くなる」という考えは“Go To Statement Considered Harmful”[4]でも否定されており、goto文の有無のみに固執するのは不毛である。構造化プログラミングの本質の一つは、状態遷移の適切な表現方法とタイミングを見極めることである
Go言語のGoto文の仕様
func main(){
log.Println(1)
goto L
log.Println(2)
L:
log.Println(3)
}
--- 結果 ---
2
3
func main(){
if false {
goto Label
}
Label:
log.Println(1)
}
--- 結果 ---
1
func main(){
goto Label
var str string
Label:
log.Println(3)
}
--- 結果 ---
goto Label jumps over declaration of str at ./Main.go:XX:X
func main(){
goto Label
if false {
Label:
log.Println(1)
}
}
--- 結果 ---
goto Label jumps into block starting at ./Main.go:X:XX
利用例を探す
本家、golangのGithubから利用例を探して行こうと思います。
エラーハンドリングに使う
Exeptionのthrow/catchがないGo言語で似たような物を実現する為の書き方ですね。
goにはdeferを使う書き方がありますが、こちらも見通しがよく良い書き方のようにみえます。
case *Pointer, *Signature, *Slice, *Map, *Chan:
if !x.isNil() {
goto Error
}
// keep nil untyped - see comment for interfaces, above
target = Typ[UntypedNil]
default:
goto Error
}
x.typ = target
check.updateExprType(x.expr, target, true) // UntypedNils are final
return
Error:
check.errorf(x.pos(), "cannot convert %s to %s", x, target)
x.mode = invalid
}
参考:https://github.com/golang/gofrontend/blob/master/libgo/go/go/types/expr.go
ネストしたLoop,条件分岐からの脱出(処理のスキップ)
上記のErrorとほぼ同様の使い方ですが、
ループと複雑な条件分岐から、処理へ転送例です。
breakを書かず、複数Loopを抜け、処理をスキップできます。
for ; nSrc < len(src); nSrc += size {
c0 := src[nSrc]
if c0 >= utf8.RuneSelf {
r, size = '\ufffd', 1
goto write
}
...
write:
if nDst+utf8.RuneLen(r) > len(dst) {
return nDst, nSrc, transform.ErrShortDst
}
nDst += utf8.EncodeRune(dst[nDst:], r)
}
return nDst, nSrc, err
参考:https://github.com/golang/text/blob/master/encoding/japanese/iso2022jp.go
構文解析の条件分岐に利用する。
htmlタグ解析の条件分岐に使っているようです。
goto文は実行速度は早いのでしょうか?
形がきっちり定まっている物の解析には良いのかもしれません。
処理自体スッキリしてみえますね。
func (z *Tokenizer) readScript() {
defer func() {
z.data.end = z.raw.end
}()
var c byte
scriptData:
c = z.readByte()
if z.err != nil {
return
}
if c == '<' {
goto scriptDataLessThanSign
}
goto scriptData
scriptDataLessThanSign:
c = z.readByte()
if z.err != nil {
return
}
switch c {
case '/':
goto scriptDataEndTagOpen
case '!':
goto scriptDataEscapeStart
}
z.raw.end--
goto scriptData
....
参考:https://github.com/golang/net/blob/master/html/token.go
gotoを使ったループ
for内で、CheckAndLoopに戻りcontinueする条件に戻っています。
条件を外れた場合、再度Skipラベル以降の処理が実行される。
forの条件判定や、for ~ CheckAndLoopまでの処理されるパターン限定し、効率よく反復する目的なのでしょうか。
普段、チーム開発を行っている私はこういった物を書く機会は少ないでしょう。
パフォーマンスが著しく悪く最適化するような際は小さいpackageに切り出してこういった処理を書く機会があるかもしれません。
for len(b.jobs) > 0 {
....
....
CheckAndLoop:
if !b.shouldVisit(pc, pos) {
continue
}
Skip:
inst := re.prog.Inst[pc]
switch inst.Op {
default:
panic("bad inst")
case syntax.InstFail:
panic("unexpected InstFail")
case syntax.InstAlt:
if arg {
arg = false
pc = inst.Arg
goto CheckAndLoop
} else {
b.push(re, pc, pos, true)
pc = inst.Out
goto CheckAndLoop
}
参考:https://github.com/golang/gofrontend/blob/master/libgo/go/regexp/backtrack.go
最後に
GoのGoto文はC言語のものと違い、自由奔放な処理転送を行えるものとは少し違うということがわかりました。
用途によってはソースの見通しもよく綺麗にかけるので、よく理解し利用すれば他言語と比べてモヤッとしていた
部分を解消できるのではないでしょうか。
ただ、使われて来なかった背景もあり、馴染みのない構文の為、利用者の技術力によってしまうところもあり、
特にチームでの開発では、一箇所に書いたこの構文をどんな人に、どんな風に利用されるかわかりません。
レビューで頑張って取り切るか、小さなPackageでの利用に留め、
基本的には使わないにこしたことはないと思います。
ということで、私のGotoキャンペーンは中止です。。
その他、利用例や、こう使うといいよ!等ありましたら、教えていただけたら幸いです。
皆様のGotoキャンペーンをお待ちしております。