やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【golangのサンプル有り】クロージャ(closure)完全に理解した人のためのクロージャを使った便利サンプル集

クロージャ完全に理解した...で何に使うの?

実は使いこなせると結構便利。特定の値に対する操作を共通化することが出来て、想定しているもの、言い方を変えれば作者の作成しているもの以外の操作を制限することも出来る。あと、かっこいい。
クロージャユースケースとしては小さなデータベースに対してコマンド経由で何かをしたい時と考えると分かりやすい。今回はそのサンプルを4つ用意したので、便利さを体感してみてほしい。1つでも刺さるものがあれば嬉しい限り。

サンプル集

設計方針としては2つの考えをベースにしている。

  • 初期値となる変数の用意(データ構造によってやりたいことを考える)
  • 用意した初期値に対する操作(取得, 更新, 削除)を行うコマンドを引数で受け取る無名関数を作成してコマンドの判定を行い、それぞれに対応する処理を記述する

クロージャを使ったタイマー

動作確認はこちらから
play.golang.org

// closure timer -> 経過時間をsecondsで返す
func ClosureTimer() func(c string) float64 {
    start := time.Now()
    return func(command string) float64 {
        switch command {
        case "GET":
            return time.Now().Sub(start).Seconds()
        case "STOP":
            return time.Now().Sub(start).Seconds()
        case "RESET":
            start = time.Now()
            return 0
        }
        return 0
    }
}

実行結果

func main(){
    // 新たなtimerを作成
    newTimer := ClosureTimer()
    time.Sleep(1 * time.Second)
    // 経過時間が1sとなるはず
    fmt.Println(int(newTimer("STOP")))
    
    newTimer("RESET")
    // timerがリセットされて0sとなるはず
    fmt.Println(int(newTimer("STOP")))
}
1
0

高階関数を使って関数内のローカルなスコープを持つ変数、この場合はClosureTimerを呼び出した時の時刻を初期値として保持しておく。この時刻と現在時刻との差分を返すのが1番の目的(GET or STOP)となるが、その他に比較時間のリセットのためのRESETというコマンドも用意してある。switch caseを増やせばいくらでも拡張できるため、都度都度、時刻時間の初期値を用意して差分を出して...値をリセットして...という変数管理をコマンドだけで行うことが出来る上にコードも共通化することが出来る。
自分は関数の実行時間を測る際にパッケージに記述したこの関数をよく使っていた。

クロージャを使ったカウンター

動作確認はこちらから

play.golang.org

func ClosureCounter(init, inc int) func(c string) int {
  count := init
  return func(command string) int {
    switch command {
      case "ADD":
        count += inc
        return count
      case "RESET":
        count = init
        return count
      case "GET":
        return count
    }
    return -1
  }
}

実行結果

func main(){
    // 新たなcounterを作成
    counter := ClosureCounter(0, 1)
    // 初期値の確認
    fmt.Println(counter("GET"))
    for i := 0; i < 10; i++ {
      counter("ADD")
    }
    
    // 正しくカウントされているかどうか
    fmt.Println(counter("GET"))
    // 値のリセット
    fmt.Println(counter("RESET"))
}
0 
10
0

やっていることは先ほど全く同じでデータ構造がintに変わったのみ。ClosureCounterを使うことでインクリメント変数の用意と管理を先ほどと同じように共通化、コマンド経由でのみ実行することが出来るため非常に便利。動作させているスレッドのカウントや、集計処理で条件にマッチしたデータ数をカウントするのによく使っていた。

クロージャを使ったkvs(キーバリューストア)

いわゆるredisに近いことがスコープが有効な範囲でやれる。

動作確認はこちらから
play.golang.org

func ClosureKVS() func(c string, k string, v interface{}) interface{} {
  init := make(map[string]interface{})
  return func(cmd string, k string, v interface{}) interface{} {
    switch cmd {
      case "ADD":
        init[k] = v
        return nil
      case "GET":
        res := init[k]
        return res
      case "RESET":
        init = make(map[string]interface{})
        return nil
      case "KEYS":
        resp := make([]string, 0)
        for k, _ := range init {
          resp = append(resp, k)
        }
        return resp
      case "VALUES":
        resp := make([]interface{}, 0)
        for _, v := range init {
          resp = append(resp, v)
        }
        return resp
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなkvsを作成
    kvs := ClosureKVS()
    
    // key valueを追加
    kvs("ADD", "okb", "cool")
    fmt.Println(kvs("GET", "okb", ""))
    
    // さらにkey valueを追加
    kvs("ADD", "bko", "bad")
    fmt.Println(kvs("GET", "bko", ""))
    
    // 現在登録されているkeyを全取得
    fmt.Println(kvs("KEYS", "", ""))
    
    // 現在登録されているvalueを全取得
    fmt.Println(kvs("VALUES", "", ""))
    
    // 一度kvsをリセット
    fmt.Println(kvs("RESET", "", ""))
    
    // リセットが問題なく行われたかを確認
    fmt.Println(kvs("KEYS", "", ""))
}
cool
bad
[okb bko]
[bad cool]
<nil>
[]

正直、interface{}型ってあんまり使うのは好きではないのですが、半ばしょうがなく採用。keyが常にstring型なのは良いとしてvalueの値の型を固定すると、都度都度、対応する型のkvsを作る必要があるのでこんな形となった。

クロージャを使ったキュー

ここまで来たらもう何でも出来そう。やっぱりコマンド経由で...(ry

動作確認はこちらから
play.golang.org

func ClosureQueue() func(c string, v interface{}) interface{} {
  queue := make([]interface{}, 0)
  return func(cmd string, v interface{}) interface{} {
    switch cmd {
      case "QUEUE":
        queue = append(queue, v)
        return true
      case "DEQUEUE":
        if len(queue) > 0 {
          head := queue[0]
          queue = queue[1:len(queue)]
          return head
        }
        return nil
      case "SIZE":
        return len(queue)
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなキューを作成
    queue := ClosureQueue()
    queue("QUEUE", 1)
    queue("QUEUE", 2)
    queue("QUEUE", 3)
    
    fmt.Println(queue("SIZE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    
    queue("QUEUE", 99)
    
    fmt.Println(queue("DEQUEUE", ""))
}
3
1
2
3
<nil>
99

最後に

どうでしょうか。クロージャも使ってみると意外とやれることが多いことに気づいてもらえれば何より。やはり値に対する操作を制限出来るというのが良い。想定外が発生しにくくなるし、共通化もされるため、インクリメント処理などを後から修正する事になっても、クロージャを使って共通化しておけば退屈な修正は最小限に済むだろう。

参考文献