🥂

【Go】range over func と仲良くなりたい!!

2024/07/31に公開

この記事は何

Go1.23から正式に導入されるrange over func、Go1.22で実験的に導入された頃から数えると、結構な時間が経ちましたね。既に慣れ親しんでいる方も多くいらっしゃると思いますが、恥ずかしながら私はまだです…。 お作法を理解できていないのが原因かと思います。

これではいけない!と、最近、一念発起してrange over funcに向き合ってみたので、私と同じようになかなか慣れられない方向けに記事を書きたいと思います。

この記事で扱うこと、扱わないこと

  • range over funcの導入に至る背景や解決したい課題に関する説明は行いません。
  • range over funcを使ってみよう!と意気込めるように、最低限押さえておきたいポイントを筆者の主観ベースでまとめた内容を紹介します。本機能の設計意図や言語仕様などを完璧に汲んだ説明は目指しておらず、あくまで「使い始める」を目標にした入門ドリルの位置付けです。

この記事の対象読者

  • Goの基本は履修済みだけど、range over funcまだ全然慣れてない!というGopher。

前提

  • GoのバージョンはGo1.23rc2であるとします。

この記事の構成

まずrange over funcを使い始めることを目的に、覚えておきたいポイントを説明する形式で進めます。

なお、ここからはストーリーテラーとして「ごふちゃん」というキャラクターを召喚してみます。

gofu.png

それでは始めましょう!

まず覚えること

ごふちゃん「今日勉強するrange over funcだけど、『これはこういうものだ!』と覚えないと始まらないことがいくつかあるよ。いっしょに覚えていこうね。」

rangeにfunc??

ごふちゃん「まずおさらいだけど、これまでのrangeの使い方といえばこういう感じだよね。」

for i, v := range []int{100, 200, 300} {
    fmt.Printf("iteration:%d, value:%d\n", v)
}
// output
// iteration:0, value:100
// iteration:1, value:200
// iteration:2, value:300

ごふちゃん「なんでこの構文でこういうことができるんだろう?っていうのは、正直『そういうものだよね』って覚えちゃうしかないよね。range over funcも同じ位置付けで、まずそういうもんだ!と覚えちゃうしかないよ!私のおすすめはGo Specの表をまずじーっと眺めてみることかなぁ。」

https://github.com/golang/go/blob/c9940fe2a9f2eb77327efca860abfbae8d94bf28/doc/go_spec.html#L6664-L6673

  • range func(func() bool)は値を何も返さない。for range ...みたいな感じで使うんだね。
  • range func(func(V) bool)はVを返す。for x := range ...みたいな感じで使うんだね。
  • range func(func(K, V) bool)はK, Vを返す。for i, x := range ...みたいな感じで使うんだね。

「いままで馴染みがあるスライスやマップと横並びで見ると、ちょっと挙動を理解しやすくないかな?」

func(V) boolみたいなのは結局どこで何をするの??

ごふちゃん「さっきのGo Specで出てきたrangeの使い方で、どんな値を引き出せるのかはわかったけど、具体的にどうやって使うのかな?ここでは例を挙げて考えてみるよ。1つ値を返す関数を例にしてみるね。」

https://goplay.tools/snippet/zwVr7t1RxUL

package main

import (
   "fmt"
   "time"
)

func main() {
   for x := range f {
   	fmt.Printf("loop=%d, begin\n", x)
   	time.Sleep(3 * time.Second)
   	fmt.Printf("loop=%d, end\n", x)
   }
}

func f(yield func(int) bool) {
   fmt.Println("start...")
   yield(1)
   fmt.Println("midway...")
   yield(2)
   fmt.Println("finish!")
}

「これを実行すると、こんな出力が得られるよ。分かりやすくするためにtime.Sleepを入れたところがあるから、時間での区切りを補記してあることに注意してね。」

start... 
loop=1, begin

(ここで待機)

loop=1, end
midway...
loop=2, begin

(ここで待機)

loop=2, end
finish!

