Redigoを使う(2)　様々なデータ型を扱う

# はじめに

[Redis](https://redis.io/)のGo言語向けクライアントライブラリ[Redigo](https://github.com/gomodule/redigo)の使い方を見ます。
この記事では様々なデータ型の扱い方を見ます。

Redisには下記の5つのデータ型があります。

* 文字列型
* リスト型
* セット型
* ソート済みセット型
* ハッシュ型

文字列型については(1)の記事で見ました。本記事では他の4つについて、Redigoでの基本的な扱い方を見ていきます。 

# 環境

* OS: Windows 10
* Redis: [win-3.2.100](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.2.100)
* Go言語: [1.11](https://blog.golang.org/go1.11)

# リスト型

リスト型は、文字列型の双方向リストです。コマンド一覧は[リファレンス](http://redis.shibu.jp/commandreference/lists.html)を参照して下さい。まずは単純な要素の追加と取得を行います。

```go:main.go
package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	// 接続
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 要素の追加
	_, err = conn.Do("RPUSH", "seasons", "spring")
	_, err = conn.Do("RPUSH", "seasons", "summer")
	_, err = conn.Do("RPUSH", "seasons", "autumn")
	_, err = conn.Do("RPUSH", "seasons", "winter")

	// 要素の取得
	a, err := redis.String(conn.Do("LINDEX", "seasons", 2))
	if err != nil {
		panic(err)
	}
	fmt.Println(a) // autumn

	// 全体の取得
	s, err := redis.Strings(conn.Do("LRANGE", "seasons", 0, -1))
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // [spring summer autumn winter]
}
```

RPUSHはリストの末尾に要素を追加するコマンドです（ここでは戻り値チェックを省略しています）。seasonsというキーのリストに4つの要素を追加しています。
LINDEXは、リストの要素のうち、インデクスで指定された要素を返します。
LRANGEは、リストの要素のうち、2つのインデクスで指定された範囲の要素を返します。「0」は先頭、「-1」は末尾を意味します（すなわち全ての要素を返す）。さらに結果を`redis.Strings`に渡しています。これは`conn.Do`の結果を[]stringに変換するためのユーティリティ関数です。

要素の削除は次のように行います。

```go:main.go
package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	// 接続
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 要素の追加
	_, err = conn.Do("RPUSH", "words", "xxxxxx")
	_, err = conn.Do("RPUSH", "words", "yyy")
	_, err = conn.Do("RPUSH", "words", "z")
	_, err = conn.Do("RPUSH", "words", "xxxxxx")

	// 要素の削除（先頭に近いxxxxxxを1個だけ消す）
	n, err := redis.Int(conn.Do("LREM", "words", 1, "xxxxxx"))
	if err != nil {
		panic(err)
	}
	fmt.Println(n) // 1

	// 要素の削除（先頭の要素を消す）
	a, err := redis.String(conn.Do("LPOP", "words"))
	if err != nil {
		panic(err)
	}
	fmt.Println(a) // yyy

	// 全体の取得
	s, err := redis.Strings(conn.Do("LRANGE", "words", 0, -1))
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // [z xxxxxx]
}
```

wordsというキーのリストに4つの文字列を追加したあと、LREMとLPOPで要素の削除を行っています。

LREMは、リストを先頭から見て、指定された値の要素を指定された個数だけ削除します。この場合は、一番最初に追加したxxxxxxが削除されます。戻り値は削除された要素数です。結果を`redis.Int`でGoのint型に変換しています。

LPOPは、リストの先頭の要素を削除します。このときの状態は[yyy, z, xxxxxx]ですので、先頭のyyyが削除されます。戻り値は削除された要素です。`redis.String`に渡してGoのstring型に変換しています。

今回は紹介しませんでしたが、リスト型のコマンドについては、ほかに、要素を先頭と末尾から切り詰めるLTRIM、特性の要素の値を書き換えるLSET、2つのリスト間で要素の移動をするRPOPLPUSHなどがあります。

# セット型

セット型は、文字列型の集合です。リスト型と異なり、要素の重複を許しません。コマンド一覧は[リファレンス](http://redis.shibu.jp/commandreference/sets.html)を参照して下さい。要素の追加・削除・取得の一連の流れを示します。

```go:main.go
package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	// 接続
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 要素の追加
	_, err = conn.Do("SADD", "numbers", "one")
	_, err = conn.Do("SADD", "numbers", "two")
	_, err = conn.Do("SADD", "numbers", "three")
	_, err = conn.Do("SADD", "numbers", "four")

	// 要素の追加（既に追加済み）
	n, err := redis.Int(conn.Do("SADD", "numbers", "one"))
	if err != nil {
		panic(err)
	}
	fmt.Println(n) // 0

	// 要素の削除
	m, err := redis.Int(conn.Do("SREM", "numbers", "three"))
	if err != nil {
		panic(err)
	}
	fmt.Println(m) // 1

	// 要素の存在確認
	k, err := redis.Int(conn.Do("SISMEMBER", "numbers", "five"))
	if err != nil {
		panic(err)
	}
	fmt.Println(k) // 0

	// 全体の取得
	s, err := redis.Strings(conn.Do("SMEMBERS", "numbers"))
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // [one two four]
}
```

SADDはセットに要素を追加します。既に存在する要素を再度追加しようとすると、戻り値は0になります（成功時は1が返る）。
SREMは、セットから指定された要素を返します。
SISMEMBERは、指定された要素がセットに存在するか否かを返します。存在すれば1、しなければ0が返ります。
SMEMBRESはセットのすべての要素を取得します。

セット型のコマンドについては、他に、ランダムに要素を削除するSPOP（いつ使うんだろ？）、要素の個数を得るSCARD、2つのセット間で要素の移動をするSMOVEなどがあります。ほか、複数のセットの積集合（SINTER）や和集合（SUNION）、差集合（SDIFF）など、集合関係のコマンドが充実しています。

# ソート済みセット型

ソート済みセット型は、スコアという数値にもとづいてソートされた文字列型の集合です。C++のstd::multimap\<float,string\>型のようなものだと思います。コマンド一覧は[リファレンス](http://redis.shibu.jp/commandreference/sortedsets.html)を参照して下さい。5人の生徒のテスト点数をモチーフに、よくある操作の例を示します。

```go:main.go
package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	// 接続
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 要素の追加
	_, err = conn.Do("ZADD", "math_test", 60, "alice")
	_, err = conn.Do("ZADD", "math_test", 50, "bob")
	_, err = conn.Do("ZADD", "math_test", 100, "charlie")
	_, err = conn.Do("ZADD", "math_test", 10, "dave")
	_, err = conn.Do("ZADD", "math_test", 20, "eve")

	// 最低スコアの要素（とそのスコア）を得る
	s, err := redis.Strings(conn.Do("ZRANGE", "math_test", 0, 0, "WITHSCORES"))
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // [dave 10]

	// スコア40以上70以下の要素（とそのスコア）を得る
	t, err := redis.Strings(conn.Do("ZRANGEBYSCORE", "math_test", 40, 70, "WITHSCORES"))
	if err != nil {
		panic(err)
	}
	fmt.Println(t) // [bob 50 alice 60]

	// スコアが小さい順に要素を2個削除する
	n, err := redis.Int(conn.Do("ZREMRANGEBYRANK", "math_test", 0, 1))
	if err != nil {
		panic(err)
	}
	fmt.Println(n) // 2

	// 全体の取得
	u, err := redis.Strings(conn.Do("ZREVRANGE", "math_test", 0, -1))
	if err != nil {
		panic(err)
	}
	fmt.Println(u) // [charlie alice bob]
}
```

点数の低い順でソートされた状態で要素が保持されます。

| ランク |    0 |   1 |   2 |     3 |       4 |
|-------|------|-----|-----|-------|---------|
| スコア |   10 |  20 |  50 |    60 |     100 |
| 値 | dave | eve | bob | alice | charlie |

特定の順位の生徒を得たいときは、ZRANGEを使います。例のように、`ZRANGE, 0, 0`と指定したときは、ランクが0以上0以下の生徒、すなわち最低点の生徒が得られます（0-index）。さらにWITHSCORESというオプションを指定するとスコアも合わせて得られます。反対に、最高点の生徒を得たいときはZREVRANGEを使います。
点数の範囲で絞り込むときは、ZRANGEBYSCOREを使います。例のように、`ZRANGEBYSCORE, 40, 70`と指定したときは、40以上70以下のスコアの要素が得られます。
要素の削除にはZREMRANGEBYRANKを用います。`ZREMRANGEBYRANK, 0, 1`は、ランクが0以上1以下の生徒、つまりdaveとeveの削除を意味します。

# ハッシュ型

ハッシュ型は、文字列型の対（フィールドと値）のマップです。Pythonの辞書型のようなものだと思います（値は文字列限定ですが）。コマンド一覧は[リファレンス](http://redis.shibu.jp/commandreference/hashes.html)を参照して下さい。以下は、profileというキーに対し、フィールドと値の対を追加・更新する例を示します。

```go:main.go
package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	// 接続
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 要素の追加
	_, err = conn.Do("HSET", "profile", "name", "yamada")
	_, err = conn.Do("HSET", "profile", "age", "30")
	_, err = conn.Do("HSET", "profile", "country", "america")

	// 要素の追加と更新
	t, err := redis.String(conn.Do("HMSET", "profile", "country", "japan", "city", "tokyo"))
	if err != nil {
		panic(err)
	}
	fmt.Println(t) // OK

	// 要素の取得
	s, err := redis.String(conn.Do("HGET", "profile", "country"))
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // japan

	// 値をインクリメントする
	n, err := redis.Int(conn.Do("HINCRBY", "profile", "age", "2"))
	if err != nil {
		panic(err)
	}
	fmt.Println(n) // 32

	// 全体の取得
	u, err := redis.Strings(conn.Do("HGETALL", "profile"))
	if err != nil {
		panic(err)
	}
	fmt.Println(u) // [name yamada age 32 country japan city tokyo]
}
```

フィールドと値を追加するにはHSETを使います。ここでは戻り値を捨てていますが、フィールドが既に存在するなら0が戻り（値は上書きされます）、しないなら1が戻ります。
HMSETで複数の対を追加することもできます。フィールド1, 値1, フィールド2, 値2, ・・のように、フィールドと値を交互に並べます。
HINCRBYで値の増減を行うことも可能です。値が64bit符号付き整数の範囲内である必要があります。
またHGETALLで全ての対を取得できます。フィールドと値が交互に現れます。

# おわりに

Redisの様々なデータ型をRedigoでの扱い方を見ていきました。

# 参考

* [Redigoを使う(1)　基本的な使い方](https://qiita.com/riverplus/items/c1df770838d3868c3a13)
* [Redigoを使う(3)　トランザクションを行う](https://qiita.com/riverplus/items/5379111adda57f9ac18a)
* [Redigoを使う(4)　コネクションプールを使う](https://qiita.com/riverplus/items/12f9b37cf1795d9bdbb1)
* [Redigoを使う(5)　ユーティリティ関数](https://qiita.com/riverplus/items/a359ba7e74642a870440)
* [Redigoを使う(6)　パブリッシュ／サブスクライブ](https://qiita.com/riverplus/items/e31c6da6f5af28d3a3f7)