Ryutaro Takemura

【Go】クロージャで何が便利なのか理解するためにRedisのようなKVS機能を作る

13 min read

Goの学習をはじめてクロージャという概念の理解に苦しんでました。「クロージャってどんな場面で使うんだろう?何が嬉しくてクロージャを使っているんだろう?」

何に使うか理解出来なものを、理解できないので調べていたらクロージャを使うことでRedisのようなKVSをGoのクロージャで作ることができるようなので、クロージャの理解と合わせてRedisのようなKVSを作ってみます

Redis

Redisとは、key - valueをペアとした値をメモリ上で永続化することができるツールです。 任意のkeyとvalueをセットすることで、あるvalueが必要なときにセットしたkeyを指定することで呼び出すことが出来ます。

# key と valueをセット
> set mykey "my value"
OK

# keyに紐づくvalueを呼び出す
> get mykey
"my value"

この様な仕組みをGoのクロージャを用いることで実装することが出来ます。

クロージャでキーバリューストアを実装する

まずは、任意のkeyとvalueを登録する機能をクロージャで実装してみます。 期待する動作を第1引数で渡し、第2引数と第3引数には key, valueを渡す実装を考えてみます 以下のように実行すれば、対応するkey, valueが保存される想定です。

kvs := closureKvs()

// "命令", "キー", "値"
kvs("ADD", "mykey", "my value")

closureKvs() という関数は、戻り値として関数を返します。この関数は、命令・キー・値を引数として受け取り、どんな型でも返せるように interface{} 型を戻り値として返す関数です。

func closureKvs() func(cmd string, key string, val interface{}) interface{} {

		store := make(map[string]interface{})

		return func(cmd string, key string, val interface{}) interface{} {
		// ...
		}
}

第3引数の val では、string型でもint型でも受け取れるように interface{} としています。 そして、 closureKvs() 内に変数storeを作成します。変数storeに指定したキーと値を格納できるように、キーをstring型、値にinterface型を指定しています。


一見、変数storeはローカル変数に見えますが、内部的にはクロージャに属する変数として機能しています。クロージャに属する変数として機能するとは、変数storeが何かしらの形で参照され続ける限り、格納されている値が破棄されることがありません。

通常、関数内のローカル変数は、関数の実行が完了した時点で破棄されるためクロージャによって捕捉された場合の変数と挙動が異なります。


この挙動を理解できずに、クロージャの理解を苦しめていました。では、どの様な変数がクロージャに属する変数としてみなされるのか?という疑問が残りますが、後ほど説明したいと思います。


キーと値を格納する処理を実装する

以下の処理を実行することで、キーと値を保持し続ける closureKvs() を実装していきます。

OK と返ってこれば、保存は成功しているとみなします。

func main() {
		kvs := closureKvs()

		// "命令", "キー", "値"
		result := kvs("ADD", "mykey", "my value")
		fmt.Println(result) // => OK
}

switch文を使って、命令として受け取った文字列を判定して処理を分けていきます。

今回はキーと値を格納する処理なので "ADD" を受け取った場合は、 クロージャに属する変数storeに store[key] = val として値を代入しています。そして成功したことを知らせる文字列 "OK" を返しています。

func closureKvs() func(cmd string, key string, val interface{}) interface{} {
	store := make(map[string]interface{})

	return func(cmd string, key string, val interface{}) interface{} {
		switch cmd {
		case "ADD":
			store[key] = val
			return "OK"
		}

		return nil
	}
}

以下で処理確認することができるので任意のキーと値を指定して実行してみてください。

OKと標準出力されれば成功です。

https://play.golang.org/p/g0fzf1gagBZ


格納した値を取得する

キーと値を保持する機能が作れたので、次はキーに紐づく値を取得できるようすることで、よりRedisらしくなっていきす。 値を取得したいので次は "GET" と命令して、取得したい キーを指定できるように実装していきます。

呼び出し側の処理はこんな感じです

func main() {
		kvs := closureKvs()
		
		// キーと値を格納する
		result := kvs("ADD", "mykey", "my value")
		fmt.Println(result) // => OK

		// 取得したいキーを取得してGETと命令する
		value := kvs("GET", "mykey", "")
		fmt.Println(value) // => my value
}

では、GETを渡したときの処理を追加していきます。

"GET" という命令を受け取ったとき、変数storeからキーを取得してその値を返す処理を追加することで、対応するキーに対応する値を取得できるようになります。

func closureKvs() func(cmd string, key string, val interface{}) interface{} {
	store := make(map[string]interface{})

	return func(cmd string, key string, val interface{}) interface{} {
		switch cmd {
		case "ADD":
			store[key] = val
			return "OK"
		// ここから
		case "GET":
			getVal := store[key]
			return getVal
		// ここまでを追加
		}

		return nil
	}
}

ここで処理を確認できます。

https://play.golang.org/p/ks4pIVqHPr8


どの様な変数がクロージャに属するのか?

先程、変数storeはクロージャに属する変数として捕捉され、通常のローカル関数とは挙動が異なると説明しましたが、ではどの様な状況で変数はクロージャに属するのかを見ていきます。

func closureKvs() func(cmd string, key string, val interface{}) interface{} {

		// クロージャに属する変数
		store := make(map[string]interface{})

		return func(cmd string, key string, val interface{}) interface{} {
		// ...
		}
}

複数の変数を関数内に定義して、どの変数がクロージャに属し、どの変数が通常のローカル変数としてみなされるのか見ていきます。

func checkClosureVariable() func() int {
	a := 1
	b := 5
	c := 10

	return func() int {
		c *= 10
		return c
	}
}

上記のクロージャを定義した場合、クロージャに属される変数と補足されるのは変数cのみです。

その他の変数a, cは checkClosureVariable() の処理が完了した時点でメモリ上から値が破棄されるが、クロージャに属される変数cは、何かしらの形で参照され続ける限り値を保持し続けます。


まとめ

「Go クロージャ」と調べると、クロージャの説明はたくさんありましたが、実際にどの様な使い方ができるのかのイメージが出来ずに理解に苦しでいました。

ただ、クロージャを利用することでRedisのようなKVSを実装できることを知り、楽しみながらクロージャを理解できた気がします。

参考

書籍: スターティングGO言語

https://www.okb-shelf.work/entry/2020/04/10/235523


Ryutaro Takemura

Hello