「ここで覚えておくポイントは次のことかな。」

  • 慣例的にyieldという名前の関数を使うことが多くなりそうだよ。別に予約語とかではないので深読みしなくて良いよ。yieldは他の言語を使ったことがある人は見覚えがあるかもしれないね。
  • yieldの型はfunc(int)boolだから、f(yield)がGo Spedで言うところのfunction, 1 valueにあたるね。ということはrangeに渡せて、yield(1)ってすると1が渡せる/飛び出してくるはずだね。

「まだ分かりづらいだろうから、図を使って呼び出しの順序について確認してみるね。」

「まず、処理全体の流れは次のようになっているよ。」

for rangeループに入ると、最初のyieldまでf(yield)内の処理が進んで、最初のyieldの引数がループ変数に渡されるよ。さっきのコードだと、start...って表示するのとyield(1)するまでの部分だね。」

「ここでいったんf(yield)の処理は止まるよ。for rangeは受け取った値を使ったりしながらループ内の処理を進めるよ。」

「思い出してみるとyieldfunc(string)boolだからboolを返すんだけど、この例のようにfor rangeのループが一周終わるとtrueを返すよ。さっきのコードだとyield(1)=trueってことだね。これはそういうもの! って覚えてしまうのが早いかな。」

「ループが一周終わったので、f(yield)の中で次のyieldまで処理が進むよ。さっきのコードだとmidway...って表示して、yield(2)するところまでだね。こんなふうに、f(yield)が終了するまで処理を繰り返していくよ。」

for range内の2周目の処理が終わると、f(yield)finish!を表示してreturnするよ。returnされてこれ以上値が出てこないのでfor rangeループ全体が終了するんだね。」

range over funcの基本的な動きはこんな感じだよ。これは覚えちゃおうね。」

func(V) boolの戻り値って??

ごふちゃん「func(V) boolboolって何なの!?って引っかかった人も多いんじゃないかな?さっきの例で、for rangeが一周したらtrueっていう説明はしたよね。じゃあ次は、falseになるのはどんな時かを覚えようね。」

「さっきの例を一箇所変更してみるね。」

package main

import (
	"fmt"
	"time"
)

func main() {
	for x := range f {
		fmt.Printf("loop=%d, begin\n", x)
		time.Sleep(3 * time.Second)
		fmt.Printf("loop=%d, end\n", x)
+		break
	}
}

func f(yield func(int) bool) {
	fmt.Println("start...")
	yield(1)
	fmt.Println("midway...")
	yield(2)
	fmt.Println("finish!")
}

「これを実行してみると…?」

start...
loop=1, begin

(ここで待機)

loop=1, end
midway...
panic: runtime error: range function continued iteration after function for loop body returned false

「あらら、panic になっちゃったね。理由は読めば分かるけど、for rangeが中断されたのにf(yield)内の処理が続いているのはダメなんだね。どう対処すればいいんだろう?…ここでyieldの戻り値を使っていくよ。」

「さっきのコードをまた少し変更するね。」

package main

import (
	"fmt"
	"time"
)

func main() {
	for x := range f {
		fmt.Printf("loop=%d, begin\n", x)
		time.Sleep(3 * time.Second)
		fmt.Printf("loop=%d, end\n", x)
		break
	}
}

func f(yield func(int) bool) {
	fmt.Println("start...")
-	yield(1)
+	if !yield(1) {
+		fmt.Println("break!")
+		return
+	}
	fmt.Println("midway...")
	yield(2)
	fmt.Println("finish!")
}

「これを実行すると…?」

start...
loop=1, begin
loop=1, end
break!

「今度は panic せずに終了したね。この例から分かるように、for rangeのループがbreakなどで中断されるとyield=falseになるよ。これも覚えておくポイントだね。」

「処理全体の流れに、ループを中断した場合も追記したよ。」

yieldの戻り値が何を意味するのかと、終了処理のお作法についてわかってきたかな??」

「まず覚えないといけないことはここまでだよ!」

おわりに:この先の学び方

ごふちゃん「一緒に勉強してみてどうだった?意外と覚えることは少なかったんじゃないかな?これでrange over funcの基本は理解できたと思うから、この後はrange over funcを使って実現できる面白いことを色々探してみてね!」

⭐️ぜひ読みたい記事たち⭐️

GitHubで編集を提案

Discussion