Taking a pause from the theory, let's look at how we can use Redis in Go. At the time of writing this book, there are two main Redis clients in Go:
- Redigo (https://github.com/garyburd/redigo) provides a print-like API for Redis commands. It also supports pipelining, publish/subscribe, connection pooling, and scripting. It's easy to use and the reference is located at https://godoc.org/github.com/garyburd/redigo/redis.
- Radix (https://github.com/mediocregopher/radix.v2) provides easy-to-use packages for most Redis commands, including pipelining, connection pooling, publish/subscribe, and scripting, but it also provides clustering support. The Radix.v2 package is broken into six sub-packages (cluster, pool, pubsub, Redis, sentinel, and util).
As an example, we will take a feature where we want to maintain likes about a hotel in Redis. The following struct defines the entity we want to model:
type Hotel struct { Id string Name string City string StarRating int Likes int }
Here, we will use the Radix client. To install it, use the following command:
go get github.com/mediocregopher/radix.v2
The first step is, of course, connecting. This can be done as follows:
conn, err:= redis.Dial("tcp", "localhost:6379") if err != nil { panic(err) } defer conn.Close()
The code connects to localhost. Of course, in production code, you should take this value as a configuration item.
Next, let's look at how we can save a hotel entity in Redis. The following code takes a hotel and saves it in Redis:
func setHotel(conn *redis.Client, h *Hotel) error { resp:= conn.Cmd("HMSET", "hotels:"+h.Id, "name", h.Name, "city", h.City, "likes", h.Likes, "rating", h.StarRating) if resp.Err != nil { fmt.Println("save err", resp.Err) return resp.Err } return nil }
As you can see, we are using the hashes data structure to store likes. This is because we know that the likes attribute will be independently incremented. HMSET is a multiple set for the hash object. Each hotel is identified by the string with the concatenation of "hotels" with the id of the hotel.
The following code gets a hotel with a specific hotel from Redis:
func getHotel(conn *redis.Client, id string) (*Hotel, error) { reply, err:= conn.Cmd("HGETALL", "hotels:"+id).Map() if err != nil { return nil, err } h:= new(Hotel) h.Id = id h.Name = reply["name"] h.City = reply["city"] if h.Likes, err = strconv.Atoi(reply["likes"]); err != nil { fmt.Println("likes err", err) return nil, err } if h.StarRating, err = strconv.Atoi(reply["rating"]); err != nil { fmt.Println("ratings err", err) return nil, err } return h, nil }
Here, we are using the HGETALL command to get all of the fields of a hash. Then, we use the Map() method of the response object to obtain a map of field names to the values. We then construct a hotel object from the individual fields.
Now, coming to the likes, which is a key method that required is to increment counts of a hotel. Along with maintaining counts, we also have a requirement of determining the most-liked hotels. To enable the latter requirement, we use a sorted-set dataset. The first code snippet implements a like for a hotel:
unc incrementLikes(conn *redis.Client, id string) error { // Sanity check to ensure that the hotel exists! exists, err:= conn.Cmd("EXISTS", "hotels:"+id).Int() if err != nil || exists == 0 { return errors.New("no such hotel") } // Use the MULTI command to inform Redis that we are starting a new // transaction. err = conn.Cmd("MULTI").Err if err != nil { return err } // Increment the number of likes for the hotel. in the album hash by 1. // Because we have initiated a MULTI command, this HINCRBY command is queued NOT executed. // We still check the reply's Err field to check if there was an error for the queing err = conn.Cmd("HINCRBY", "hotels:"+id, "likes", 1).Err if err != nil { return err } // Now we increment the leaderboard sorted set err = conn.Cmd("ZINCRBY", "likes", 1, id).Err if err != nil { return err } // Execute both commands in our transaction atomically. // EXEC returns the replies from both commands as an array err = conn.Cmd("EXEC").Err if err != nil { return err } return nil }
This uses the MULTI option of redis to start a transaction and update both the likes for a hotel and the likes sorted set atomically.
The following code snippet gets the top-three liked hotels:
func top3LikedHotels(conn *redis.Client) ([]string, error) { // Use the ZREVRANGE command to fetch the hotels from likes sorted set // with the highest score first // The start and stop values are zero-based indexes, so we use 0 and 2 // respectively to limit the reply to the top three. reply, err:= conn.Cmd("ZREVRANGE", "likes", 0, 2).List() if err != nil { return nil, err } return reply, nil }
The ZREVRANGE command returns the sorted set members in reverse order of rank. Since it returns an array response, we use the List() helper function to convert the response to []string.