“wwangchuanjin 5 年之前
父节点
当前提交
6fc1701dc7
共有 51 个文件被更改,包括 10549 次插入0 次删除
  1. 45 0
      src/github.com/garyburd/redigo/internal/commandinfo.go
  2. 65 0
      src/github.com/garyburd/redigo/internal/redistest/testdb.go
  3. 455 0
      src/github.com/garyburd/redigo/redis/conn.go
  4. 169 0
      src/github.com/garyburd/redigo/redis/doc.go
  5. 117 0
      src/github.com/garyburd/redigo/redis/log.go
  6. 389 0
      src/github.com/garyburd/redigo/redis/pool.go
  7. 129 0
      src/github.com/garyburd/redigo/redis/pubsub.go
  8. 44 0
      src/github.com/garyburd/redigo/redis/redis.go
  9. 312 0
      src/github.com/garyburd/redigo/redis/reply.go
  10. 513 0
      src/github.com/garyburd/redigo/redis/scan.go
  11. 86 0
      src/github.com/garyburd/redigo/redis/script.go
  12. 152 0
      src/github.com/garyburd/redigo/redisx/connmux.go
  13. 17 0
      src/github.com/garyburd/redigo/redisx/doc.go
  14. 5 0
      src/github.com/gomodule/redigo/.github/CONTRIBUTING.md
  15. 1 0
      src/github.com/gomodule/redigo/.github/ISSUE_TEMPLATE.md
  16. 23 0
      src/github.com/gomodule/redigo/.travis.yml
  17. 175 0
      src/github.com/gomodule/redigo/LICENSE
  18. 51 0
      src/github.com/gomodule/redigo/README.markdown
  19. 55 0
      src/github.com/gomodule/redigo/redis/commandinfo.go
  20. 27 0
      src/github.com/gomodule/redigo/redis/commandinfo_test.go
  21. 704 0
      src/github.com/gomodule/redigo/redis/conn.go
  22. 946 0
      src/github.com/gomodule/redigo/redis/conn_test.go
  23. 177 0
      src/github.com/gomodule/redigo/redis/doc.go
  24. 29 0
      src/github.com/gomodule/redigo/redis/go17.go
  25. 9 0
      src/github.com/gomodule/redigo/redis/go18.go
  26. 85 0
      src/github.com/gomodule/redigo/redis/list_test.go
  27. 146 0
      src/github.com/gomodule/redigo/redis/log.go
  28. 621 0
      src/github.com/gomodule/redigo/redis/pool.go
  29. 875 0
      src/github.com/gomodule/redigo/redis/pool_test.go
  30. 148 0
      src/github.com/gomodule/redigo/redis/pubsub.go
  31. 165 0
      src/github.com/gomodule/redigo/redis/pubsub_example_test.go
  32. 74 0
      src/github.com/gomodule/redigo/redis/pubsub_test.go
  33. 117 0
      src/github.com/gomodule/redigo/redis/redis.go
  34. 71 0
      src/github.com/gomodule/redigo/redis/redis_test.go
  35. 479 0
      src/github.com/gomodule/redigo/redis/reply.go
  36. 209 0
      src/github.com/gomodule/redigo/redis/reply_test.go
  37. 630 0
      src/github.com/gomodule/redigo/redis/scan.go
  38. 513 0
      src/github.com/gomodule/redigo/redis/scan_test.go
  39. 91 0
      src/github.com/gomodule/redigo/redis/script.go
  40. 100 0
      src/github.com/gomodule/redigo/redis/script_test.go
  41. 183 0
      src/github.com/gomodule/redigo/redis/test_test.go
  42. 114 0
      src/github.com/gomodule/redigo/redis/zpop_example_test.go
  43. 54 0
      src/github.com/gomodule/redigo/redisx/commandinfo.go
  44. 11 0
      src/github.com/gomodule/redigo/redisx/commandinfo_test.go
  45. 151 0
      src/github.com/gomodule/redigo/redisx/connmux.go
  46. 258 0
      src/github.com/gomodule/redigo/redisx/connmux_test.go
  47. 68 0
      src/github.com/gomodule/redigo/redisx/db_test.go
  48. 17 0
      src/github.com/gomodule/redigo/redisx/doc.go
  49. 70 0
      src/redis/redisloginutil.go
  50. 518 0
      src/redis/redisutil.go
  51. 86 0
      src/redis/redisutil_test.go

+ 45 - 0
src/github.com/garyburd/redigo/internal/commandinfo.go

@@ -0,0 +1,45 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package internal // import "github.com/garyburd/redigo/internal"
+
+import (
+	"strings"
+)
+
+const (
+	WatchState = 1 << iota
+	MultiState
+	SubscribeState
+	MonitorState
+)
+
+type CommandInfo struct {
+	Set, Clear int
+}
+
+var commandInfos = map[string]CommandInfo{
+	"WATCH":      {Set: WatchState},
+	"UNWATCH":    {Clear: WatchState},
+	"MULTI":      {Set: MultiState},
+	"EXEC":       {Clear: WatchState | MultiState},
+	"DISCARD":    {Clear: WatchState | MultiState},
+	"PSUBSCRIBE": {Set: SubscribeState},
+	"SUBSCRIBE":  {Set: SubscribeState},
+	"MONITOR":    {Set: MonitorState},
+}
+
+func LookupCommandInfo(commandName string) CommandInfo {
+	return commandInfos[strings.ToUpper(commandName)]
+}

+ 65 - 0
src/github.com/garyburd/redigo/internal/redistest/testdb.go

@@ -0,0 +1,65 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redistest contains utilities for writing Redigo tests.
+package redistest
+
+import (
+	"errors"
+	"time"
+
+	"github.com/garyburd/redigo/redis"
+)
+
+type testConn struct {
+	redis.Conn
+}
+
+func (t testConn) Close() error {
+	_, err := t.Conn.Do("SELECT", "9")
+	if err != nil {
+		return nil
+	}
+	_, err = t.Conn.Do("FLUSHDB")
+	if err != nil {
+		return err
+	}
+	return t.Conn.Close()
+}
+
+// Dial dials the local Redis server and selects database 9. To prevent
+// stomping on real data, DialTestDB fails if database 9 contains data. The
+// returned connection flushes database 9 on close.
+func Dial() (redis.Conn, error) {
+	c, err := redis.DialTimeout("tcp", ":6379", 0, 1*time.Second, 1*time.Second)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = c.Do("SELECT", "9")
+	if err != nil {
+		return nil, err
+	}
+
+	n, err := redis.Int(c.Do("DBSIZE"))
+	if err != nil {
+		return nil, err
+	}
+
+	if n != 0 {
+		return nil, errors.New("database #9 is not empty, test can not continue")
+	}
+
+	return testConn{c}, nil
+}

+ 455 - 0
src/github.com/garyburd/redigo/redis/conn.go

@@ -0,0 +1,455 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"strconv"
+	"sync"
+	"time"
+)
+
+// conn is the low-level implementation of Conn
+type conn struct {
+
+	// Shared
+	mu      sync.Mutex
+	pending int
+	err     error
+	conn    net.Conn
+
+	// Read
+	readTimeout time.Duration
+	br          *bufio.Reader
+
+	// Write
+	writeTimeout time.Duration
+	bw           *bufio.Writer
+
+	// Scratch space for formatting argument length.
+	// '*' or '$', length, "\r\n"
+	lenScratch [32]byte
+
+	// Scratch space for formatting integers and floats.
+	numScratch [40]byte
+}
+
+// Dial connects to the Redis server at the given network and address.
+func Dial(network, address string) (Conn, error) {
+	dialer := xDialer{}
+	return dialer.Dial(network, address)
+}
+
+// DialTimeout acts like Dial but takes timeouts for establishing the
+// connection to the server, writing a command and reading a reply.
+func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
+	netDialer := net.Dialer{Timeout: connectTimeout}
+	dialer := xDialer{
+		NetDial:      netDialer.Dial,
+		ReadTimeout:  readTimeout,
+		WriteTimeout: writeTimeout,
+	}
+	return dialer.Dial(network, address)
+}
+
+// A Dialer specifies options for connecting to a Redis server.
+type xDialer struct {
+	// NetDial specifies the dial function for creating TCP connections. If
+	// NetDial is nil, then net.Dial is used.
+	NetDial func(network, addr string) (net.Conn, error)
+
+	// ReadTimeout specifies the timeout for reading a single command
+	// reply. If ReadTimeout is zero, then no timeout is used.
+	ReadTimeout time.Duration
+
+	// WriteTimeout specifies the timeout for writing a single command.  If
+	// WriteTimeout is zero, then no timeout is used.
+	WriteTimeout time.Duration
+}
+
+// Dial connects to the Redis server at address on the named network.
+func (d *xDialer) Dial(network, address string) (Conn, error) {
+	dial := d.NetDial
+	if dial == nil {
+		dial = net.Dial
+	}
+	netConn, err := dial(network, address)
+	if err != nil {
+		return nil, err
+	}
+	return &conn{
+		conn:         netConn,
+		bw:           bufio.NewWriter(netConn),
+		br:           bufio.NewReader(netConn),
+		readTimeout:  d.ReadTimeout,
+		writeTimeout: d.WriteTimeout,
+	}, nil
+}
+
+// NewConn returns a new Redigo connection for the given net connection.
+func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
+	return &conn{
+		conn:         netConn,
+		bw:           bufio.NewWriter(netConn),
+		br:           bufio.NewReader(netConn),
+		readTimeout:  readTimeout,
+		writeTimeout: writeTimeout,
+	}
+}
+
+func (c *conn) Close() error {
+	c.mu.Lock()
+	err := c.err
+	if c.err == nil {
+		c.err = errors.New("redigo: closed")
+		err = c.conn.Close()
+	}
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) fatal(err error) error {
+	c.mu.Lock()
+	if c.err == nil {
+		c.err = err
+		// Close connection to force errors on subsequent calls and to unblock
+		// other reader or writer.
+		c.conn.Close()
+	}
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) Err() error {
+	c.mu.Lock()
+	err := c.err
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) writeLen(prefix byte, n int) error {
+	c.lenScratch[len(c.lenScratch)-1] = '\n'
+	c.lenScratch[len(c.lenScratch)-2] = '\r'
+	i := len(c.lenScratch) - 3
+	for {
+		c.lenScratch[i] = byte('0' + n%10)
+		i -= 1
+		n = n / 10
+		if n == 0 {
+			break
+		}
+	}
+	c.lenScratch[i] = prefix
+	_, err := c.bw.Write(c.lenScratch[i:])
+	return err
+}
+
+func (c *conn) writeString(s string) error {
+	c.writeLen('$', len(s))
+	c.bw.WriteString(s)
+	_, err := c.bw.WriteString("\r\n")
+	return err
+}
+
+func (c *conn) writeBytes(p []byte) error {
+	c.writeLen('$', len(p))
+	c.bw.Write(p)
+	_, err := c.bw.WriteString("\r\n")
+	return err
+}
+
+func (c *conn) writeInt64(n int64) error {
+	return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
+}
+
+func (c *conn) writeFloat64(n float64) error {
+	return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
+}
+
+func (c *conn) writeCommand(cmd string, args []interface{}) (err error) {
+	c.writeLen('*', 1+len(args))
+	err = c.writeString(cmd)
+	for _, arg := range args {
+		if err != nil {
+			break
+		}
+		switch arg := arg.(type) {
+		case string:
+			err = c.writeString(arg)
+		case []byte:
+			err = c.writeBytes(arg)
+		case int:
+			err = c.writeInt64(int64(arg))
+		case int64:
+			err = c.writeInt64(arg)
+		case float64:
+			err = c.writeFloat64(arg)
+		case bool:
+			if arg {
+				err = c.writeString("1")
+			} else {
+				err = c.writeString("0")
+			}
+		case nil:
+			err = c.writeString("")
+		default:
+			var buf bytes.Buffer
+			fmt.Fprint(&buf, arg)
+			err = c.writeBytes(buf.Bytes())
+		}
+	}
+	return err
+}
+
+type protocolError string
+
+func (pe protocolError) Error() string {
+	return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
+}
+
+func (c *conn) readLine() ([]byte, error) {
+	p, err := c.br.ReadSlice('\n')
+	if err == bufio.ErrBufferFull {
+		return nil, protocolError("long response line")
+	}
+	if err != nil {
+		return nil, err
+	}
+	i := len(p) - 2
+	if i < 0 || p[i] != '\r' {
+		return nil, protocolError("bad response line terminator")
+	}
+	return p[:i], nil
+}
+
+// parseLen parses bulk string and array lengths.
+func parseLen(p []byte) (int, error) {
+	if len(p) == 0 {
+		return -1, protocolError("malformed length")
+	}
+
+	if p[0] == '-' && len(p) == 2 && p[1] == '1' {
+		// handle $-1 and $-1 null replies.
+		return -1, nil
+	}
+
+	var n int
+	for _, b := range p {
+		n *= 10
+		if b < '0' || b > '9' {
+			return -1, protocolError("illegal bytes in length")
+		}
+		n += int(b - '0')
+	}
+
+	return n, nil
+}
+
+// parseInt parses an integer reply.
+func parseInt(p []byte) (interface{}, error) {
+	if len(p) == 0 {
+		return 0, protocolError("malformed integer")
+	}
+
+	var negate bool
+	if p[0] == '-' {
+		negate = true
+		p = p[1:]
+		if len(p) == 0 {
+			return 0, protocolError("malformed integer")
+		}
+	}
+
+	var n int64
+	for _, b := range p {
+		n *= 10
+		if b < '0' || b > '9' {
+			return 0, protocolError("illegal bytes in length")
+		}
+		n += int64(b - '0')
+	}
+
+	if negate {
+		n = -n
+	}
+	return n, nil
+}
+
+var (
+	okReply   interface{} = "OK"
+	pongReply interface{} = "PONG"
+)
+
+func (c *conn) readReply() (interface{}, error) {
+	line, err := c.readLine()
+	if err != nil {
+		return nil, err
+	}
+	if len(line) == 0 {
+		return nil, protocolError("short response line")
+	}
+	switch line[0] {
+	case '+':
+		switch {
+		case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
+			// Avoid allocation for frequent "+OK" response.
+			return okReply, nil
+		case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
+			// Avoid allocation in PING command benchmarks :)
+			return pongReply, nil
+		default:
+			return string(line[1:]), nil
+		}
+	case '-':
+		return Error(string(line[1:])), nil
+	case ':':
+		return parseInt(line[1:])
+	case '$':
+		n, err := parseLen(line[1:])
+		if n < 0 || err != nil {
+			return nil, err
+		}
+		p := make([]byte, n)
+		_, err = io.ReadFull(c.br, p)
+		if err != nil {
+			return nil, err
+		}
+		if line, err := c.readLine(); err != nil {
+			return nil, err
+		} else if len(line) != 0 {
+			return nil, protocolError("bad bulk string format")
+		}
+		return p, nil
+	case '*':
+		n, err := parseLen(line[1:])
+		if n < 0 || err != nil {
+			return nil, err
+		}
+		r := make([]interface{}, n)
+		for i := range r {
+			r[i], err = c.readReply()
+			if err != nil {
+				return nil, err
+			}
+		}
+		return r, nil
+	}
+	return nil, protocolError("unexpected response line")
+}
+
+func (c *conn) Send(cmd string, args ...interface{}) error {
+	c.mu.Lock()
+	c.pending += 1
+	c.mu.Unlock()
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+	if err := c.writeCommand(cmd, args); err != nil {
+		return c.fatal(err)
+	}
+	return nil
+}
+
+func (c *conn) Flush() error {
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+	if err := c.bw.Flush(); err != nil {
+		return c.fatal(err)
+	}
+	return nil
+}
+
+func (c *conn) Receive() (reply interface{}, err error) {
+	if c.readTimeout != 0 {
+		c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
+	}
+	if reply, err = c.readReply(); err != nil {
+		return nil, c.fatal(err)
+	}
+	// When using pub/sub, the number of receives can be greater than the
+	// number of sends. To enable normal use of the connection after
+	// unsubscribing from all channels, we do not decrement pending to a
+	// negative value.
+	//
+	// The pending field is decremented after the reply is read to handle the
+	// case where Receive is called before Send.
+	c.mu.Lock()
+	if c.pending > 0 {
+		c.pending -= 1
+	}
+	c.mu.Unlock()
+	if err, ok := reply.(Error); ok {
+		return nil, err
+	}
+	return
+}
+
+func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
+	c.mu.Lock()
+	pending := c.pending
+	c.pending = 0
+	c.mu.Unlock()
+
+	if cmd == "" && pending == 0 {
+		return nil, nil
+	}
+
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+
+	if cmd != "" {
+		c.writeCommand(cmd, args)
+	}
+
+	if err := c.bw.Flush(); err != nil {
+		return nil, c.fatal(err)
+	}
+
+	if c.readTimeout != 0 {
+		c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
+	}
+
+	if cmd == "" {
+		reply := make([]interface{}, pending)
+		for i := range reply {
+			r, e := c.readReply()
+			if e != nil {
+				return nil, c.fatal(e)
+			}
+			reply[i] = r
+		}
+		return reply, nil
+	}
+
+	var err error
+	var reply interface{}
+	for i := 0; i <= pending; i++ {
+		var e error
+		if reply, e = c.readReply(); e != nil {
+			return nil, c.fatal(e)
+		}
+		if e, ok := reply.(Error); ok && err == nil {
+			err = e
+		}
+	}
+	return reply, err
+}

+ 169 - 0
src/github.com/garyburd/redigo/redis/doc.go

@@ -0,0 +1,169 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redis is a client for the Redis database.
+//
+// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more
+// documentation about this package.
+//
+// Connections
+//
+// The Conn interface is the primary interface for working with Redis.
+// Applications create connections by calling the Dial, DialWithTimeout or
+// NewConn functions. In the future, functions will be added for creating
+// sharded and other types of connections.
+//
+// The application must call the connection Close method when the application
+// is done with the connection.
+//
+// Executing Commands
+//
+// The Conn interface has a generic method for executing Redis commands:
+//
+//  Do(commandName string, args ...interface{}) (reply interface{}, err error)
+//
+// The Redis command reference (http://redis.io/commands) lists the available
+// commands. An example of using the Redis APPEND command is:
+//
+//  n, err := conn.Do("APPEND", "key", "value")
+//
+// The Do method converts command arguments to binary strings for transmission
+// to the server as follows:
+//
+//  Go Type                 Conversion
+//  []byte                  Sent as is
+//  string                  Sent as is
+//  int, int64              strconv.FormatInt(v)
+//  float64                 strconv.FormatFloat(v, 'g', -1, 64)
+//  bool                    true -> "1", false -> "0"
+//  nil                     ""
+//  all other types         fmt.Print(v)
+//
+// Redis command reply types are represented using the following Go types:
+//
+//  Redis type              Go type
+//  error                   redis.Error
+//  integer                 int64
+//  simple string           string
+//  bulk string             []byte or nil if value not present.
+//  array                   []interface{} or nil if value not present.
+//
+// Use type assertions or the reply helper functions to convert from
+// interface{} to the specific Go type for the command result.
+//
+// Pipelining
+//
+// Connections support pipelining using the Send, Flush and Receive methods.
+//
+//  Send(commandName string, args ...interface{}) error
+//  Flush() error
+//  Receive() (reply interface{}, err error)
+//
+// Send writes the command to the connection's output buffer. Flush flushes the
+// connection's output buffer to the server. Receive reads a single reply from
+// the server. The following example shows a simple pipeline.
+//
+//  c.Send("SET", "foo", "bar")
+//  c.Send("GET", "foo")
+//  c.Flush()
+//  c.Receive() // reply from SET
+//  v, err = c.Receive() // reply from GET
+//
+// The Do method combines the functionality of the Send, Flush and Receive
+// methods. The Do method starts by writing the command and flushing the output
+// buffer. Next, the Do method receives all pending replies including the reply
+// for the command just sent by Do. If any of the received replies is an error,
+// then Do returns the error. If there are no errors, then Do returns the last
+// reply. If the command argument to the Do method is "", then the Do method
+// will flush the output buffer and receive pending replies without sending a
+// command.
+//
+// Use the Send and Do methods to implement pipelined transactions.
+//
+//  c.Send("MULTI")
+//  c.Send("INCR", "foo")
+//  c.Send("INCR", "bar")
+//  r, err := c.Do("EXEC")
+//  fmt.Println(r) // prints [1, 1]
+//
+// Concurrency
+//
+// Connections do not support concurrent calls to the write methods (Send,
+// Flush) or concurrent calls to the read method (Receive). Connections do
+// allow a concurrent reader and writer.
+//
+// Because the Do method combines the functionality of Send, Flush and Receive,
+// the Do method cannot be called concurrently with the other methods.
+//
+// For full concurrent access to Redis, use the thread-safe Pool to get and
+// release connections from within a goroutine.
+//
+// Publish and Subscribe
+//
+// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
+//
+//  c.Send("SUBSCRIBE", "example")
+//  c.Flush()
+//  for {
+//      reply, err := c.Receive()
+//      if err != nil {
+//          return err
+//      }
+//      // process pushed message
+//  }
+//
+// The PubSubConn type wraps a Conn with convenience methods for implementing
+// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
+// send and flush a subscription management command. The receive method
+// converts a pushed message to convenient types for use in a type switch.
+//
+//  psc := redis.PubSubConn{c}
+//  psc.Subscribe("example")
+//  for {
+//      switch v := psc.Receive().(type) {
+//      case redis.Message:
+//          fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
+//      case redis.Subscription:
+//          fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
+//      case error:
+//          return v
+//      }
+//  }
+//
+// Reply Helpers
+//
+// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
+// to a value of a specific type. To allow convenient wrapping of calls to the
+// connection Do and Receive methods, the functions take a second argument of
+// type error.  If the error is non-nil, then the helper function returns the
+// error. If the error is nil, the function converts the reply to the specified
+// type:
+//
+//  exists, err := redis.Bool(c.Do("EXISTS", "foo"))
+//  if err != nil {
+//      // handle error return from c.Do or type conversion error.
+//  }
+//
+// The Scan function converts elements of a array reply to Go types:
+//
+//  var value1 int
+//  var value2 string
+//  reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
+//  if err != nil {
+//      // handle error
+//  }
+//   if _, err := redis.Scan(reply, &value1, &value2); err != nil {
+//      // handle error
+//  }
+package redis // import "github.com/garyburd/redigo/redis"

+ 117 - 0
src/github.com/garyburd/redigo/redis/log.go

@@ -0,0 +1,117 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+)
+
+// NewLoggingConn returns a logging wrapper around a connection.
+func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
+	if prefix != "" {
+		prefix = prefix + "."
+	}
+	return &loggingConn{conn, logger, prefix}
+}
+
+type loggingConn struct {
+	Conn
+	logger *log.Logger
+	prefix string
+}
+
+func (c *loggingConn) Close() error {
+	err := c.Conn.Close()
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
+	c.logger.Output(2, buf.String())
+	return err
+}
+
+func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
+	const chop = 32
+	switch v := v.(type) {
+	case []byte:
+		if len(v) > chop {
+			fmt.Fprintf(buf, "%q...", v[:chop])
+		} else {
+			fmt.Fprintf(buf, "%q", v)
+		}
+	case string:
+		if len(v) > chop {
+			fmt.Fprintf(buf, "%q...", v[:chop])
+		} else {
+			fmt.Fprintf(buf, "%q", v)
+		}
+	case []interface{}:
+		if len(v) == 0 {
+			buf.WriteString("[]")
+		} else {
+			sep := "["
+			fin := "]"
+			if len(v) > chop {
+				v = v[:chop]
+				fin = "...]"
+			}
+			for _, vv := range v {
+				buf.WriteString(sep)
+				c.printValue(buf, vv)
+				sep = ", "
+			}
+			buf.WriteString(fin)
+		}
+	default:
+		fmt.Fprint(buf, v)
+	}
+}
+
+func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
+	if method != "Receive" {
+		buf.WriteString(commandName)
+		for _, arg := range args {
+			buf.WriteString(", ")
+			c.printValue(&buf, arg)
+		}
+	}
+	buf.WriteString(") -> (")
+	if method != "Send" {
+		c.printValue(&buf, reply)
+		buf.WriteString(", ")
+	}
+	fmt.Fprintf(&buf, "%v)", err)
+	c.logger.Output(3, buf.String())
+}
+
+func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
+	reply, err := c.Conn.Do(commandName, args...)
+	c.print("Do", commandName, args, reply, err)
+	return reply, err
+}
+
+func (c *loggingConn) Send(commandName string, args ...interface{}) error {
+	err := c.Conn.Send(commandName, args...)
+	c.print("Send", commandName, args, nil, err)
+	return err
+}
+
+func (c *loggingConn) Receive() (interface{}, error) {
+	reply, err := c.Conn.Receive()
+	c.print("Receive", "", nil, reply, err)
+	return reply, err
+}

+ 389 - 0
src/github.com/garyburd/redigo/redis/pool.go

@@ -0,0 +1,389 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bytes"
+	"container/list"
+	"crypto/rand"
+	"crypto/sha1"
+	"errors"
+	"io"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/garyburd/redigo/internal"
+)
+
+var nowFunc = time.Now // for testing
+
+// ErrPoolExhausted is returned from a pool connection method (Do, Send,
+// Receive, Flush, Err) when the maximum number of database connections in the
+// pool has been reached.
+var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
+
+var (
+	errPoolClosed = errors.New("redigo: connection pool closed")
+	errConnClosed = errors.New("redigo: connection closed")
+)
+
+// Pool maintains a pool of connections. The application calls the Get method
+// to get a connection from the pool and the connection's Close method to
+// return the connection's resources to the pool.
+//
+// The following example shows how to use a pool in a web application. The
+// application creates a pool at application startup and makes it available to
+// request handlers using a global variable.
+//
+//  func newPool(server, password string) *redis.Pool {
+//      return &redis.Pool{
+//          MaxIdle: 3,
+//          IdleTimeout: 240 * time.Second,
+//          Dial: func () (redis.Conn, error) {
+//              c, err := redis.Dial("tcp", server)
+//              if err != nil {
+//                  return nil, err
+//              }
+//              if _, err := c.Do("AUTH", password); err != nil {
+//                  c.Close()
+//                  return nil, err
+//              }
+//              return c, err
+//          },
+//          TestOnBorrow: func(c redis.Conn, t time.Time) error {
+//              _, err := c.Do("PING")
+//              return err
+//          },
+//      }
+//  }
+//
+//  var (
+//      pool *redis.Pool
+//      redisServer = flag.String("redisServer", ":6379", "")
+//      redisPassword = flag.String("redisPassword", "", "")
+//  )
+//
+//  func main() {
+//      flag.Parse()
+//      pool = newPool(*redisServer, *redisPassword)
+//      ...
+//  }
+//
+// A request handler gets a connection from the pool and closes the connection
+// when the handler is done:
+//
+//  func serveHome(w http.ResponseWriter, r *http.Request) {
+//      conn := pool.Get()
+//      defer conn.Close()
+//      ....
+//  }
+//
+type Pool struct {
+
+	// Dial is an application supplied function for creating and configuring a
+	// connection
+	Dial func() (Conn, error)
+
+	// TestOnBorrow is an optional application supplied function for checking
+	// the health of an idle connection before the connection is used again by
+	// the application. Argument t is the time that the connection was returned
+	// to the pool. If the function returns an error, then the connection is
+	// closed.
+	TestOnBorrow func(c Conn, t time.Time) error
+
+	// Maximum number of idle connections in the pool.
+	MaxIdle int
+
+	// Maximum number of connections allocated by the pool at a given time.
+	// When zero, there is no limit on the number of connections in the pool.
+	MaxActive int
+
+	// Close connections after remaining idle for this duration. If the value
+	// is zero, then idle connections are not closed. Applications should set
+	// the timeout to a value less than the server's timeout.
+	IdleTimeout time.Duration
+
+	// If Wait is true and the pool is at the MaxIdle limit, then Get() waits
+	// for a connection to be returned to the pool before returning.
+	Wait bool
+
+	// mu protects fields defined below.
+	mu     sync.Mutex
+	cond   *sync.Cond
+	closed bool
+	active int
+
+	// Stack of idleConn with most recently used at the front.
+	idle list.List
+}
+
+type idleConn struct {
+	c Conn
+	t time.Time
+}
+
+// NewPool creates a new pool. This function is deprecated. Applications should
+// initialize the Pool fields directly as shown in example.
+func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
+	return &Pool{Dial: newFn, MaxIdle: maxIdle}
+}
+
+// Get gets a connection. The application must close the returned connection.
+// This method always returns a valid connection so that applications can defer
+// error handling to the first use of the connection. If there is an error
+// getting an underlying connection, then the connection Err, Do, Send, Flush
+// and Receive methods return that error.
+func (p *Pool) Get() Conn {
+	c, err := p.get()
+	if err != nil {
+		return errorConnection{err}
+	}
+	return &pooledConnection{p: p, c: c}
+}
+
+// ActiveCount returns the number of active connections in the pool.
+func (p *Pool) ActiveCount() int {
+	p.mu.Lock()
+	active := p.active
+	p.mu.Unlock()
+	return active
+}
+
+// Close releases the resources used by the pool.
+func (p *Pool) Close() error {
+	p.mu.Lock()
+	idle := p.idle
+	p.idle.Init()
+	p.closed = true
+	p.active -= idle.Len()
+	if p.cond != nil {
+		p.cond.Broadcast()
+	}
+	p.mu.Unlock()
+	for e := idle.Front(); e != nil; e = e.Next() {
+		e.Value.(idleConn).c.Close()
+	}
+	return nil
+}
+
+// release decrements the active count and signals waiters. The caller must
+// hold p.mu during the call.
+func (p *Pool) release() {
+	p.active -= 1
+	if p.cond != nil {
+		p.cond.Signal()
+	}
+}
+
+// get prunes stale connections and returns a connection from the idle list or
+// creates a new connection.
+func (p *Pool) get() (Conn, error) {
+	p.mu.Lock()
+
+	// Prune stale connections.
+
+	if timeout := p.IdleTimeout; timeout > 0 {
+		for i, n := 0, p.idle.Len(); i < n; i++ {
+			e := p.idle.Back()
+			if e == nil {
+				break
+			}
+			ic := e.Value.(idleConn)
+			if ic.t.Add(timeout).After(nowFunc()) {
+				break
+			}
+			p.idle.Remove(e)
+			p.release()
+			p.mu.Unlock()
+			ic.c.Close()
+			p.mu.Lock()
+		}
+	}
+
+	for {
+
+		// Get idle connection.
+
+		for i, n := 0, p.idle.Len(); i < n; i++ {
+			e := p.idle.Front()
+			if e == nil {
+				break
+			}
+			ic := e.Value.(idleConn)
+			p.idle.Remove(e)
+			test := p.TestOnBorrow
+			p.mu.Unlock()
+			if test == nil || test(ic.c, ic.t) == nil {
+				return ic.c, nil
+			}
+			ic.c.Close()
+			p.mu.Lock()
+			p.release()
+		}
+
+		// Check for pool closed before dialing a new connection.
+
+		if p.closed {
+			p.mu.Unlock()
+			return nil, errors.New("redigo: get on closed pool")
+		}
+
+		// Dial new connection if under limit.
+
+		if p.MaxActive == 0 || p.active < p.MaxActive {
+			dial := p.Dial
+			p.active += 1
+			p.mu.Unlock()
+			c, err := dial()
+			if err != nil {
+				p.mu.Lock()
+				p.release()
+				p.mu.Unlock()
+				c = nil
+			}
+			return c, err
+		}
+
+		if !p.Wait {
+			p.mu.Unlock()
+			return nil, ErrPoolExhausted
+		}
+
+		if p.cond == nil {
+			p.cond = sync.NewCond(&p.mu)
+		}
+		p.cond.Wait()
+	}
+}
+
+func (p *Pool) put(c Conn, forceClose bool) error {
+	err := c.Err()
+	p.mu.Lock()
+	if !p.closed && err == nil && !forceClose {
+		p.idle.PushFront(idleConn{t: nowFunc(), c: c})
+		if p.idle.Len() > p.MaxIdle {
+			c = p.idle.Remove(p.idle.Back()).(idleConn).c
+		} else {
+			c = nil
+		}
+	}
+
+	if c == nil {
+		if p.cond != nil {
+			p.cond.Signal()
+		}
+		p.mu.Unlock()
+		return nil
+	}
+
+	p.release()
+	p.mu.Unlock()
+	return c.Close()
+}
+
+type pooledConnection struct {
+	p     *Pool
+	c     Conn
+	state int
+}
+
+var (
+	sentinel     []byte
+	sentinelOnce sync.Once
+)
+
+func initSentinel() {
+	p := make([]byte, 64)
+	if _, err := rand.Read(p); err == nil {
+		sentinel = p
+	} else {
+		h := sha1.New()
+		io.WriteString(h, "Oops, rand failed. Use time instead.")
+		io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
+		sentinel = h.Sum(nil)
+	}
+}
+
+func (pc *pooledConnection) Close() error {
+	c := pc.c
+	if _, ok := c.(errorConnection); ok {
+		return nil
+	}
+	pc.c = errorConnection{errConnClosed}
+
+	if pc.state&internal.MultiState != 0 {
+		c.Send("DISCARD")
+		pc.state &^= (internal.MultiState | internal.WatchState)
+	} else if pc.state&internal.WatchState != 0 {
+		c.Send("UNWATCH")
+		pc.state &^= internal.WatchState
+	}
+	if pc.state&internal.SubscribeState != 0 {
+		c.Send("UNSUBSCRIBE")
+		c.Send("PUNSUBSCRIBE")
+		// To detect the end of the message stream, ask the server to echo
+		// a sentinel value and read until we see that value.
+		sentinelOnce.Do(initSentinel)
+		c.Send("ECHO", sentinel)
+		c.Flush()
+		for {
+			p, err := c.Receive()
+			if err != nil {
+				break
+			}
+			if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
+				pc.state &^= internal.SubscribeState
+				break
+			}
+		}
+	}
+	c.Do("")
+	pc.p.put(c, pc.state != 0)
+	return nil
+}
+
+func (pc *pooledConnection) Err() error {
+	return pc.c.Err()
+}
+
+func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	ci := internal.LookupCommandInfo(commandName)
+	pc.state = (pc.state | ci.Set) &^ ci.Clear
+	return pc.c.Do(commandName, args...)
+}
+
+func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
+	ci := internal.LookupCommandInfo(commandName)
+	pc.state = (pc.state | ci.Set) &^ ci.Clear
+	return pc.c.Send(commandName, args...)
+}
+
+func (pc *pooledConnection) Flush() error {
+	return pc.c.Flush()
+}
+
+func (pc *pooledConnection) Receive() (reply interface{}, err error) {
+	return pc.c.Receive()
+}
+
+type errorConnection struct{ err error }
+
+func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
+func (ec errorConnection) Send(string, ...interface{}) error              { return ec.err }
+func (ec errorConnection) Err() error                                     { return ec.err }
+func (ec errorConnection) Close() error                                   { return ec.err }
+func (ec errorConnection) Flush() error                                   { return ec.err }
+func (ec errorConnection) Receive() (interface{}, error)                  { return nil, ec.err }

+ 129 - 0
src/github.com/garyburd/redigo/redis/pubsub.go

@@ -0,0 +1,129 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+)
+
+// Subscription represents a subscribe or unsubscribe notification.
+type Subscription struct {
+
+	// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
+	Kind string
+
+	// The channel that was changed.
+	Channel string
+
+	// The current number of subscriptions for connection.
+	Count int
+}
+
+// Message represents a message notification.
+type Message struct {
+
+	// The originating channel.
+	Channel string
+
+	// The message data.
+	Data []byte
+}
+
+// PMessage represents a pmessage notification.
+type PMessage struct {
+
+	// The matched pattern.
+	Pattern string
+
+	// The originating channel.
+	Channel string
+
+	// The message data.
+	Data []byte
+}
+
+// PubSubConn wraps a Conn with convenience methods for subscribers.
+type PubSubConn struct {
+	Conn Conn
+}
+
+// Close closes the connection.
+func (c PubSubConn) Close() error {
+	return c.Conn.Close()
+}
+
+// Subscribe subscribes the connection to the specified channels.
+func (c PubSubConn) Subscribe(channel ...interface{}) error {
+	c.Conn.Send("SUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// PSubscribe subscribes the connection to the given patterns.
+func (c PubSubConn) PSubscribe(channel ...interface{}) error {
+	c.Conn.Send("PSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// Unsubscribe unsubscribes the connection from the given channels, or from all
+// of them if none is given.
+func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
+	c.Conn.Send("UNSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// PUnsubscribe unsubscribes the connection from the given patterns, or from all
+// of them if none is given.
+func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
+	c.Conn.Send("PUNSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// Receive returns a pushed message as a Subscription, Message, PMessage or
+// error. The return value is intended to be used directly in a type switch as
+// illustrated in the PubSubConn example.
+func (c PubSubConn) Receive() interface{} {
+	reply, err := Values(c.Conn.Receive())
+	if err != nil {
+		return err
+	}
+
+	var kind string
+	reply, err = Scan(reply, &kind)
+	if err != nil {
+		return err
+	}
+
+	switch kind {
+	case "message":
+		var m Message
+		if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
+			return err
+		}
+		return m
+	case "pmessage":
+		var pm PMessage
+		if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
+			return err
+		}
+		return pm
+	case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
+		s := Subscription{Kind: kind}
+		if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
+			return err
+		}
+		return s
+	}
+	return errors.New("redigo: unknown pubsub notification")
+}

+ 44 - 0
src/github.com/garyburd/redigo/redis/redis.go

@@ -0,0 +1,44 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+// Error represents an error returned in a command reply.
+type Error string
+
+func (err Error) Error() string { return string(err) }
+
+// Conn represents a connection to a Redis server.
+type Conn interface {
+	// Close closes the connection.
+	Close() error
+
+	// Err returns a non-nil value if the connection is broken. The returned
+	// value is either the first non-nil value returned from the underlying
+	// network connection or a protocol parsing error. Applications should
+	// close broken connections.
+	Err() error
+
+	// Do sends a command to the server and returns the received reply.
+	Do(commandName string, args ...interface{}) (reply interface{}, err error)
+
+	// Send writes the command to the client's output buffer.
+	Send(commandName string, args ...interface{}) error
+
+	// Flush flushes the output buffer to the Redis server.
+	Flush() error
+
+	// Receive receives a single reply from the Redis server
+	Receive() (reply interface{}, err error)
+}

+ 312 - 0
src/github.com/garyburd/redigo/redis/reply.go

@@ -0,0 +1,312 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+)
+
+// ErrNil indicates that a reply value is nil.
+var ErrNil = errors.New("redigo: nil returned")
+
+// Int is a helper that converts a command reply to an integer. If err is not
+// equal to nil, then Int returns 0, err. Otherwise, Int converts the
+// reply to an int as follows:
+//
+//  Reply type    Result
+//  integer       int(reply), nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Int(reply interface{}, err error) (int, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		x := int(reply)
+		if int64(x) != reply {
+			return 0, strconv.ErrRange
+		}
+		return x, nil
+	case []byte:
+		n, err := strconv.ParseInt(string(reply), 10, 0)
+		return int(n), err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
+}
+
+// Int64 is a helper that converts a command reply to 64 bit integer. If err is
+// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
+// reply to an int64 as follows:
+//
+//  Reply type    Result
+//  integer       reply, nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Int64(reply interface{}, err error) (int64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		return reply, nil
+	case []byte:
+		n, err := strconv.ParseInt(string(reply), 10, 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
+}
+
+var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
+
+// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
+// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
+// reply to an int64 as follows:
+//
+//  Reply type    Result
+//  integer       reply, nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Uint64(reply interface{}, err error) (uint64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		if reply < 0 {
+			return 0, errNegativeInt
+		}
+		return uint64(reply), nil
+	case []byte:
+		n, err := strconv.ParseUint(string(reply), 10, 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
+}
+
+// Float64 is a helper that converts a command reply to 64 bit float. If err is
+// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
+// the reply to an int as follows:
+//
+//  Reply type    Result
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Float64(reply interface{}, err error) (float64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		n, err := strconv.ParseFloat(string(reply), 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
+}
+
+// String is a helper that converts a command reply to a string. If err is not
+// equal to nil, then String returns "", err. Otherwise String converts the
+// reply to a string as follows:
+//
+//  Reply type      Result
+//  bulk string     string(reply), nil
+//  simple string   reply, nil
+//  nil             "",  ErrNil
+//  other           "",  error
+func String(reply interface{}, err error) (string, error) {
+	if err != nil {
+		return "", err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		return string(reply), nil
+	case string:
+		return reply, nil
+	case nil:
+		return "", ErrNil
+	case Error:
+		return "", reply
+	}
+	return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
+}
+
+// Bytes is a helper that converts a command reply to a slice of bytes. If err
+// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
+// the reply to a slice of bytes as follows:
+//
+//  Reply type      Result
+//  bulk string     reply, nil
+//  simple string   []byte(reply), nil
+//  nil             nil, ErrNil
+//  other           nil, error
+func Bytes(reply interface{}, err error) ([]byte, error) {
+	if err != nil {
+		return nil, err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		return reply, nil
+	case string:
+		return []byte(reply), nil
+	case nil:
+		return nil, ErrNil
+	case Error:
+		return nil, reply
+	}
+	return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
+}
+
+// Bool is a helper that converts a command reply to a boolean. If err is not
+// equal to nil, then Bool returns false, err. Otherwise Bool converts the
+// reply to boolean as follows:
+//
+//  Reply type      Result
+//  integer         value != 0, nil
+//  bulk string     strconv.ParseBool(reply)
+//  nil             false, ErrNil
+//  other           false, error
+func Bool(reply interface{}, err error) (bool, error) {
+	if err != nil {
+		return false, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		return reply != 0, nil
+	case []byte:
+		return strconv.ParseBool(string(reply))
+	case nil:
+		return false, ErrNil
+	case Error:
+		return false, reply
+	}
+	return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
+}
+
+// MultiBulk is deprecated. Use Values.
+func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
+
+// Values is a helper that converts an array command reply to a []interface{}.
+// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
+// converts the reply as follows:
+//
+//  Reply type      Result
+//  array           reply, nil
+//  nil             nil, ErrNil
+//  other           nil, error
+func Values(reply interface{}, err error) ([]interface{}, error) {
+	if err != nil {
+		return nil, err
+	}
+	switch reply := reply.(type) {
+	case []interface{}:
+		return reply, nil
+	case nil:
+		return nil, ErrNil
+	case Error:
+		return nil, reply
+	}
+	return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
+}
+
+// Strings is a helper that converts an array command reply to a []string. If
+// err is not equal to nil, then Strings returns nil, err. Nil array items are
+// converted to "" in the output slice. Strings returns an error if an array
+// item is not a bulk string or nil.
+func Strings(reply interface{}, err error) ([]string, error) {
+	if err != nil {
+		return nil, err
+	}
+	switch reply := reply.(type) {
+	case []interface{}:
+		result := make([]string, len(reply))
+		for i := range reply {
+			if reply[i] == nil {
+				continue
+			}
+			p, ok := reply[i].([]byte)
+			if !ok {
+				return nil, fmt.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i])
+			}
+			result[i] = string(p)
+		}
+		return result, nil
+	case nil:
+		return nil, ErrNil
+	case Error:
+		return nil, reply
+	}
+	return nil, fmt.Errorf("redigo: unexpected type for Strings, got type %T", reply)
+}
+
+// Ints is a helper that converts an array command reply to a []int. If
+// err is not equal to nil, then Ints returns nil, err.
+func Ints(reply interface{}, err error) ([]int, error) {
+	var ints []int
+	if reply == nil {
+		return ints, ErrNil
+	}
+	values, err := Values(reply, err)
+	if err != nil {
+		return ints, err
+	}
+	if err := ScanSlice(values, &ints); err != nil {
+		return ints, err
+	}
+	return ints, nil
+}
+
+// StringMap is a helper that converts an array of strings (alternating key, value)
+// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
+// Requires an even number of values in result.
+func StringMap(result interface{}, err error) (map[string]string, error) {
+	values, err := Values(result, err)
+	if err != nil {
+		return nil, err
+	}
+	if len(values)%2 != 0 {
+		return nil, errors.New("redigo: StringMap expects even number of values result")
+	}
+	m := make(map[string]string, len(values)/2)
+	for i := 0; i < len(values); i += 2 {
+		key, okKey := values[i].([]byte)
+		value, okValue := values[i+1].([]byte)
+		if !okKey || !okValue {
+			return nil, errors.New("redigo: ScanMap key not a bulk string value")
+		}
+		m[string(key)] = string(value)
+	}
+	return m, nil
+}

+ 513 - 0
src/github.com/garyburd/redigo/redis/scan.go

@@ -0,0 +1,513 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+)
+
+func ensureLen(d reflect.Value, n int) {
+	if n > d.Cap() {
+		d.Set(reflect.MakeSlice(d.Type(), n, n))
+	} else {
+		d.SetLen(n)
+	}
+}
+
+func cannotConvert(d reflect.Value, s interface{}) error {
+	return fmt.Errorf("redigo: Scan cannot convert from %s to %s",
+		reflect.TypeOf(s), d.Type())
+}
+
+func convertAssignBytes(d reflect.Value, s []byte) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Float32, reflect.Float64:
+		var x float64
+		x, err = strconv.ParseFloat(string(s), d.Type().Bits())
+		d.SetFloat(x)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		var x int64
+		x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
+		d.SetInt(x)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		var x uint64
+		x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
+		d.SetUint(x)
+	case reflect.Bool:
+		var x bool
+		x, err = strconv.ParseBool(string(s))
+		d.SetBool(x)
+	case reflect.String:
+		d.SetString(string(s))
+	case reflect.Slice:
+		if d.Type().Elem().Kind() != reflect.Uint8 {
+			err = cannotConvert(d, s)
+		} else {
+			d.SetBytes(s)
+		}
+	default:
+		err = cannotConvert(d, s)
+	}
+	return
+}
+
+func convertAssignInt(d reflect.Value, s int64) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		d.SetInt(s)
+		if d.Int() != s {
+			err = strconv.ErrRange
+			d.SetInt(0)
+		}
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		if s < 0 {
+			err = strconv.ErrRange
+		} else {
+			x := uint64(s)
+			d.SetUint(x)
+			if d.Uint() != x {
+				err = strconv.ErrRange
+				d.SetUint(0)
+			}
+		}
+	case reflect.Bool:
+		d.SetBool(s != 0)
+	default:
+		err = cannotConvert(d, s)
+	}
+	return
+}
+
+func convertAssignValue(d reflect.Value, s interface{}) (err error) {
+	switch s := s.(type) {
+	case []byte:
+		err = convertAssignBytes(d, s)
+	case int64:
+		err = convertAssignInt(d, s)
+	default:
+		err = cannotConvert(d, s)
+	}
+	return err
+}
+
+func convertAssignValues(d reflect.Value, s []interface{}) error {
+	if d.Type().Kind() != reflect.Slice {
+		return cannotConvert(d, s)
+	}
+	ensureLen(d, len(s))
+	for i := 0; i < len(s); i++ {
+		if err := convertAssignValue(d.Index(i), s[i]); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func convertAssign(d interface{}, s interface{}) (err error) {
+	// Handle the most common destination types using type switches and
+	// fall back to reflection for all other types.
+	switch s := s.(type) {
+	case nil:
+		// ingore
+	case []byte:
+		switch d := d.(type) {
+		case *string:
+			*d = string(s)
+		case *int:
+			*d, err = strconv.Atoi(string(s))
+		case *bool:
+			*d, err = strconv.ParseBool(string(s))
+		case *[]byte:
+			*d = s
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignBytes(d.Elem(), s)
+			}
+		}
+	case int64:
+		switch d := d.(type) {
+		case *int:
+			x := int(s)
+			if int64(x) != s {
+				err = strconv.ErrRange
+				x = 0
+			}
+			*d = x
+		case *bool:
+			*d = s != 0
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignInt(d.Elem(), s)
+			}
+		}
+	case []interface{}:
+		switch d := d.(type) {
+		case *[]interface{}:
+			*d = s
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignValues(d.Elem(), s)
+			}
+		}
+	case Error:
+		err = s
+	default:
+		err = cannotConvert(reflect.ValueOf(d), s)
+	}
+	return
+}
+
+// Scan copies from src to the values pointed at by dest.
+//
+// The values pointed at by dest must be an integer, float, boolean, string,
+// []byte, interface{} or slices of these types. Scan uses the standard strconv
+// package to convert bulk strings to numeric and boolean types.
+//
+// If a dest value is nil, then the corresponding src value is skipped.
+//
+// If a src element is nil, then the corresponding dest value is not modified.
+//
+// To enable easy use of Scan in a loop, Scan returns the slice of src
+// following the copied values.
+func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
+	if len(src) < len(dest) {
+		return nil, errors.New("redigo: Scan array short")
+	}
+	var err error
+	for i, d := range dest {
+		err = convertAssign(d, src[i])
+		if err != nil {
+			break
+		}
+	}
+	return src[len(dest):], err
+}
+
+type fieldSpec struct {
+	name  string
+	index []int
+	//omitEmpty bool
+}
+
+type structSpec struct {
+	m map[string]*fieldSpec
+	l []*fieldSpec
+}
+
+func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
+	return ss.m[string(name)]
+}
+
+func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
+		switch {
+		case f.PkgPath != "":
+			// Ignore unexported fields.
+		case f.Anonymous:
+			// TODO: Handle pointers. Requires change to decoder and
+			// protection against infinite recursion.
+			if f.Type.Kind() == reflect.Struct {
+				compileStructSpec(f.Type, depth, append(index, i), ss)
+			}
+		default:
+			fs := &fieldSpec{name: f.Name}
+			tag := f.Tag.Get("redis")
+			p := strings.Split(tag, ",")
+			if len(p) > 0 {
+				if p[0] == "-" {
+					continue
+				}
+				if len(p[0]) > 0 {
+					fs.name = p[0]
+				}
+				for _, s := range p[1:] {
+					switch s {
+					//case "omitempty":
+					//  fs.omitempty = true
+					default:
+						panic(errors.New("redigo: unknown field flag " + s + " for type " + t.Name()))
+					}
+				}
+			}
+			d, found := depth[fs.name]
+			if !found {
+				d = 1 << 30
+			}
+			switch {
+			case len(index) == d:
+				// At same depth, remove from result.
+				delete(ss.m, fs.name)
+				j := 0
+				for i := 0; i < len(ss.l); i++ {
+					if fs.name != ss.l[i].name {
+						ss.l[j] = ss.l[i]
+						j += 1
+					}
+				}
+				ss.l = ss.l[:j]
+			case len(index) < d:
+				fs.index = make([]int, len(index)+1)
+				copy(fs.index, index)
+				fs.index[len(index)] = i
+				depth[fs.name] = len(index)
+				ss.m[fs.name] = fs
+				ss.l = append(ss.l, fs)
+			}
+		}
+	}
+}
+
+var (
+	structSpecMutex  sync.RWMutex
+	structSpecCache  = make(map[reflect.Type]*structSpec)
+	defaultFieldSpec = &fieldSpec{}
+)
+
+func structSpecForType(t reflect.Type) *structSpec {
+
+	structSpecMutex.RLock()
+	ss, found := structSpecCache[t]
+	structSpecMutex.RUnlock()
+	if found {
+		return ss
+	}
+
+	structSpecMutex.Lock()
+	defer structSpecMutex.Unlock()
+	ss, found = structSpecCache[t]
+	if found {
+		return ss
+	}
+
+	ss = &structSpec{m: make(map[string]*fieldSpec)}
+	compileStructSpec(t, make(map[string]int), nil, ss)
+	structSpecCache[t] = ss
+	return ss
+}
+
+var errScanStructValue = errors.New("redigo: ScanStruct value must be non-nil pointer to a struct")
+
+// ScanStruct scans alternating names and values from src to a struct. The
+// HGETALL and CONFIG GET commands return replies in this format.
+//
+// ScanStruct uses exported field names to match values in the response. Use
+// 'redis' field tag to override the name:
+//
+//      Field int `redis:"myName"`
+//
+// Fields with the tag redis:"-" are ignored.
+//
+// Integer, float, boolean, string and []byte fields are supported. Scan uses the
+// standard strconv package to convert bulk string values to numeric and
+// boolean types.
+//
+// If a src element is nil, then the corresponding field is not modified.
+func ScanStruct(src []interface{}, dest interface{}) error {
+	d := reflect.ValueOf(dest)
+	if d.Kind() != reflect.Ptr || d.IsNil() {
+		return errScanStructValue
+	}
+	d = d.Elem()
+	if d.Kind() != reflect.Struct {
+		return errScanStructValue
+	}
+	ss := structSpecForType(d.Type())
+
+	if len(src)%2 != 0 {
+		return errors.New("redigo: ScanStruct expects even number of values in values")
+	}
+
+	for i := 0; i < len(src); i += 2 {
+		s := src[i+1]
+		if s == nil {
+			continue
+		}
+		name, ok := src[i].([]byte)
+		if !ok {
+			return errors.New("redigo: ScanStruct key not a bulk string value")
+		}
+		fs := ss.fieldSpec(name)
+		if fs == nil {
+			continue
+		}
+		if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+var (
+	errScanSliceValue = errors.New("redigo: ScanSlice dest must be non-nil pointer to a struct")
+)
+
+// ScanSlice scans src to the slice pointed to by dest. The elements the dest
+// slice must be integer, float, boolean, string, struct or pointer to struct
+// values.
+//
+// Struct fields must be integer, float, boolean or string values. All struct
+// fields are used unless a subset is specified using fieldNames.
+func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
+	d := reflect.ValueOf(dest)
+	if d.Kind() != reflect.Ptr || d.IsNil() {
+		return errScanSliceValue
+	}
+	d = d.Elem()
+	if d.Kind() != reflect.Slice {
+		return errScanSliceValue
+	}
+
+	isPtr := false
+	t := d.Type().Elem()
+	if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
+		isPtr = true
+		t = t.Elem()
+	}
+
+	if t.Kind() != reflect.Struct {
+		ensureLen(d, len(src))
+		for i, s := range src {
+			if s == nil {
+				continue
+			}
+			if err := convertAssignValue(d.Index(i), s); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	ss := structSpecForType(t)
+	fss := ss.l
+	if len(fieldNames) > 0 {
+		fss = make([]*fieldSpec, len(fieldNames))
+		for i, name := range fieldNames {
+			fss[i] = ss.m[name]
+			if fss[i] == nil {
+				return errors.New("redigo: ScanSlice bad field name " + name)
+			}
+		}
+	}
+
+	if len(fss) == 0 {
+		return errors.New("redigo: ScanSlice no struct fields")
+	}
+
+	n := len(src) / len(fss)
+	if n*len(fss) != len(src) {
+		return errors.New("redigo: ScanSlice length not a multiple of struct field count")
+	}
+
+	ensureLen(d, n)
+	for i := 0; i < n; i++ {
+		d := d.Index(i)
+		if isPtr {
+			if d.IsNil() {
+				d.Set(reflect.New(t))
+			}
+			d = d.Elem()
+		}
+		for j, fs := range fss {
+			s := src[i*len(fss)+j]
+			if s == nil {
+				continue
+			}
+			if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// Args is a helper for constructing command arguments from structured values.
+type Args []interface{}
+
+// Add returns the result of appending value to args.
+func (args Args) Add(value ...interface{}) Args {
+	return append(args, value...)
+}
+
+// AddFlat returns the result of appending the flattened value of v to args.
+//
+// Maps are flattened by appending the alternating keys and map values to args.
+//
+// Slices are flattened by appending the slice elements to args.
+//
+// Structs are flattened by appending the alternating names and values of
+// exported fields to args. If v is a nil struct pointer, then nothing is
+// appended. The 'redis' field tag overrides struct field names. See ScanStruct
+// for more information on the use of the 'redis' field tag.
+//
+// Other types are appended to args as is.
+func (args Args) AddFlat(v interface{}) Args {
+	rv := reflect.ValueOf(v)
+	switch rv.Kind() {
+	case reflect.Struct:
+		args = flattenStruct(args, rv)
+	case reflect.Slice:
+		for i := 0; i < rv.Len(); i++ {
+			args = append(args, rv.Index(i).Interface())
+		}
+	case reflect.Map:
+		for _, k := range rv.MapKeys() {
+			args = append(args, k.Interface(), rv.MapIndex(k).Interface())
+		}
+	case reflect.Ptr:
+		if rv.Type().Elem().Kind() == reflect.Struct {
+			if !rv.IsNil() {
+				args = flattenStruct(args, rv.Elem())
+			}
+		} else {
+			args = append(args, v)
+		}
+	default:
+		args = append(args, v)
+	}
+	return args
+}
+
+func flattenStruct(args Args, v reflect.Value) Args {
+	ss := structSpecForType(v.Type())
+	for _, fs := range ss.l {
+		fv := v.FieldByIndex(fs.index)
+		args = append(args, fs.name, fv.Interface())
+	}
+	return args
+}

+ 86 - 0
src/github.com/garyburd/redigo/redis/script.go

@@ -0,0 +1,86 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+	"io"
+	"strings"
+)
+
+// Script encapsulates the source, hash and key count for a Lua script. See
+// http://redis.io/commands/eval for information on scripts in Redis.
+type Script struct {
+	keyCount int
+	src      string
+	hash     string
+}
+
+// NewScript returns a new script object. If keyCount is greater than or equal
+// to zero, then the count is automatically inserted in the EVAL command
+// argument list. If keyCount is less than zero, then the application supplies
+// the count as the first value in the keysAndArgs argument to the Do, Send and
+// SendHash methods.
+func NewScript(keyCount int, src string) *Script {
+	h := sha1.New()
+	io.WriteString(h, src)
+	return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
+}
+
+func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
+	var args []interface{}
+	if s.keyCount < 0 {
+		args = make([]interface{}, 1+len(keysAndArgs))
+		args[0] = spec
+		copy(args[1:], keysAndArgs)
+	} else {
+		args = make([]interface{}, 2+len(keysAndArgs))
+		args[0] = spec
+		args[1] = s.keyCount
+		copy(args[2:], keysAndArgs)
+	}
+	return args
+}
+
+// Do evaluates the script. Under the covers, Do optimistically evaluates the
+// script using the EVALSHA command. If the command fails because the script is
+// not loaded, then Do evaluates the script using the EVAL command (thus
+// causing the script to load).
+func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
+	v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
+	if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
+		v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
+	}
+	return v, err
+}
+
+// SendHash evaluates the script without waiting for the reply. The script is
+// evaluated with the EVALSHA command. The application must ensure that the
+// script is loaded by a previous call to Send, Do or Load methods.
+func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
+	return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
+}
+
+// Send evaluates the script without waiting for the reply.
+func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
+	return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
+}
+
+// Load loads the script without evaluating it.
+func (s *Script) Load(c Conn) error {
+	_, err := c.Do("SCRIPT", "LOAD", s.src)
+	return err
+}

+ 152 - 0
src/github.com/garyburd/redigo/redisx/connmux.go

@@ -0,0 +1,152 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redisx
+
+import (
+	"errors"
+	"sync"
+
+	"github.com/garyburd/redigo/internal"
+	"github.com/garyburd/redigo/redis"
+)
+
+// ConnMux multiplexes one or more connections to a single underlying
+// connection. The ConnMux connections do not support concurrency, commands
+// that associate server side state with the connection or commands that put
+// the connection in a special mode.
+type ConnMux struct {
+	c redis.Conn
+
+	sendMu sync.Mutex
+	sendID uint
+
+	recvMu   sync.Mutex
+	recvID   uint
+	recvWait map[uint]chan struct{}
+}
+
+func NewConnMux(c redis.Conn) *ConnMux {
+	return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})}
+}
+
+// Get gets a connection. The application must close the returned connection.
+func (p *ConnMux) Get() redis.Conn {
+	c := &muxConn{p: p}
+	c.ids = c.buf[:0]
+	return c
+}
+
+// Close closes the underlying connection.
+func (p *ConnMux) Close() error {
+	return p.c.Close()
+}
+
+type muxConn struct {
+	p   *ConnMux
+	ids []uint
+	buf [8]uint
+}
+
+func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error {
+	if internal.LookupCommandInfo(cmd).Set != 0 {
+		return errors.New("command not supported by mux pool")
+	}
+	p := c.p
+	p.sendMu.Lock()
+	id := p.sendID
+	c.ids = append(c.ids, id)
+	p.sendID++
+	err := p.c.Send(cmd, args...)
+	if flush {
+		err = p.c.Flush()
+	}
+	p.sendMu.Unlock()
+	return err
+}
+
+func (c *muxConn) Send(cmd string, args ...interface{}) error {
+	return c.send(false, cmd, args...)
+}
+
+func (c *muxConn) Flush() error {
+	p := c.p
+	p.sendMu.Lock()
+	err := p.c.Flush()
+	p.sendMu.Unlock()
+	return err
+}
+
+func (c *muxConn) Receive() (interface{}, error) {
+	if len(c.ids) == 0 {
+		return nil, errors.New("mux pool underflow")
+	}
+
+	id := c.ids[0]
+	c.ids = c.ids[1:]
+	if len(c.ids) == 0 {
+		c.ids = c.buf[:0]
+	}
+
+	p := c.p
+	p.recvMu.Lock()
+	if p.recvID != id {
+		ch := make(chan struct{})
+		p.recvWait[id] = ch
+		p.recvMu.Unlock()
+		<-ch
+		p.recvMu.Lock()
+		if p.recvID != id {
+			panic("out of sync")
+		}
+	}
+
+	v, err := p.c.Receive()
+
+	id++
+	p.recvID = id
+	ch, ok := p.recvWait[id]
+	if ok {
+		delete(p.recvWait, id)
+	}
+	p.recvMu.Unlock()
+	if ok {
+		ch <- struct{}{}
+	}
+
+	return v, err
+}
+
+func (c *muxConn) Close() error {
+	var err error
+	if len(c.ids) == 0 {
+		return nil
+	}
+	c.Flush()
+	for _ = range c.ids {
+		_, err = c.Receive()
+	}
+	return err
+}
+
+func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) {
+	if err := c.send(true, cmd, args...); err != nil {
+		return nil, err
+	}
+	return c.Receive()
+}
+
+func (c *muxConn) Err() error {
+	return c.p.c.Err()
+}

+ 17 - 0
src/github.com/garyburd/redigo/redisx/doc.go

@@ -0,0 +1,17 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redisx contains experimental features for Redigo. Features in this
+// package may be modified or deleted at any time.
+package redisx // import "github.com/garyburd/redigo/redisx"

+ 5 - 0
src/github.com/gomodule/redigo/.github/CONTRIBUTING.md

@@ -0,0 +1,5 @@
+Ask questions at
+[StackOverflow](https://stackoverflow.com/questions/ask?tags=go+redis).
+
+[Open an issue](https://github.com/gomodule/redigo/issues/new) to discuss your
+plans before doing any work on Redigo.

+ 1 - 0
src/github.com/gomodule/redigo/.github/ISSUE_TEMPLATE.md

@@ -0,0 +1 @@
+Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis

+ 23 - 0
src/github.com/gomodule/redigo/.travis.yml

@@ -0,0 +1,23 @@
+language: go
+sudo: false
+services:
+  - redis-server
+
+go:
+  - 1.7.x
+  - 1.8.x
+  - 1.9.x
+  - 1.10.x
+  - 1.11.x
+  - 1.12.x
+  - tip
+
+matrix:
+  allow_failures:
+    - go: tip
+
+script:
+  - go get -t -v ./...
+  - diff -u <(echo -n) <(gofmt -d .)
+  - go vet $(go list ./... | grep -v /vendor/)
+  - go test -v -race ./...

+ 175 - 0
src/github.com/gomodule/redigo/LICENSE

@@ -0,0 +1,175 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.

+ 51 - 0
src/github.com/gomodule/redigo/README.markdown

@@ -0,0 +1,51 @@
+Redigo
+======
+
+[![Build Status](https://travis-ci.org/gomodule/redigo.svg?branch=master)](https://travis-ci.org/gomodule/redigo)
+[![GoDoc](https://godoc.org/github.com/gomodule/redigo/redis?status.svg)](https://godoc.org/github.com/gomodule/redigo/redis)
+
+Redigo is a [Go](http://golang.org/) client for the [Redis](http://redis.io/) database.
+
+Features
+-------
+
+* A [Print-like](http://godoc.org/github.com/gomodule/redigo/redis#hdr-Executing_Commands) API with support for all Redis commands.
+* [Pipelining](http://godoc.org/github.com/gomodule/redigo/redis#hdr-Pipelining), including pipelined transactions.
+* [Publish/Subscribe](http://godoc.org/github.com/gomodule/redigo/redis#hdr-Publish_and_Subscribe).
+* [Connection pooling](http://godoc.org/github.com/gomodule/redigo/redis#Pool).
+* [Script helper type](http://godoc.org/github.com/gomodule/redigo/redis#Script) with optimistic use of EVALSHA.
+* [Helper functions](http://godoc.org/github.com/gomodule/redigo/redis#hdr-Reply_Helpers) for working with command replies.
+
+Documentation
+-------------
+
+- [API Reference](http://godoc.org/github.com/gomodule/redigo/redis)
+- [FAQ](https://github.com/gomodule/redigo/wiki/FAQ)
+- [Examples](https://godoc.org/github.com/gomodule/redigo/redis#pkg-examples)
+
+Installation
+------------
+
+Install Redigo using the "go get" command:
+
+    go get github.com/gomodule/redigo/redis
+
+The Go distribution is Redigo's only dependency.
+
+Related Projects
+----------------
+
+- [rafaeljusto/redigomock](https://godoc.org/github.com/rafaeljusto/redigomock) - A mock library for Redigo.
+- [chasex/redis-go-cluster](https://github.com/chasex/redis-go-cluster) - A Redis cluster client implementation.
+- [FZambia/sentinel](https://github.com/FZambia/sentinel) - Redis Sentinel support for Redigo
+- [mna/redisc](https://github.com/mna/redisc) - Redis Cluster client built on top of Redigo
+
+Contributing
+------------
+
+See [CONTRIBUTING.md](https://github.com/gomodule/redigo/blob/master/.github/CONTRIBUTING.md).
+
+License
+-------
+
+Redigo is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).

+ 55 - 0
src/github.com/gomodule/redigo/redis/commandinfo.go

@@ -0,0 +1,55 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"strings"
+)
+
+const (
+	connectionWatchState = 1 << iota
+	connectionMultiState
+	connectionSubscribeState
+	connectionMonitorState
+)
+
+type commandInfo struct {
+	// Set or Clear these states on connection.
+	Set, Clear int
+}
+
+var commandInfos = map[string]commandInfo{
+	"WATCH":      {Set: connectionWatchState},
+	"UNWATCH":    {Clear: connectionWatchState},
+	"MULTI":      {Set: connectionMultiState},
+	"EXEC":       {Clear: connectionWatchState | connectionMultiState},
+	"DISCARD":    {Clear: connectionWatchState | connectionMultiState},
+	"PSUBSCRIBE": {Set: connectionSubscribeState},
+	"SUBSCRIBE":  {Set: connectionSubscribeState},
+	"MONITOR":    {Set: connectionMonitorState},
+}
+
+func init() {
+	for n, ci := range commandInfos {
+		commandInfos[strings.ToLower(n)] = ci
+	}
+}
+
+func lookupCommandInfo(commandName string) commandInfo {
+	if ci, ok := commandInfos[commandName]; ok {
+		return ci
+	}
+	return commandInfos[strings.ToUpper(commandName)]
+}

+ 27 - 0
src/github.com/gomodule/redigo/redis/commandinfo_test.go

@@ -0,0 +1,27 @@
+package redis
+
+import "testing"
+
+func TestLookupCommandInfo(t *testing.T) {
+	for _, n := range []string{"watch", "WATCH", "wAtch"} {
+		if lookupCommandInfo(n) == (commandInfo{}) {
+			t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n)
+		}
+	}
+}
+
+func benchmarkLookupCommandInfo(b *testing.B, names ...string) {
+	for i := 0; i < b.N; i++ {
+		for _, c := range names {
+			lookupCommandInfo(c)
+		}
+	}
+}
+
+func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) {
+	benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR")
+}
+
+func BenchmarkLookupCommandInfoMixedCase(b *testing.B) {
+	benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR")
+}

+ 704 - 0
src/github.com/gomodule/redigo/redis/conn.go

@@ -0,0 +1,704 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/url"
+	"regexp"
+	"strconv"
+	"sync"
+	"time"
+)
+
+var (
+	_ ConnWithTimeout = (*conn)(nil)
+)
+
+// conn is the low-level implementation of Conn
+type conn struct {
+	// Shared
+	mu      sync.Mutex
+	pending int
+	err     error
+	conn    net.Conn
+
+	// Read
+	readTimeout time.Duration
+	br          *bufio.Reader
+
+	// Write
+	writeTimeout time.Duration
+	bw           *bufio.Writer
+
+	// Scratch space for formatting argument length.
+	// '*' or '$', length, "\r\n"
+	lenScratch [32]byte
+
+	// Scratch space for formatting integers and floats.
+	numScratch [40]byte
+}
+
+// DialTimeout acts like Dial but takes timeouts for establishing the
+// connection to the server, writing a command and reading a reply.
+//
+// Deprecated: Use Dial with options instead.
+func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
+	return Dial(network, address,
+		DialConnectTimeout(connectTimeout),
+		DialReadTimeout(readTimeout),
+		DialWriteTimeout(writeTimeout))
+}
+
+// DialOption specifies an option for dialing a Redis server.
+type DialOption struct {
+	f func(*dialOptions)
+}
+
+type dialOptions struct {
+	readTimeout  time.Duration
+	writeTimeout time.Duration
+	dialer       *net.Dialer
+	dial         func(network, addr string) (net.Conn, error)
+	db           int
+	password     string
+	clientName   string
+	useTLS       bool
+	skipVerify   bool
+	tlsConfig    *tls.Config
+}
+
+// DialReadTimeout specifies the timeout for reading a single command reply.
+func DialReadTimeout(d time.Duration) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.readTimeout = d
+	}}
+}
+
+// DialWriteTimeout specifies the timeout for writing a single command.
+func DialWriteTimeout(d time.Duration) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.writeTimeout = d
+	}}
+}
+
+// DialConnectTimeout specifies the timeout for connecting to the Redis server when
+// no DialNetDial option is specified.
+func DialConnectTimeout(d time.Duration) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.dialer.Timeout = d
+	}}
+}
+
+// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
+// when no DialNetDial option is specified.
+// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
+// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
+func DialKeepAlive(d time.Duration) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.dialer.KeepAlive = d
+	}}
+}
+
+// DialNetDial specifies a custom dial function for creating TCP
+// connections, otherwise a net.Dialer customized via the other options is used.
+// DialNetDial overrides DialConnectTimeout and DialKeepAlive.
+func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.dial = dial
+	}}
+}
+
+// DialDatabase specifies the database to select when dialing a connection.
+func DialDatabase(db int) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.db = db
+	}}
+}
+
+// DialPassword specifies the password to use when connecting to
+// the Redis server.
+func DialPassword(password string) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.password = password
+	}}
+}
+
+// DialClientName specifies a client name to be used
+// by the Redis server connection.
+func DialClientName(name string) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.clientName = name
+	}}
+}
+
+// DialTLSConfig specifies the config to use when a TLS connection is dialed.
+// Has no effect when not dialing a TLS connection.
+func DialTLSConfig(c *tls.Config) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.tlsConfig = c
+	}}
+}
+
+// DialTLSSkipVerify disables server name verification when connecting over
+// TLS. Has no effect when not dialing a TLS connection.
+func DialTLSSkipVerify(skip bool) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.skipVerify = skip
+	}}
+}
+
+// DialUseTLS specifies whether TLS should be used when connecting to the
+// server. This option is ignore by DialURL.
+func DialUseTLS(useTLS bool) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.useTLS = useTLS
+	}}
+}
+
+// Dial connects to the Redis server at the given network and
+// address using the specified options.
+func Dial(network, address string, options ...DialOption) (Conn, error) {
+	do := dialOptions{
+		dialer: &net.Dialer{
+			KeepAlive: time.Minute * 5,
+		},
+	}
+	for _, option := range options {
+		option.f(&do)
+	}
+	if do.dial == nil {
+		do.dial = do.dialer.Dial
+	}
+
+	netConn, err := do.dial(network, address)
+	if err != nil {
+		return nil, err
+	}
+
+	if do.useTLS {
+		var tlsConfig *tls.Config
+		if do.tlsConfig == nil {
+			tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify}
+		} else {
+			tlsConfig = cloneTLSConfig(do.tlsConfig)
+		}
+		if tlsConfig.ServerName == "" {
+			host, _, err := net.SplitHostPort(address)
+			if err != nil {
+				netConn.Close()
+				return nil, err
+			}
+			tlsConfig.ServerName = host
+		}
+
+		tlsConn := tls.Client(netConn, tlsConfig)
+		if err := tlsConn.Handshake(); err != nil {
+			netConn.Close()
+			return nil, err
+		}
+		netConn = tlsConn
+	}
+
+	c := &conn{
+		conn:         netConn,
+		bw:           bufio.NewWriter(netConn),
+		br:           bufio.NewReader(netConn),
+		readTimeout:  do.readTimeout,
+		writeTimeout: do.writeTimeout,
+	}
+
+	if do.password != "" {
+		if _, err := c.Do("AUTH", do.password); err != nil {
+			netConn.Close()
+			return nil, err
+		}
+	}
+
+	if do.clientName != "" {
+		if _, err := c.Do("CLIENT", "SETNAME", do.clientName); err != nil {
+			netConn.Close()
+			return nil, err
+		}
+	}
+
+	if do.db != 0 {
+		if _, err := c.Do("SELECT", do.db); err != nil {
+			netConn.Close()
+			return nil, err
+		}
+	}
+
+	return c, nil
+}
+
+var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
+
+// DialURL connects to a Redis server at the given URL using the Redis
+// URI scheme. URLs should follow the draft IANA specification for the
+// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
+func DialURL(rawurl string, options ...DialOption) (Conn, error) {
+	u, err := url.Parse(rawurl)
+	if err != nil {
+		return nil, err
+	}
+
+	if u.Scheme != "redis" && u.Scheme != "rediss" {
+		return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
+	}
+
+	if u.Opaque != "" {
+		return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl)
+	}
+
+	// As per the IANA draft spec, the host defaults to localhost and
+	// the port defaults to 6379.
+	host, port, err := net.SplitHostPort(u.Host)
+	if err != nil {
+		// assume port is missing
+		host = u.Host
+		port = "6379"
+	}
+	if host == "" {
+		host = "localhost"
+	}
+	address := net.JoinHostPort(host, port)
+
+	if u.User != nil {
+		password, isSet := u.User.Password()
+		if isSet {
+			options = append(options, DialPassword(password))
+		}
+	}
+
+	match := pathDBRegexp.FindStringSubmatch(u.Path)
+	if len(match) == 2 {
+		db := 0
+		if len(match[1]) > 0 {
+			db, err = strconv.Atoi(match[1])
+			if err != nil {
+				return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
+			}
+		}
+		if db != 0 {
+			options = append(options, DialDatabase(db))
+		}
+	} else if u.Path != "" {
+		return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
+	}
+
+	options = append(options, DialUseTLS(u.Scheme == "rediss"))
+
+	return Dial("tcp", address, options...)
+}
+
+// NewConn returns a new Redigo connection for the given net connection.
+func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
+	return &conn{
+		conn:         netConn,
+		bw:           bufio.NewWriter(netConn),
+		br:           bufio.NewReader(netConn),
+		readTimeout:  readTimeout,
+		writeTimeout: writeTimeout,
+	}
+}
+
+func (c *conn) Close() error {
+	c.mu.Lock()
+	err := c.err
+	if c.err == nil {
+		c.err = errors.New("redigo: closed")
+		err = c.conn.Close()
+	}
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) fatal(err error) error {
+	c.mu.Lock()
+	if c.err == nil {
+		c.err = err
+		// Close connection to force errors on subsequent calls and to unblock
+		// other reader or writer.
+		c.conn.Close()
+	}
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) Err() error {
+	c.mu.Lock()
+	err := c.err
+	c.mu.Unlock()
+	return err
+}
+
+func (c *conn) writeLen(prefix byte, n int) error {
+	c.lenScratch[len(c.lenScratch)-1] = '\n'
+	c.lenScratch[len(c.lenScratch)-2] = '\r'
+	i := len(c.lenScratch) - 3
+	for {
+		c.lenScratch[i] = byte('0' + n%10)
+		i -= 1
+		n = n / 10
+		if n == 0 {
+			break
+		}
+	}
+	c.lenScratch[i] = prefix
+	_, err := c.bw.Write(c.lenScratch[i:])
+	return err
+}
+
+func (c *conn) writeString(s string) error {
+	c.writeLen('$', len(s))
+	c.bw.WriteString(s)
+	_, err := c.bw.WriteString("\r\n")
+	return err
+}
+
+func (c *conn) writeBytes(p []byte) error {
+	c.writeLen('$', len(p))
+	c.bw.Write(p)
+	_, err := c.bw.WriteString("\r\n")
+	return err
+}
+
+func (c *conn) writeInt64(n int64) error {
+	return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
+}
+
+func (c *conn) writeFloat64(n float64) error {
+	return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
+}
+
+func (c *conn) writeCommand(cmd string, args []interface{}) error {
+	c.writeLen('*', 1+len(args))
+	if err := c.writeString(cmd); err != nil {
+		return err
+	}
+	for _, arg := range args {
+		if err := c.writeArg(arg, true); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
+	switch arg := arg.(type) {
+	case string:
+		return c.writeString(arg)
+	case []byte:
+		return c.writeBytes(arg)
+	case int:
+		return c.writeInt64(int64(arg))
+	case int64:
+		return c.writeInt64(arg)
+	case float64:
+		return c.writeFloat64(arg)
+	case bool:
+		if arg {
+			return c.writeString("1")
+		} else {
+			return c.writeString("0")
+		}
+	case nil:
+		return c.writeString("")
+	case Argument:
+		if argumentTypeOK {
+			return c.writeArg(arg.RedisArg(), false)
+		}
+		// See comment in default clause below.
+		var buf bytes.Buffer
+		fmt.Fprint(&buf, arg)
+		return c.writeBytes(buf.Bytes())
+	default:
+		// This default clause is intended to handle builtin numeric types.
+		// The function should return an error for other types, but this is not
+		// done for compatibility with previous versions of the package.
+		var buf bytes.Buffer
+		fmt.Fprint(&buf, arg)
+		return c.writeBytes(buf.Bytes())
+	}
+}
+
+type protocolError string
+
+func (pe protocolError) Error() string {
+	return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
+}
+
+// readLine reads a line of input from the RESP stream.
+func (c *conn) readLine() ([]byte, error) {
+	// To avoid allocations, attempt to read the line using ReadSlice. This
+	// call typically succeeds. The known case where the call fails is when
+	// reading the output from the MONITOR command.
+	p, err := c.br.ReadSlice('\n')
+	if err == bufio.ErrBufferFull {
+		// The line does not fit in the bufio.Reader's buffer. Fall back to
+		// allocating a buffer for the line.
+		buf := append([]byte{}, p...)
+		for err == bufio.ErrBufferFull {
+			p, err = c.br.ReadSlice('\n')
+			buf = append(buf, p...)
+		}
+		p = buf
+	}
+	if err != nil {
+		return nil, err
+	}
+	i := len(p) - 2
+	if i < 0 || p[i] != '\r' {
+		return nil, protocolError("bad response line terminator")
+	}
+	return p[:i], nil
+}
+
+// parseLen parses bulk string and array lengths.
+func parseLen(p []byte) (int, error) {
+	if len(p) == 0 {
+		return -1, protocolError("malformed length")
+	}
+
+	if p[0] == '-' && len(p) == 2 && p[1] == '1' {
+		// handle $-1 and $-1 null replies.
+		return -1, nil
+	}
+
+	var n int
+	for _, b := range p {
+		n *= 10
+		if b < '0' || b > '9' {
+			return -1, protocolError("illegal bytes in length")
+		}
+		n += int(b - '0')
+	}
+
+	return n, nil
+}
+
+// parseInt parses an integer reply.
+func parseInt(p []byte) (interface{}, error) {
+	if len(p) == 0 {
+		return 0, protocolError("malformed integer")
+	}
+
+	var negate bool
+	if p[0] == '-' {
+		negate = true
+		p = p[1:]
+		if len(p) == 0 {
+			return 0, protocolError("malformed integer")
+		}
+	}
+
+	var n int64
+	for _, b := range p {
+		n *= 10
+		if b < '0' || b > '9' {
+			return 0, protocolError("illegal bytes in length")
+		}
+		n += int64(b - '0')
+	}
+
+	if negate {
+		n = -n
+	}
+	return n, nil
+}
+
+var (
+	okReply   interface{} = "OK"
+	pongReply interface{} = "PONG"
+)
+
+func (c *conn) readReply() (interface{}, error) {
+	line, err := c.readLine()
+	if err != nil {
+		return nil, err
+	}
+	if len(line) == 0 {
+		return nil, protocolError("short response line")
+	}
+	switch line[0] {
+	case '+':
+		switch {
+		case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
+			// Avoid allocation for frequent "+OK" response.
+			return okReply, nil
+		case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
+			// Avoid allocation in PING command benchmarks :)
+			return pongReply, nil
+		default:
+			return string(line[1:]), nil
+		}
+	case '-':
+		return Error(string(line[1:])), nil
+	case ':':
+		return parseInt(line[1:])
+	case '$':
+		n, err := parseLen(line[1:])
+		if n < 0 || err != nil {
+			return nil, err
+		}
+		p := make([]byte, n)
+		_, err = io.ReadFull(c.br, p)
+		if err != nil {
+			return nil, err
+		}
+		if line, err := c.readLine(); err != nil {
+			return nil, err
+		} else if len(line) != 0 {
+			return nil, protocolError("bad bulk string format")
+		}
+		return p, nil
+	case '*':
+		n, err := parseLen(line[1:])
+		if n < 0 || err != nil {
+			return nil, err
+		}
+		r := make([]interface{}, n)
+		for i := range r {
+			r[i], err = c.readReply()
+			if err != nil {
+				return nil, err
+			}
+		}
+		return r, nil
+	}
+	return nil, protocolError("unexpected response line")
+}
+
+func (c *conn) Send(cmd string, args ...interface{}) error {
+	c.mu.Lock()
+	c.pending += 1
+	c.mu.Unlock()
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+	if err := c.writeCommand(cmd, args); err != nil {
+		return c.fatal(err)
+	}
+	return nil
+}
+
+func (c *conn) Flush() error {
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+	if err := c.bw.Flush(); err != nil {
+		return c.fatal(err)
+	}
+	return nil
+}
+
+func (c *conn) Receive() (interface{}, error) {
+	return c.ReceiveWithTimeout(c.readTimeout)
+}
+
+func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
+	var deadline time.Time
+	if timeout != 0 {
+		deadline = time.Now().Add(timeout)
+	}
+	c.conn.SetReadDeadline(deadline)
+
+	if reply, err = c.readReply(); err != nil {
+		return nil, c.fatal(err)
+	}
+	// When using pub/sub, the number of receives can be greater than the
+	// number of sends. To enable normal use of the connection after
+	// unsubscribing from all channels, we do not decrement pending to a
+	// negative value.
+	//
+	// The pending field is decremented after the reply is read to handle the
+	// case where Receive is called before Send.
+	c.mu.Lock()
+	if c.pending > 0 {
+		c.pending -= 1
+	}
+	c.mu.Unlock()
+	if err, ok := reply.(Error); ok {
+		return nil, err
+	}
+	return
+}
+
+func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
+	return c.DoWithTimeout(c.readTimeout, cmd, args...)
+}
+
+func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
+	c.mu.Lock()
+	pending := c.pending
+	c.pending = 0
+	c.mu.Unlock()
+
+	if cmd == "" && pending == 0 {
+		return nil, nil
+	}
+
+	if c.writeTimeout != 0 {
+		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
+	}
+
+	if cmd != "" {
+		if err := c.writeCommand(cmd, args); err != nil {
+			return nil, c.fatal(err)
+		}
+	}
+
+	if err := c.bw.Flush(); err != nil {
+		return nil, c.fatal(err)
+	}
+
+	var deadline time.Time
+	if readTimeout != 0 {
+		deadline = time.Now().Add(readTimeout)
+	}
+	c.conn.SetReadDeadline(deadline)
+
+	if cmd == "" {
+		reply := make([]interface{}, pending)
+		for i := range reply {
+			r, e := c.readReply()
+			if e != nil {
+				return nil, c.fatal(e)
+			}
+			reply[i] = r
+		}
+		return reply, nil
+	}
+
+	var err error
+	var reply interface{}
+	for i := 0; i <= pending; i++ {
+		var e error
+		if reply, e = c.readReply(); e != nil {
+			return nil, c.fatal(e)
+		}
+		if e, ok := reply.(Error); ok && err == nil {
+			err = e
+		}
+	}
+	return reply, err
+}

+ 946 - 0
src/github.com/gomodule/redigo/redis/conn_test.go

@@ -0,0 +1,946 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"math"
+	"net"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type testConn struct {
+	io.Reader
+	io.Writer
+	readDeadline  time.Time
+	writeDeadline time.Time
+}
+
+func (*testConn) Close() error                         { return nil }
+func (*testConn) LocalAddr() net.Addr                  { return nil }
+func (*testConn) RemoteAddr() net.Addr                 { return nil }
+func (c *testConn) SetDeadline(t time.Time) error      { c.readDeadline = t; c.writeDeadline = t; return nil }
+func (c *testConn) SetReadDeadline(t time.Time) error  { c.readDeadline = t; return nil }
+func (c *testConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t; return nil }
+
+func dialTestConn(r string, w io.Writer) redis.DialOption {
+	return redis.DialNetDial(func(network, addr string) (net.Conn, error) {
+		return &testConn{Reader: strings.NewReader(r), Writer: w}, nil
+	})
+}
+
+type tlsTestConn struct {
+	net.Conn
+	done chan struct{}
+}
+
+func (c *tlsTestConn) Close() error {
+	c.Conn.Close()
+	<-c.done
+	return nil
+}
+
+func dialTestConnTLS(r string, w io.Writer) redis.DialOption {
+	return redis.DialNetDial(func(network, addr string) (net.Conn, error) {
+		client, server := net.Pipe()
+		tlsServer := tls.Server(server, &serverTLSConfig)
+		go io.Copy(tlsServer, strings.NewReader(r))
+		done := make(chan struct{})
+		go func() {
+			io.Copy(w, tlsServer)
+			close(done)
+		}()
+		return &tlsTestConn{Conn: client, done: done}, nil
+	})
+}
+
+type durationArg struct {
+	time.Duration
+}
+
+func (t durationArg) RedisArg() interface{} {
+	return t.Seconds()
+}
+
+type recursiveArg int
+
+func (v recursiveArg) RedisArg() interface{} { return v }
+
+var writeTests = []struct {
+	args     []interface{}
+	expected string
+}{
+	{
+		[]interface{}{"SET", "key", "value"},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", "value"},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", byte(100)},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", 100},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", int64(math.MinInt64)},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", float64(1349673917.939762)},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", ""},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", nil},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", durationArg{time.Minute}},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$2\r\n60\r\n",
+	},
+	{
+		[]interface{}{"SET", "key", recursiveArg(123)},
+		"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n123\r\n",
+	},
+	{
+		[]interface{}{"ECHO", true, false},
+		"*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n",
+	},
+}
+
+func TestWrite(t *testing.T) {
+	for _, tt := range writeTests {
+		var buf bytes.Buffer
+		c, _ := redis.Dial("", "", dialTestConn("", &buf))
+		err := c.Send(tt.args[0].(string), tt.args[1:]...)
+		if err != nil {
+			t.Errorf("Send(%v) returned error %v", tt.args, err)
+			continue
+		}
+		c.Flush()
+		actual := buf.String()
+		if actual != tt.expected {
+			t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected)
+		}
+	}
+}
+
+var errorSentinel = &struct{}{}
+
+var readTests = []struct {
+	reply    string
+	expected interface{}
+}{
+	{
+		"+OK\r\n",
+		"OK",
+	},
+	{
+		"+PONG\r\n",
+		"PONG",
+	},
+	{
+		"+OK\n\n", // no \r
+		errorSentinel,
+	},
+	{
+		"@OK\r\n",
+		errorSentinel,
+	},
+	{
+		"$6\r\nfoobar\r\n",
+		[]byte("foobar"),
+	},
+	{
+		"$-1\r\n",
+		nil,
+	},
+	{
+		":1\r\n",
+		int64(1),
+	},
+	{
+		":-2\r\n",
+		int64(-2),
+	},
+	{
+		"*0\r\n",
+		[]interface{}{},
+	},
+	{
+		"*-1\r\n",
+		nil,
+	},
+	{
+		"*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n",
+		[]interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")},
+	},
+	{
+		"*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n",
+		[]interface{}{[]byte("foo"), nil, []byte("bar")},
+	},
+
+	{
+		// "" is not a valid length
+		"$\r\nfoobar\r\n",
+		errorSentinel,
+	},
+	{
+		// "x" is not a valid length
+		"$x\r\nfoobar\r\n",
+		errorSentinel,
+	},
+	{
+		// -2 is not a valid length
+		"$-2\r\n",
+		errorSentinel,
+	},
+	{
+		// ""  is not a valid integer
+		":\r\n",
+		errorSentinel,
+	},
+	{
+		// "x"  is not a valid integer
+		":x\r\n",
+		errorSentinel,
+	},
+	{
+		// missing \r\n following value
+		"$6\r\nfoobar",
+		errorSentinel,
+	},
+	{
+		// short value
+		"$6\r\nxx",
+		errorSentinel,
+	},
+	{
+		// long value
+		"$6\r\nfoobarx\r\n",
+		errorSentinel,
+	},
+}
+
+func TestRead(t *testing.T) {
+	for _, tt := range readTests {
+		c, _ := redis.Dial("", "", dialTestConn(tt.reply, nil))
+		actual, err := c.Receive()
+		if tt.expected == errorSentinel {
+			if err == nil {
+				t.Errorf("Receive(%q) did not return expected error", tt.reply)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Receive(%q) returned error %v", tt.reply, err)
+				continue
+			}
+			if !reflect.DeepEqual(actual, tt.expected) {
+				t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected)
+			}
+		}
+	}
+}
+
+func TestReadString(t *testing.T) {
+	// n is value of bufio.defaultBufSize
+	const n = 4096
+
+	// Test read string lengths near bufio.Reader buffer boundaries.
+	testRanges := [][2]int{{0, 64}, {n - 64, n + 64}, {2*n - 64, 2*n + 64}}
+
+	p := make([]byte, 2*n+64)
+	for i := range p {
+		p[i] = byte('a' + i%26)
+	}
+	s := string(p)
+
+	for _, r := range testRanges {
+		for i := r[0]; i < r[1]; i++ {
+			c, _ := redis.Dial("", "", dialTestConn("+"+s[:i]+"\r\n", nil))
+			actual, err := c.Receive()
+			if err != nil || actual != s[:i] {
+				t.Fatalf("Receive(string len %d) -> err=%v, equal=%v", i, err, actual != s[:i])
+			}
+		}
+	}
+}
+
+var testCommands = []struct {
+	args     []interface{}
+	expected interface{}
+}{
+	{
+		[]interface{}{"PING"},
+		"PONG",
+	},
+	{
+		[]interface{}{"SET", "foo", "bar"},
+		"OK",
+	},
+	{
+		[]interface{}{"GET", "foo"},
+		[]byte("bar"),
+	},
+	{
+		[]interface{}{"GET", "nokey"},
+		nil,
+	},
+	{
+		[]interface{}{"MGET", "nokey", "foo"},
+		[]interface{}{nil, []byte("bar")},
+	},
+	{
+		[]interface{}{"INCR", "mycounter"},
+		int64(1),
+	},
+	{
+		[]interface{}{"LPUSH", "mylist", "foo"},
+		int64(1),
+	},
+	{
+		[]interface{}{"LPUSH", "mylist", "bar"},
+		int64(2),
+	},
+	{
+		[]interface{}{"LRANGE", "mylist", 0, -1},
+		[]interface{}{[]byte("bar"), []byte("foo")},
+	},
+	{
+		[]interface{}{"MULTI"},
+		"OK",
+	},
+	{
+		[]interface{}{"LRANGE", "mylist", 0, -1},
+		"QUEUED",
+	},
+	{
+		[]interface{}{"PING"},
+		"QUEUED",
+	},
+	{
+		[]interface{}{"EXEC"},
+		[]interface{}{
+			[]interface{}{[]byte("bar"), []byte("foo")},
+			"PONG",
+		},
+	},
+}
+
+func TestDoCommands(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	for _, cmd := range testCommands {
+		actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...)
+		if err != nil {
+			t.Errorf("Do(%v) returned error %v", cmd.args, err)
+			continue
+		}
+		if !reflect.DeepEqual(actual, cmd.expected) {
+			t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected)
+		}
+	}
+}
+
+func TestPipelineCommands(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	for _, cmd := range testCommands {
+		if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil {
+			t.Fatalf("Send(%v) returned error %v", cmd.args, err)
+		}
+	}
+	if err := c.Flush(); err != nil {
+		t.Errorf("Flush() returned error %v", err)
+	}
+	for _, cmd := range testCommands {
+		actual, err := c.Receive()
+		if err != nil {
+			t.Fatalf("Receive(%v) returned error %v", cmd.args, err)
+		}
+		if !reflect.DeepEqual(actual, cmd.expected) {
+			t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected)
+		}
+	}
+}
+
+func TestBlankCommand(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	for _, cmd := range testCommands {
+		if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil {
+			t.Fatalf("Send(%v) returned error %v", cmd.args, err)
+		}
+	}
+	reply, err := redis.Values(c.Do(""))
+	if err != nil {
+		t.Fatalf("Do() returned error %v", err)
+	}
+	if len(reply) != len(testCommands) {
+		t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands))
+	}
+	for i, cmd := range testCommands {
+		actual := reply[i]
+		if !reflect.DeepEqual(actual, cmd.expected) {
+			t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected)
+		}
+	}
+}
+
+func TestRecvBeforeSend(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+	done := make(chan struct{})
+	go func() {
+		c.Receive()
+		close(done)
+	}()
+	time.Sleep(time.Millisecond)
+	c.Send("PING")
+	c.Flush()
+	<-done
+	_, err = c.Do("")
+	if err != nil {
+		t.Fatalf("error=%v", err)
+	}
+}
+
+func TestError(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	c.Do("SET", "key", "val")
+	_, err = c.Do("HSET", "key", "fld", "val")
+	if err == nil {
+		t.Errorf("Expected err for HSET on string key.")
+	}
+	if c.Err() != nil {
+		t.Errorf("Conn has Err()=%v, expect nil", c.Err())
+	}
+	_, err = c.Do("SET", "key", "val")
+	if err != nil {
+		t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err)
+	}
+}
+
+func TestReadTimeout(t *testing.T) {
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("net.Listen returned %v", err)
+	}
+	defer l.Close()
+
+	go func() {
+		for {
+			c, err := l.Accept()
+			if err != nil {
+				return
+			}
+			go func() {
+				time.Sleep(time.Second)
+				c.Write([]byte("+OK\r\n"))
+				c.Close()
+			}()
+		}
+	}()
+
+	// Do
+
+	c1, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond))
+	if err != nil {
+		t.Fatalf("redis.Dial returned %v", err)
+	}
+	defer c1.Close()
+
+	_, err = c1.Do("PING")
+	if err == nil {
+		t.Fatalf("c1.Do() returned nil, expect error")
+	}
+	if c1.Err() == nil {
+		t.Fatalf("c1.Err() = nil, expect error")
+	}
+
+	// Send/Flush/Receive
+
+	c2, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond))
+	if err != nil {
+		t.Fatalf("redis.Dial returned %v", err)
+	}
+	defer c2.Close()
+
+	c2.Send("PING")
+	c2.Flush()
+	_, err = c2.Receive()
+	if err == nil {
+		t.Fatalf("c2.Receive() returned nil, expect error")
+	}
+	if c2.Err() == nil {
+		t.Fatalf("c2.Err() = nil, expect error")
+	}
+}
+
+var dialErrors = []struct {
+	rawurl        string
+	expectedError string
+}{
+	{
+		"localhost",
+		"invalid redis URL scheme",
+	},
+	// The error message for invalid hosts is different in different
+	// versions of Go, so just check that there is an error message.
+	{
+		"redis://weird url",
+		"",
+	},
+	{
+		"redis://foo:bar:baz",
+		"",
+	},
+	{
+		"http://www.google.com",
+		"invalid redis URL scheme: http",
+	},
+	{
+		"redis://localhost:6379/abc123",
+		"invalid database: abc123",
+	},
+	{
+		"redis:foo//localhost:6379",
+		"invalid redis URL, url is opaque: redis:foo//localhost:6379",
+	},
+}
+
+func TestDialURLErrors(t *testing.T) {
+	for _, d := range dialErrors {
+		_, err := redis.DialURL(d.rawurl)
+		if err == nil || !strings.Contains(err.Error(), d.expectedError) {
+			t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError)
+		}
+	}
+}
+
+func TestDialURLPort(t *testing.T) {
+	checkPort := func(network, address string) (net.Conn, error) {
+		if address != "localhost:6379" {
+			t.Errorf("DialURL did not set port to 6379 by default (got %v)", address)
+		}
+		return nil, nil
+	}
+	_, err := redis.DialURL("redis://localhost", redis.DialNetDial(checkPort))
+	if err != nil {
+		t.Error("dial error:", err)
+	}
+}
+
+func TestDialURLHost(t *testing.T) {
+	checkHost := func(network, address string) (net.Conn, error) {
+		if address != "localhost:6379" {
+			t.Errorf("DialURL did not set host to localhost by default (got %v)", address)
+		}
+		return nil, nil
+	}
+	_, err := redis.DialURL("redis://:6379", redis.DialNetDial(checkHost))
+	if err != nil {
+		t.Error("dial error:", err)
+	}
+}
+
+var dialURLTests = []struct {
+	description string
+	url         string
+	r           string
+	w           string
+}{
+	{"password", "redis://x:abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
+	{"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"},
+	{"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"},
+	{"no database", "redis://localhost/", "+OK\r\n", ""},
+}
+
+func TestDialURL(t *testing.T) {
+	for _, tt := range dialURLTests {
+		var buf bytes.Buffer
+		// UseTLS should be ignored in all of these tests.
+		_, err := redis.DialURL(tt.url, dialTestConn(tt.r, &buf), redis.DialUseTLS(true))
+		if err != nil {
+			t.Errorf("%s dial error: %v", tt.description, err)
+			continue
+		}
+		if w := buf.String(); w != tt.w {
+			t.Errorf("%s commands = %q, want %q", tt.description, w, tt.w)
+		}
+	}
+}
+
+func checkPingPong(t *testing.T, buf *bytes.Buffer, c redis.Conn) {
+	resp, err := c.Do("PING")
+	if err != nil {
+		t.Fatal("ping error:", err)
+	}
+	// Close connection to ensure that writes to buf are complete.
+	c.Close()
+	expected := "*1\r\n$4\r\nPING\r\n"
+	actual := buf.String()
+	if actual != expected {
+		t.Errorf("commands = %q, want %q", actual, expected)
+	}
+	if resp != "PONG" {
+		t.Errorf("resp = %v, want %v", resp, "PONG")
+	}
+}
+
+const pingResponse = "+PONG\r\n"
+
+func TestDialURLTLS(t *testing.T) {
+	var buf bytes.Buffer
+	c, err := redis.DialURL("rediss://example.com/",
+		redis.DialTLSConfig(&clientTLSConfig),
+		dialTestConnTLS(pingResponse, &buf))
+	if err != nil {
+		t.Fatal("dial error:", err)
+	}
+	checkPingPong(t, &buf, c)
+}
+
+func TestDialUseTLS(t *testing.T) {
+	var buf bytes.Buffer
+	c, err := redis.Dial("tcp", "example.com:6379",
+		redis.DialTLSConfig(&clientTLSConfig),
+		dialTestConnTLS(pingResponse, &buf),
+		redis.DialUseTLS(true))
+	if err != nil {
+		t.Fatal("dial error:", err)
+	}
+	checkPingPong(t, &buf, c)
+}
+
+func TestDialTLSSKipVerify(t *testing.T) {
+	var buf bytes.Buffer
+	c, err := redis.Dial("tcp", "example.com:6379",
+		dialTestConnTLS(pingResponse, &buf),
+		redis.DialTLSSkipVerify(true),
+		redis.DialUseTLS(true))
+	if err != nil {
+		t.Fatal("dial error:", err)
+	}
+	checkPingPong(t, &buf, c)
+}
+
+func TestDialClientName(t *testing.T) {
+	var buf bytes.Buffer
+	_, err := redis.Dial("tcp", ":6379",
+		dialTestConn(pingResponse, &buf),
+		redis.DialClientName("redis-connection"),
+	)
+	if err != nil {
+		t.Fatal("dial error:", err)
+	}
+	expected := "*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$16\r\nredis-connection\r\n"
+	if w := buf.String(); w != expected {
+		t.Errorf("got %q, want %q", w, expected)
+	}
+
+	// testing against a real server
+	connectionName := "test-connection"
+	c, err := redis.DialDefaultServer(redis.DialClientName(connectionName))
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	v, err := c.Do("CLIENT", "GETNAME")
+	if err != nil {
+		t.Fatalf("CLIENT GETNAME returned error %v", err)
+	}
+
+	vs, err := redis.String(v, nil)
+	if err != nil {
+		t.Fatalf("String(v) returned error %v", err)
+	}
+
+	if vs != connectionName {
+		t.Fatalf("wrong connection name. Got '%s', expected '%s'", vs, connectionName)
+	}
+}
+
+// Connect to local instance of Redis running on the default port.
+func ExampleDial() {
+	c, err := redis.Dial("tcp", ":6379")
+	if err != nil {
+		// handle error
+	}
+	defer c.Close()
+}
+
+// Connect to remote instance of Redis using a URL.
+func ExampleDialURL() {
+	c, err := redis.DialURL(os.Getenv("REDIS_URL"))
+	if err != nil {
+		// handle connection error
+	}
+	defer c.Close()
+}
+
+// TextExecError tests handling of errors in a transaction. See
+// http://redis.io/topics/transactions for information on how Redis handles
+// errors in a transaction.
+func TestExecError(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	// Execute commands that fail before EXEC is called.
+
+	c.Do("DEL", "k0")
+	c.Do("ZADD", "k0", 0, 0)
+	c.Send("MULTI")
+	c.Send("NOTACOMMAND", "k0", 0, 0)
+	c.Send("ZINCRBY", "k0", 0, 0)
+	v, err := c.Do("EXEC")
+	if err == nil {
+		t.Fatalf("EXEC returned values %v, expected error", v)
+	}
+
+	// Execute commands that fail after EXEC is called. The first command
+	// returns an error.
+
+	c.Do("DEL", "k1")
+	c.Do("ZADD", "k1", 0, 0)
+	c.Send("MULTI")
+	c.Send("HSET", "k1", 0, 0)
+	c.Send("ZINCRBY", "k1", 0, 0)
+	v, err = c.Do("EXEC")
+	if err != nil {
+		t.Fatalf("EXEC returned error %v", err)
+	}
+
+	vs, err := redis.Values(v, nil)
+	if err != nil {
+		t.Fatalf("Values(v) returned error %v", err)
+	}
+
+	if len(vs) != 2 {
+		t.Fatalf("len(vs) == %d, want 2", len(vs))
+	}
+
+	if _, ok := vs[0].(error); !ok {
+		t.Fatalf("first result is type %T, expected error", vs[0])
+	}
+
+	if _, ok := vs[1].([]byte); !ok {
+		t.Fatalf("second result is type %T, expected []byte", vs[1])
+	}
+
+	// Execute commands that fail after EXEC is called. The second command
+	// returns an error.
+
+	c.Do("ZADD", "k2", 0, 0)
+	c.Send("MULTI")
+	c.Send("ZINCRBY", "k2", 0, 0)
+	c.Send("HSET", "k2", 0, 0)
+	v, err = c.Do("EXEC")
+	if err != nil {
+		t.Fatalf("EXEC returned error %v", err)
+	}
+
+	vs, err = redis.Values(v, nil)
+	if err != nil {
+		t.Fatalf("Values(v) returned error %v", err)
+	}
+
+	if len(vs) != 2 {
+		t.Fatalf("len(vs) == %d, want 2", len(vs))
+	}
+
+	if _, ok := vs[0].([]byte); !ok {
+		t.Fatalf("first result is type %T, expected []byte", vs[0])
+	}
+
+	if _, ok := vs[1].(error); !ok {
+		t.Fatalf("second result is type %T, expected error", vs[2])
+	}
+}
+
+func BenchmarkDoEmpty(b *testing.B) {
+	b.StopTimer()
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		b.Fatal(err)
+	}
+	defer c.Close()
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		if _, err := c.Do(""); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkDoPing(b *testing.B) {
+	b.StopTimer()
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		b.Fatal(err)
+	}
+	defer c.Close()
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		if _, err := c.Do("PING"); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+var clientTLSConfig, serverTLSConfig tls.Config
+
+func init() {
+	// The certificate and key for testing TLS dial options was created
+	// using the command
+	//
+	//   go run GOROOT/src/crypto/tls/generate_cert.go  \
+	//      --rsa-bits 1024 \
+	//      --host 127.0.0.1,::1,example.com --ca \
+	//      --start-date "Jan 1 00:00:00 1970" \
+	//      --duration=1000000h
+	//
+	// where GOROOT is the value of GOROOT reported by go env.
+	localhostCert := []byte(`
+-----BEGIN CERTIFICATE-----
+MIICFDCCAX2gAwIBAgIRAJfBL4CUxkXcdlFurb3K+iowDQYJKoZIhvcNAQELBQAw
+EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
+MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEArizw8WxMUQ3bGHLeuJ4fDrEpy+L2pqrbYRlKk1DasJ/VkB8bImzIpe6+
+LGjiYIxvnDCOJ3f3QplcQuiuMyl6f2irJlJsbFT8Lo/3obnuTKAIaqUdJUqBg6y+
+JaL8Auk97FvunfKFv8U1AIhgiLzAfQ/3Eaq1yi87Ra6pMjGbTtcCAwEAAaNoMGYw
+DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
+MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
+AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAdZ8daIVkyhVwflt5I19m0oq1TycbGO1+
+ach7T6cZiBQeNR/SJtxr/wKPEpmvUgbv2BfFrKJ8QoIHYsbNSURTWSEa02pfw4k9
+6RQhij3ZkG79Ituj5OYRORV6Z0HUW32r670BtcuHuAhq7YA6Nxy4FtSt7bAlVdRt
+rrKgNsltzMk=
+-----END CERTIFICATE-----`)
+
+	localhostKey := []byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCuLPDxbExRDdsYct64nh8OsSnL4vamqtthGUqTUNqwn9WQHxsi
+bMil7r4saOJgjG+cMI4nd/dCmVxC6K4zKXp/aKsmUmxsVPwuj/ehue5MoAhqpR0l
+SoGDrL4lovwC6T3sW+6d8oW/xTUAiGCIvMB9D/cRqrXKLztFrqkyMZtO1wIDAQAB
+AoGACrc5G6FOEK6JjDeE/Fa+EmlT6PdNtXNNi+vCas3Opo8u1G8VfEi1D4BgstrB
+Eq+RLkrOdB8tVyuYQYWPMhabMqF+hhKJN72j0OwfuPlVvTInwb/cKjo/zbH1IA+Y
+HenHNK4ywv7/p/9/MvQPJ3I32cQBCgGUW5chVSH5M1sj5gECQQDabQAI1X0uDqCm
+KbX9gXVkAgxkFddrt6LBHt57xujFcqEKFE7nwKhDh7DweVs/VEJ+kpid4z+UnLOw
+KjtP9JolAkEAzCNBphQ//IsbH5rNs10wIUw3Ks/Oepicvr6kUFbIv+neRzi1iJHa
+m6H7EayK3PWgax6BAsR/t0Jc9XV7r2muSwJAVzN09BHnK+ADGtNEKLTqXMbEk6B0
+pDhn7ZmZUOkUPN+Kky+QYM11X6Bob1jDqQDGmymDbGUxGO+GfSofC8inUQJAGfci
+Eo3g1a6b9JksMPRZeuLG4ZstGErxJRH6tH1Va5PDwitka8qhk8o2tTjNMO3NSdLH
+diKoXBcE2/Pll5pJoQJBAIMiiMIzXJhnN4mX8may44J/HvMlMf2xuVH2gNMwmZuc
+Bjqn3yoLHaoZVvbWOi0C2TCN4FjXjaLNZGifQPbIcaA=
+-----END RSA PRIVATE KEY-----`)
+
+	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
+	if err != nil {
+		panic(fmt.Sprintf("error creating key pair: %v", err))
+	}
+	serverTLSConfig.Certificates = []tls.Certificate{cert}
+
+	certificate, err := x509.ParseCertificate(serverTLSConfig.Certificates[0].Certificate[0])
+	if err != nil {
+		panic(fmt.Sprintf("error parsing x509 certificate: %v", err))
+	}
+
+	clientTLSConfig.RootCAs = x509.NewCertPool()
+	clientTLSConfig.RootCAs.AddCert(certificate)
+}
+
+func TestWithTimeout(t *testing.T) {
+	for _, recv := range []bool{true, false} {
+		for _, defaultTimout := range []time.Duration{0, time.Minute} {
+			var buf bytes.Buffer
+			nc := &testConn{Reader: strings.NewReader("+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n"), Writer: &buf}
+			c, _ := redis.Dial("", "", redis.DialReadTimeout(defaultTimout), redis.DialNetDial(func(network, addr string) (net.Conn, error) { return nc, nil }))
+			for i := 0; i < 4; i++ {
+				var minDeadline, maxDeadline time.Time
+
+				// Alternate between default and specified timeout.
+				if i%2 == 0 {
+					if defaultTimout != 0 {
+						minDeadline = time.Now().Add(defaultTimout)
+					}
+					if recv {
+						c.Receive()
+					} else {
+						c.Do("PING")
+					}
+					if defaultTimout != 0 {
+						maxDeadline = time.Now().Add(defaultTimout)
+					}
+				} else {
+					timeout := 10 * time.Minute
+					minDeadline = time.Now().Add(timeout)
+					if recv {
+						redis.ReceiveWithTimeout(c, timeout)
+					} else {
+						redis.DoWithTimeout(c, timeout, "PING")
+					}
+					maxDeadline = time.Now().Add(timeout)
+				}
+
+				// Expect set deadline in expected range.
+				if nc.readDeadline.Before(minDeadline) || nc.readDeadline.After(maxDeadline) {
+					t.Errorf("recv %v, %d: do deadline error: %v, %v, %v", recv, i, minDeadline, nc.readDeadline, maxDeadline)
+				}
+			}
+		}
+	}
+}

+ 177 - 0
src/github.com/gomodule/redigo/redis/doc.go

@@ -0,0 +1,177 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redis is a client for the Redis database.
+//
+// The Redigo FAQ (https://github.com/gomodule/redigo/wiki/FAQ) contains more
+// documentation about this package.
+//
+// Connections
+//
+// The Conn interface is the primary interface for working with Redis.
+// Applications create connections by calling the Dial, DialWithTimeout or
+// NewConn functions. In the future, functions will be added for creating
+// sharded and other types of connections.
+//
+// The application must call the connection Close method when the application
+// is done with the connection.
+//
+// Executing Commands
+//
+// The Conn interface has a generic method for executing Redis commands:
+//
+//  Do(commandName string, args ...interface{}) (reply interface{}, err error)
+//
+// The Redis command reference (http://redis.io/commands) lists the available
+// commands. An example of using the Redis APPEND command is:
+//
+//  n, err := conn.Do("APPEND", "key", "value")
+//
+// The Do method converts command arguments to bulk strings for transmission
+// to the server as follows:
+//
+//  Go Type                 Conversion
+//  []byte                  Sent as is
+//  string                  Sent as is
+//  int, int64              strconv.FormatInt(v)
+//  float64                 strconv.FormatFloat(v, 'g', -1, 64)
+//  bool                    true -> "1", false -> "0"
+//  nil                     ""
+//  all other types         fmt.Fprint(w, v)
+//
+// Redis command reply types are represented using the following Go types:
+//
+//  Redis type              Go type
+//  error                   redis.Error
+//  integer                 int64
+//  simple string           string
+//  bulk string             []byte or nil if value not present.
+//  array                   []interface{} or nil if value not present.
+//
+// Use type assertions or the reply helper functions to convert from
+// interface{} to the specific Go type for the command result.
+//
+// Pipelining
+//
+// Connections support pipelining using the Send, Flush and Receive methods.
+//
+//  Send(commandName string, args ...interface{}) error
+//  Flush() error
+//  Receive() (reply interface{}, err error)
+//
+// Send writes the command to the connection's output buffer. Flush flushes the
+// connection's output buffer to the server. Receive reads a single reply from
+// the server. The following example shows a simple pipeline.
+//
+//  c.Send("SET", "foo", "bar")
+//  c.Send("GET", "foo")
+//  c.Flush()
+//  c.Receive() // reply from SET
+//  v, err = c.Receive() // reply from GET
+//
+// The Do method combines the functionality of the Send, Flush and Receive
+// methods. The Do method starts by writing the command and flushing the output
+// buffer. Next, the Do method receives all pending replies including the reply
+// for the command just sent by Do. If any of the received replies is an error,
+// then Do returns the error. If there are no errors, then Do returns the last
+// reply. If the command argument to the Do method is "", then the Do method
+// will flush the output buffer and receive pending replies without sending a
+// command.
+//
+// Use the Send and Do methods to implement pipelined transactions.
+//
+//  c.Send("MULTI")
+//  c.Send("INCR", "foo")
+//  c.Send("INCR", "bar")
+//  r, err := c.Do("EXEC")
+//  fmt.Println(r) // prints [1, 1]
+//
+// Concurrency
+//
+// Connections support one concurrent caller to the Receive method and one
+// concurrent caller to the Send and Flush methods. No other concurrency is
+// supported including concurrent calls to the Do and Close methods.
+//
+// For full concurrent access to Redis, use the thread-safe Pool to get, use
+// and release a connection from within a goroutine. Connections returned from
+// a Pool have the concurrency restrictions described in the previous
+// paragraph.
+//
+// Publish and Subscribe
+//
+// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
+//
+//  c.Send("SUBSCRIBE", "example")
+//  c.Flush()
+//  for {
+//      reply, err := c.Receive()
+//      if err != nil {
+//          return err
+//      }
+//      // process pushed message
+//  }
+//
+// The PubSubConn type wraps a Conn with convenience methods for implementing
+// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
+// send and flush a subscription management command. The receive method
+// converts a pushed message to convenient types for use in a type switch.
+//
+//  psc := redis.PubSubConn{Conn: c}
+//  psc.Subscribe("example")
+//  for {
+//      switch v := psc.Receive().(type) {
+//      case redis.Message:
+//          fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
+//      case redis.Subscription:
+//          fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
+//      case error:
+//          return v
+//      }
+//  }
+//
+// Reply Helpers
+//
+// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
+// to a value of a specific type. To allow convenient wrapping of calls to the
+// connection Do and Receive methods, the functions take a second argument of
+// type error.  If the error is non-nil, then the helper function returns the
+// error. If the error is nil, the function converts the reply to the specified
+// type:
+//
+//  exists, err := redis.Bool(c.Do("EXISTS", "foo"))
+//  if err != nil {
+//      // handle error return from c.Do or type conversion error.
+//  }
+//
+// The Scan function converts elements of a array reply to Go types:
+//
+//  var value1 int
+//  var value2 string
+//  reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
+//  if err != nil {
+//      // handle error
+//  }
+//   if _, err := redis.Scan(reply, &value1, &value2); err != nil {
+//      // handle error
+//  }
+//
+// Errors
+//
+// Connection methods return error replies from the server as type redis.Error.
+//
+// Call the connection Err() method to determine if the connection encountered
+// non-recoverable error such as a network error or protocol parsing error. If
+// Err() returns a non-nil value, then the connection is not usable and should
+// be closed.
+package redis

+ 29 - 0
src/github.com/gomodule/redigo/redis/go17.go

@@ -0,0 +1,29 @@
+// +build go1.7,!go1.8
+
+package redis
+
+import "crypto/tls"
+
+func cloneTLSConfig(cfg *tls.Config) *tls.Config {
+	return &tls.Config{
+		Rand:                        cfg.Rand,
+		Time:                        cfg.Time,
+		Certificates:                cfg.Certificates,
+		NameToCertificate:           cfg.NameToCertificate,
+		GetCertificate:              cfg.GetCertificate,
+		RootCAs:                     cfg.RootCAs,
+		NextProtos:                  cfg.NextProtos,
+		ServerName:                  cfg.ServerName,
+		ClientAuth:                  cfg.ClientAuth,
+		ClientCAs:                   cfg.ClientCAs,
+		InsecureSkipVerify:          cfg.InsecureSkipVerify,
+		CipherSuites:                cfg.CipherSuites,
+		PreferServerCipherSuites:    cfg.PreferServerCipherSuites,
+		ClientSessionCache:          cfg.ClientSessionCache,
+		MinVersion:                  cfg.MinVersion,
+		MaxVersion:                  cfg.MaxVersion,
+		CurvePreferences:            cfg.CurvePreferences,
+		DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled,
+		Renegotiation:               cfg.Renegotiation,
+	}
+}

+ 9 - 0
src/github.com/gomodule/redigo/redis/go18.go

@@ -0,0 +1,9 @@
+// +build go1.8
+
+package redis
+
+import "crypto/tls"
+
+func cloneTLSConfig(cfg *tls.Config) *tls.Config {
+	return cfg.Clone()
+}

+ 85 - 0
src/github.com/gomodule/redigo/redis/list_test.go

@@ -0,0 +1,85 @@
+// Copyright 2018 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// +build go1.9
+
+package redis
+
+import "testing"
+
+func TestPoolList(t *testing.T) {
+	var idle idleList
+	var a, b, c poolConn
+
+	check := func(pcs ...*poolConn) {
+		if idle.count != len(pcs) {
+			t.Fatal("idle.count != len(pcs)")
+		}
+		if len(pcs) == 0 {
+			if idle.front != nil {
+				t.Fatalf("front not nil")
+			}
+			if idle.back != nil {
+				t.Fatalf("back not nil")
+			}
+			return
+		}
+		if idle.front != pcs[0] {
+			t.Fatal("front != pcs[0]")
+		}
+		if idle.back != pcs[len(pcs)-1] {
+			t.Fatal("back != pcs[len(pcs)-1]")
+		}
+		if idle.front.prev != nil {
+			t.Fatal("front.prev != nil")
+		}
+		if idle.back.next != nil {
+			t.Fatal("back.next != nil")
+		}
+		for i := 1; i < len(pcs)-1; i++ {
+			if pcs[i-1].next != pcs[i] {
+				t.Fatal("pcs[i-1].next != pcs[i]")
+			}
+			if pcs[i+1].prev != pcs[i] {
+				t.Fatal("pcs[i+1].prev != pcs[i]")
+			}
+		}
+	}
+
+	idle.pushFront(&c)
+	check(&c)
+	idle.pushFront(&b)
+	check(&b, &c)
+	idle.pushFront(&a)
+	check(&a, &b, &c)
+	idle.popFront()
+	check(&b, &c)
+	idle.popFront()
+	check(&c)
+	idle.popFront()
+	check()
+
+	idle.pushFront(&c)
+	check(&c)
+	idle.pushFront(&b)
+	check(&b, &c)
+	idle.pushFront(&a)
+	check(&a, &b, &c)
+	idle.popBack()
+	check(&a, &b)
+	idle.popBack()
+	check(&a)
+	idle.popBack()
+	check()
+}

+ 146 - 0
src/github.com/gomodule/redigo/redis/log.go

@@ -0,0 +1,146 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"time"
+)
+
+var (
+	_ ConnWithTimeout = (*loggingConn)(nil)
+)
+
+// NewLoggingConn returns a logging wrapper around a connection.
+func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
+	if prefix != "" {
+		prefix = prefix + "."
+	}
+	return &loggingConn{conn, logger, prefix, nil}
+}
+
+//NewLoggingConnFilter returns a logging wrapper around a connection and a filter function.
+func NewLoggingConnFilter(conn Conn, logger *log.Logger, prefix string, skip func(cmdName string) bool) Conn {
+	if prefix != "" {
+		prefix = prefix + "."
+	}
+	return &loggingConn{conn, logger, prefix, skip}
+}
+
+type loggingConn struct {
+	Conn
+	logger *log.Logger
+	prefix string
+	skip   func(cmdName string) bool
+}
+
+func (c *loggingConn) Close() error {
+	err := c.Conn.Close()
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
+	c.logger.Output(2, buf.String())
+	return err
+}
+
+func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
+	const chop = 32
+	switch v := v.(type) {
+	case []byte:
+		if len(v) > chop {
+			fmt.Fprintf(buf, "%q...", v[:chop])
+		} else {
+			fmt.Fprintf(buf, "%q", v)
+		}
+	case string:
+		if len(v) > chop {
+			fmt.Fprintf(buf, "%q...", v[:chop])
+		} else {
+			fmt.Fprintf(buf, "%q", v)
+		}
+	case []interface{}:
+		if len(v) == 0 {
+			buf.WriteString("[]")
+		} else {
+			sep := "["
+			fin := "]"
+			if len(v) > chop {
+				v = v[:chop]
+				fin = "...]"
+			}
+			for _, vv := range v {
+				buf.WriteString(sep)
+				c.printValue(buf, vv)
+				sep = ", "
+			}
+			buf.WriteString(fin)
+		}
+	default:
+		fmt.Fprint(buf, v)
+	}
+}
+
+func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
+	if c.skip != nil && c.skip(commandName) {
+		return
+	}
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
+	if method != "Receive" {
+		buf.WriteString(commandName)
+		for _, arg := range args {
+			buf.WriteString(", ")
+			c.printValue(&buf, arg)
+		}
+	}
+	buf.WriteString(") -> (")
+	if method != "Send" {
+		c.printValue(&buf, reply)
+		buf.WriteString(", ")
+	}
+	fmt.Fprintf(&buf, "%v)", err)
+	c.logger.Output(3, buf.String())
+}
+
+func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
+	reply, err := c.Conn.Do(commandName, args...)
+	c.print("Do", commandName, args, reply, err)
+	return reply, err
+}
+
+func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) {
+	reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...)
+	c.print("DoWithTimeout", commandName, args, reply, err)
+	return reply, err
+}
+
+func (c *loggingConn) Send(commandName string, args ...interface{}) error {
+	err := c.Conn.Send(commandName, args...)
+	c.print("Send", commandName, args, nil, err)
+	return err
+}
+
+func (c *loggingConn) Receive() (interface{}, error) {
+	reply, err := c.Conn.Receive()
+	c.print("Receive", "", nil, reply, err)
+	return reply, err
+}
+
+func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
+	reply, err := ReceiveWithTimeout(c.Conn, timeout)
+	c.print("ReceiveWithTimeout", "", nil, reply, err)
+	return reply, err
+}

+ 621 - 0
src/github.com/gomodule/redigo/redis/pool.go

@@ -0,0 +1,621 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"crypto/sha1"
+	"errors"
+	"io"
+	"strconv"
+	"sync"
+	"sync/atomic"
+	"time"
+)
+
+var (
+	_ ConnWithTimeout = (*activeConn)(nil)
+	_ ConnWithTimeout = (*errorConn)(nil)
+)
+
+var nowFunc = time.Now // for testing
+
+// ErrPoolExhausted is returned from a pool connection method (Do, Send,
+// Receive, Flush, Err) when the maximum number of database connections in the
+// pool has been reached.
+var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
+
+var (
+	errPoolClosed = errors.New("redigo: connection pool closed")
+	errConnClosed = errors.New("redigo: connection closed")
+)
+
+// Pool maintains a pool of connections. The application calls the Get method
+// to get a connection from the pool and the connection's Close method to
+// return the connection's resources to the pool.
+//
+// The following example shows how to use a pool in a web application. The
+// application creates a pool at application startup and makes it available to
+// request handlers using a package level variable. The pool configuration used
+// here is an example, not a recommendation.
+//
+//  func newPool(addr string) *redis.Pool {
+//    return &redis.Pool{
+//      MaxIdle: 3,
+//      IdleTimeout: 240 * time.Second,
+//      // Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.
+//      Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
+//    }
+//  }
+//
+//  var (
+//    pool *redis.Pool
+//    redisServer = flag.String("redisServer", ":6379", "")
+//  )
+//
+//  func main() {
+//    flag.Parse()
+//    pool = newPool(*redisServer)
+//    ...
+//  }
+//
+// A request handler gets a connection from the pool and closes the connection
+// when the handler is done:
+//
+//  func serveHome(w http.ResponseWriter, r *http.Request) {
+//      conn := pool.Get()
+//      defer conn.Close()
+//      ...
+//  }
+//
+// Use the Dial function to authenticate connections with the AUTH command or
+// select a database with the SELECT command:
+//
+//  pool := &redis.Pool{
+//    // Other pool configuration not shown in this example.
+//    Dial: func () (redis.Conn, error) {
+//      c, err := redis.Dial("tcp", server)
+//      if err != nil {
+//        return nil, err
+//      }
+//      if _, err := c.Do("AUTH", password); err != nil {
+//        c.Close()
+//        return nil, err
+//      }
+//      if _, err := c.Do("SELECT", db); err != nil {
+//        c.Close()
+//        return nil, err
+//      }
+//      return c, nil
+//    },
+//  }
+//
+// Use the TestOnBorrow function to check the health of an idle connection
+// before the connection is returned to the application. This example PINGs
+// connections that have been idle more than a minute:
+//
+//  pool := &redis.Pool{
+//    // Other pool configuration not shown in this example.
+//    TestOnBorrow: func(c redis.Conn, t time.Time) error {
+//      if time.Since(t) < time.Minute {
+//        return nil
+//      }
+//      _, err := c.Do("PING")
+//      return err
+//    },
+//  }
+//
+type Pool struct {
+	// Dial is an application supplied function for creating and configuring a
+	// connection.
+	//
+	// The connection returned from Dial must not be in a special state
+	// (subscribed to pubsub channel, transaction started, ...).
+	Dial func() (Conn, error)
+
+	// DialContext is an application supplied function for creating and configuring a
+	// connection with the given context.
+	//
+	// The connection returned from Dial must not be in a special state
+	// (subscribed to pubsub channel, transaction started, ...).
+	DialContext func(ctx context.Context) (Conn, error)
+
+	// TestOnBorrow is an optional application supplied function for checking
+	// the health of an idle connection before the connection is used again by
+	// the application. Argument t is the time that the connection was returned
+	// to the pool. If the function returns an error, then the connection is
+	// closed.
+	TestOnBorrow func(c Conn, t time.Time) error
+
+	// Maximum number of idle connections in the pool.
+	MaxIdle int
+
+	// Maximum number of connections allocated by the pool at a given time.
+	// When zero, there is no limit on the number of connections in the pool.
+	MaxActive int
+
+	// Close connections after remaining idle for this duration. If the value
+	// is zero, then idle connections are not closed. Applications should set
+	// the timeout to a value less than the server's timeout.
+	IdleTimeout time.Duration
+
+	// If Wait is true and the pool is at the MaxActive limit, then Get() waits
+	// for a connection to be returned to the pool before returning.
+	Wait bool
+
+	// Close connections older than this duration. If the value is zero, then
+	// the pool does not close connections based on age.
+	MaxConnLifetime time.Duration
+
+	chInitialized uint32 // set to 1 when field ch is initialized
+
+	mu           sync.Mutex    // mu protects the following fields
+	closed       bool          // set to true when the pool is closed.
+	active       int           // the number of open connections in the pool
+	ch           chan struct{} // limits open connections when p.Wait is true
+	idle         idleList      // idle connections
+	waitCount    int64         // total number of connections waited for.
+	waitDuration time.Duration // total time waited for new connections.
+}
+
+// NewPool creates a new pool.
+//
+// Deprecated: Initialize the Pool directory as shown in the example.
+func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
+	return &Pool{Dial: newFn, MaxIdle: maxIdle}
+}
+
+// Get gets a connection. The application must close the returned connection.
+// This method always returns a valid connection so that applications can defer
+// error handling to the first use of the connection. If there is an error
+// getting an underlying connection, then the connection Err, Do, Send, Flush
+// and Receive methods return that error.
+func (p *Pool) Get() Conn {
+	pc, err := p.get(nil)
+	if err != nil {
+		return errorConn{err}
+	}
+	return &activeConn{p: p, pc: pc}
+}
+
+// GetContext gets a connection using the provided context.
+//
+// The provided Context must be non-nil. If the context expires before the
+// connection is complete, an error is returned. Any expiration on the context
+// will not affect the returned connection.
+//
+// If the function completes without error, then the application must close the
+// returned connection.
+func (p *Pool) GetContext(ctx context.Context) (Conn, error) {
+	pc, err := p.get(ctx)
+	if err != nil {
+		return errorConn{err}, err
+	}
+	return &activeConn{p: p, pc: pc}, nil
+}
+
+// PoolStats contains pool statistics.
+type PoolStats struct {
+	// ActiveCount is the number of connections in the pool. The count includes
+	// idle connections and connections in use.
+	ActiveCount int
+	// IdleCount is the number of idle connections in the pool.
+	IdleCount int
+
+	// WaitCount is the total number of connections waited for.
+	// This value is currently not guaranteed to be 100% accurate.
+	WaitCount int64
+
+	// WaitDuration is the total time blocked waiting for a new connection.
+	// This value is currently not guaranteed to be 100% accurate.
+	WaitDuration time.Duration
+}
+
+// Stats returns pool's statistics.
+func (p *Pool) Stats() PoolStats {
+	p.mu.Lock()
+	stats := PoolStats{
+		ActiveCount:  p.active,
+		IdleCount:    p.idle.count,
+		WaitCount:    p.waitCount,
+		WaitDuration: p.waitDuration,
+	}
+	p.mu.Unlock()
+
+	return stats
+}
+
+// ActiveCount returns the number of connections in the pool. The count
+// includes idle connections and connections in use.
+func (p *Pool) ActiveCount() int {
+	p.mu.Lock()
+	active := p.active
+	p.mu.Unlock()
+	return active
+}
+
+// IdleCount returns the number of idle connections in the pool.
+func (p *Pool) IdleCount() int {
+	p.mu.Lock()
+	idle := p.idle.count
+	p.mu.Unlock()
+	return idle
+}
+
+// Close releases the resources used by the pool.
+func (p *Pool) Close() error {
+	p.mu.Lock()
+	if p.closed {
+		p.mu.Unlock()
+		return nil
+	}
+	p.closed = true
+	p.active -= p.idle.count
+	pc := p.idle.front
+	p.idle.count = 0
+	p.idle.front, p.idle.back = nil, nil
+	if p.ch != nil {
+		close(p.ch)
+	}
+	p.mu.Unlock()
+	for ; pc != nil; pc = pc.next {
+		pc.c.Close()
+	}
+	return nil
+}
+
+func (p *Pool) lazyInit() {
+	// Fast path.
+	if atomic.LoadUint32(&p.chInitialized) == 1 {
+		return
+	}
+	// Slow path.
+	p.mu.Lock()
+	if p.chInitialized == 0 {
+		p.ch = make(chan struct{}, p.MaxActive)
+		if p.closed {
+			close(p.ch)
+		} else {
+			for i := 0; i < p.MaxActive; i++ {
+				p.ch <- struct{}{}
+			}
+		}
+		atomic.StoreUint32(&p.chInitialized, 1)
+	}
+	p.mu.Unlock()
+}
+
+// get prunes stale connections and returns a connection from the idle list or
+// creates a new connection.
+func (p *Pool) get(ctx context.Context) (*poolConn, error) {
+
+	// Handle limit for p.Wait == true.
+	var waited time.Duration
+	if p.Wait && p.MaxActive > 0 {
+		p.lazyInit()
+
+		// wait indicates if we believe it will block so its not 100% accurate
+		// however for stats it should be good enough.
+		wait := len(p.ch) == 0
+		var start time.Time
+		if wait {
+			start = time.Now()
+		}
+		if ctx == nil {
+			<-p.ch
+		} else {
+			select {
+			case <-p.ch:
+			case <-ctx.Done():
+				return nil, ctx.Err()
+			}
+		}
+		if wait {
+			waited = time.Since(start)
+		}
+	}
+
+	p.mu.Lock()
+
+	if waited > 0 {
+		p.waitCount++
+		p.waitDuration += waited
+	}
+
+	// Prune stale connections at the back of the idle list.
+	if p.IdleTimeout > 0 {
+		n := p.idle.count
+		for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
+			pc := p.idle.back
+			p.idle.popBack()
+			p.mu.Unlock()
+			pc.c.Close()
+			p.mu.Lock()
+			p.active--
+		}
+	}
+
+	// Get idle connection from the front of idle list.
+	for p.idle.front != nil {
+		pc := p.idle.front
+		p.idle.popFront()
+		p.mu.Unlock()
+		if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
+			(p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
+			return pc, nil
+		}
+		pc.c.Close()
+		p.mu.Lock()
+		p.active--
+	}
+
+	// Check for pool closed before dialing a new connection.
+	if p.closed {
+		p.mu.Unlock()
+		return nil, errors.New("redigo: get on closed pool")
+	}
+
+	// Handle limit for p.Wait == false.
+	if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
+		p.mu.Unlock()
+		return nil, ErrPoolExhausted
+	}
+
+	p.active++
+	p.mu.Unlock()
+	c, err := p.dial(ctx)
+	if err != nil {
+		c = nil
+		p.mu.Lock()
+		p.active--
+		if p.ch != nil && !p.closed {
+			p.ch <- struct{}{}
+		}
+		p.mu.Unlock()
+	}
+	return &poolConn{c: c, created: nowFunc()}, err
+}
+
+func (p *Pool) dial(ctx context.Context) (Conn, error) {
+	if p.DialContext != nil {
+		return p.DialContext(ctx)
+	}
+	if p.Dial != nil {
+		return p.Dial()
+	}
+	return nil, errors.New("redigo: must pass Dial or DialContext to pool")
+}
+
+func (p *Pool) put(pc *poolConn, forceClose bool) error {
+	p.mu.Lock()
+	if !p.closed && !forceClose {
+		pc.t = nowFunc()
+		p.idle.pushFront(pc)
+		if p.idle.count > p.MaxIdle {
+			pc = p.idle.back
+			p.idle.popBack()
+		} else {
+			pc = nil
+		}
+	}
+
+	if pc != nil {
+		p.mu.Unlock()
+		pc.c.Close()
+		p.mu.Lock()
+		p.active--
+	}
+
+	if p.ch != nil && !p.closed {
+		p.ch <- struct{}{}
+	}
+	p.mu.Unlock()
+	return nil
+}
+
+type activeConn struct {
+	p     *Pool
+	pc    *poolConn
+	state int
+}
+
+var (
+	sentinel     []byte
+	sentinelOnce sync.Once
+)
+
+func initSentinel() {
+	p := make([]byte, 64)
+	if _, err := rand.Read(p); err == nil {
+		sentinel = p
+	} else {
+		h := sha1.New()
+		io.WriteString(h, "Oops, rand failed. Use time instead.")
+		io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
+		sentinel = h.Sum(nil)
+	}
+}
+
+func (ac *activeConn) Close() error {
+	pc := ac.pc
+	if pc == nil {
+		return nil
+	}
+	ac.pc = nil
+
+	if ac.state&connectionMultiState != 0 {
+		pc.c.Send("DISCARD")
+		ac.state &^= (connectionMultiState | connectionWatchState)
+	} else if ac.state&connectionWatchState != 0 {
+		pc.c.Send("UNWATCH")
+		ac.state &^= connectionWatchState
+	}
+	if ac.state&connectionSubscribeState != 0 {
+		pc.c.Send("UNSUBSCRIBE")
+		pc.c.Send("PUNSUBSCRIBE")
+		// To detect the end of the message stream, ask the server to echo
+		// a sentinel value and read until we see that value.
+		sentinelOnce.Do(initSentinel)
+		pc.c.Send("ECHO", sentinel)
+		pc.c.Flush()
+		for {
+			p, err := pc.c.Receive()
+			if err != nil {
+				break
+			}
+			if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
+				ac.state &^= connectionSubscribeState
+				break
+			}
+		}
+	}
+	pc.c.Do("")
+	ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil)
+	return nil
+}
+
+func (ac *activeConn) Err() error {
+	pc := ac.pc
+	if pc == nil {
+		return errConnClosed
+	}
+	return pc.c.Err()
+}
+
+func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	ci := lookupCommandInfo(commandName)
+	ac.state = (ac.state | ci.Set) &^ ci.Clear
+	return pc.c.Do(commandName, args...)
+}
+
+func (ac *activeConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	cwt, ok := pc.c.(ConnWithTimeout)
+	if !ok {
+		return nil, errTimeoutNotSupported
+	}
+	ci := lookupCommandInfo(commandName)
+	ac.state = (ac.state | ci.Set) &^ ci.Clear
+	return cwt.DoWithTimeout(timeout, commandName, args...)
+}
+
+func (ac *activeConn) Send(commandName string, args ...interface{}) error {
+	pc := ac.pc
+	if pc == nil {
+		return errConnClosed
+	}
+	ci := lookupCommandInfo(commandName)
+	ac.state = (ac.state | ci.Set) &^ ci.Clear
+	return pc.c.Send(commandName, args...)
+}
+
+func (ac *activeConn) Flush() error {
+	pc := ac.pc
+	if pc == nil {
+		return errConnClosed
+	}
+	return pc.c.Flush()
+}
+
+func (ac *activeConn) Receive() (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	return pc.c.Receive()
+}
+
+func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	cwt, ok := pc.c.(ConnWithTimeout)
+	if !ok {
+		return nil, errTimeoutNotSupported
+	}
+	return cwt.ReceiveWithTimeout(timeout)
+}
+
+type errorConn struct{ err error }
+
+func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
+func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) {
+	return nil, ec.err
+}
+func (ec errorConn) Send(string, ...interface{}) error                     { return ec.err }
+func (ec errorConn) Err() error                                            { return ec.err }
+func (ec errorConn) Close() error                                          { return nil }
+func (ec errorConn) Flush() error                                          { return ec.err }
+func (ec errorConn) Receive() (interface{}, error)                         { return nil, ec.err }
+func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err }
+
+type idleList struct {
+	count       int
+	front, back *poolConn
+}
+
+type poolConn struct {
+	c          Conn
+	t          time.Time
+	created    time.Time
+	next, prev *poolConn
+}
+
+func (l *idleList) pushFront(pc *poolConn) {
+	pc.next = l.front
+	pc.prev = nil
+	if l.count == 0 {
+		l.back = pc
+	} else {
+		l.front.prev = pc
+	}
+	l.front = pc
+	l.count++
+	return
+}
+
+func (l *idleList) popFront() {
+	pc := l.front
+	l.count--
+	if l.count == 0 {
+		l.front, l.back = nil, nil
+	} else {
+		pc.next.prev = nil
+		l.front = pc.next
+	}
+	pc.next, pc.prev = nil, nil
+}
+
+func (l *idleList) popBack() {
+	pc := l.back
+	l.count--
+	if l.count == 0 {
+		l.front, l.back = nil, nil
+	} else {
+		pc.prev.next = nil
+		l.back = pc.prev
+	}
+	pc.next, pc.prev = nil, nil
+}

+ 875 - 0
src/github.com/gomodule/redigo/redis/pool_test.go

@@ -0,0 +1,875 @@
+// Copyright 2011 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"context"
+	"errors"
+	"io"
+	"reflect"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+const (
+	testGoRoutines = 10
+)
+
+type poolTestConn struct {
+	d   *poolDialer
+	err error
+	redis.Conn
+}
+
+func (c *poolTestConn) Close() error {
+	c.d.mu.Lock()
+	c.d.open -= 1
+	c.d.mu.Unlock()
+	return c.Conn.Close()
+}
+
+func (c *poolTestConn) Err() error { return c.err }
+
+func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) {
+	if commandName == "ERR" {
+		c.err = args[0].(error)
+		commandName = "PING"
+	}
+	if commandName != "" {
+		c.d.commands = append(c.d.commands, commandName)
+	}
+	return c.Conn.Do(commandName, args...)
+}
+
+func (c *poolTestConn) Send(commandName string, args ...interface{}) error {
+	c.d.commands = append(c.d.commands, commandName)
+	return c.Conn.Send(commandName, args...)
+}
+
+type poolDialer struct {
+	mu       sync.Mutex
+	t        *testing.T
+	dialed   int
+	open     int
+	commands []string
+	dialErr  error
+}
+
+func (d *poolDialer) dial() (redis.Conn, error) {
+	d.mu.Lock()
+	d.dialed += 1
+	dialErr := d.dialErr
+	d.mu.Unlock()
+	if dialErr != nil {
+		return nil, d.dialErr
+	}
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		return nil, err
+	}
+	d.mu.Lock()
+	d.open += 1
+	d.mu.Unlock()
+	return &poolTestConn{d: d, Conn: c}, nil
+}
+
+func (d *poolDialer) dialContext(ctx context.Context) (redis.Conn, error) {
+	return d.dial()
+}
+
+func (d *poolDialer) check(message string, p *redis.Pool, dialed, open, inuse int) {
+	d.checkAll(message, p, dialed, open, inuse, 0, 0)
+}
+
+func (d *poolDialer) checkAll(message string, p *redis.Pool, dialed, open, inuse int, waitCountMax int64, waitDurationMax time.Duration) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	if d.dialed != dialed {
+		d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed)
+	}
+	if d.open != open {
+		d.t.Errorf("%s: open=%d, want %d", message, d.open, open)
+	}
+
+	stats := p.Stats()
+
+	if stats.ActiveCount != open {
+		d.t.Errorf("%s: active=%d, want %d", message, stats.ActiveCount, open)
+	}
+	if stats.IdleCount != open-inuse {
+		d.t.Errorf("%s: idle=%d, want %d", message, stats.IdleCount, open-inuse)
+	}
+
+	if stats.WaitCount > waitCountMax {
+		d.t.Errorf("%s: unexpected wait=%d want at most %d", message, stats.WaitCount, waitCountMax)
+	}
+
+	if waitCountMax == 0 {
+		if stats.WaitDuration != 0 {
+			d.t.Errorf("%s: unexpected waitDuration=%v want %v", message, stats.WaitDuration, 0)
+		}
+		return
+	}
+
+	if stats.WaitDuration > waitDurationMax {
+		d.t.Errorf("%s: unexpected waitDuration=%v want < %v", message, stats.WaitDuration, waitDurationMax)
+	}
+}
+
+func TestPoolReuse(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle: 2,
+		Dial:    d.dial,
+	}
+
+	for i := 0; i < 10; i++ {
+		c1 := p.Get()
+		c1.Do("PING")
+		c2 := p.Get()
+		c2.Do("PING")
+		c1.Close()
+		c2.Close()
+	}
+
+	d.check("before close", p, 2, 2, 0)
+	p.Close()
+	d.check("after close", p, 2, 0, 0)
+}
+
+func TestPoolMaxIdle(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle: 2,
+		Dial:    d.dial,
+	}
+	defer p.Close()
+
+	for i := 0; i < 10; i++ {
+		c1 := p.Get()
+		c1.Do("PING")
+		c2 := p.Get()
+		c2.Do("PING")
+		c3 := p.Get()
+		c3.Do("PING")
+		c1.Close()
+		c2.Close()
+		c3.Close()
+	}
+	d.check("before close", p, 12, 2, 0)
+	p.Close()
+	d.check("after close", p, 12, 0, 0)
+}
+
+func TestPoolError(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle: 2,
+		Dial:    d.dial,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	c.Do("ERR", io.EOF)
+	if c.Err() == nil {
+		t.Errorf("expected c.Err() != nil")
+	}
+	c.Close()
+
+	c = p.Get()
+	c.Do("ERR", io.EOF)
+	c.Close()
+
+	d.check(".", p, 2, 0, 0)
+}
+
+func TestPoolClose(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle: 2,
+		Dial:    d.dial,
+	}
+	defer p.Close()
+
+	c1 := p.Get()
+	c1.Do("PING")
+	c2 := p.Get()
+	c2.Do("PING")
+	c3 := p.Get()
+	c3.Do("PING")
+
+	c1.Close()
+	if _, err := c1.Do("PING"); err == nil {
+		t.Errorf("expected error after connection closed")
+	}
+
+	c2.Close()
+	c2.Close()
+
+	p.Close()
+
+	d.check("after pool close", p, 3, 1, 1)
+
+	if _, err := c1.Do("PING"); err == nil {
+		t.Errorf("expected error after connection and pool closed")
+	}
+
+	c3.Close()
+
+	d.check("after conn close", p, 3, 0, 0)
+
+	c1 = p.Get()
+	if _, err := c1.Do("PING"); err == nil {
+		t.Errorf("expected error after pool closed")
+	}
+}
+
+func TestPoolClosedConn(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:     2,
+		IdleTimeout: 300 * time.Second,
+		Dial:        d.dial,
+	}
+	defer p.Close()
+	c := p.Get()
+	if c.Err() != nil {
+		t.Fatal("get failed")
+	}
+	c.Close()
+	if err := c.Err(); err == nil {
+		t.Fatal("Err on closed connection did not return error")
+	}
+	if _, err := c.Do("PING"); err == nil {
+		t.Fatal("Do on closed connection did not return error")
+	}
+	if err := c.Send("PING"); err == nil {
+		t.Fatal("Send on closed connection did not return error")
+	}
+	if err := c.Flush(); err == nil {
+		t.Fatal("Flush on closed connection did not return error")
+	}
+	if _, err := c.Receive(); err == nil {
+		t.Fatal("Receive on closed connection did not return error")
+	}
+}
+
+func TestPoolIdleTimeout(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:     2,
+		IdleTimeout: 300 * time.Second,
+		Dial:        d.dial,
+	}
+	defer p.Close()
+
+	now := time.Now()
+	redis.SetNowFunc(func() time.Time { return now })
+	defer redis.SetNowFunc(time.Now)
+
+	c := p.Get()
+	c.Do("PING")
+	c.Close()
+
+	d.check("1", p, 1, 1, 0)
+
+	now = now.Add(p.IdleTimeout + 1)
+
+	c = p.Get()
+	c.Do("PING")
+	c.Close()
+
+	d.check("2", p, 2, 1, 0)
+}
+
+func TestPoolMaxLifetime(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:         2,
+		MaxConnLifetime: 300 * time.Second,
+		Dial:            d.dial,
+	}
+	defer p.Close()
+
+	now := time.Now()
+	redis.SetNowFunc(func() time.Time { return now })
+	defer redis.SetNowFunc(time.Now)
+
+	c := p.Get()
+	c.Do("PING")
+	c.Close()
+
+	d.check("1", p, 1, 1, 0)
+
+	now = now.Add(p.MaxConnLifetime + 1)
+
+	c = p.Get()
+	c.Do("PING")
+	c.Close()
+
+	d.check("2", p, 2, 1, 0)
+}
+
+func TestPoolConcurrenSendReceive(t *testing.T) {
+	p := &redis.Pool{
+		Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() },
+	}
+	defer p.Close()
+
+	c := p.Get()
+	done := make(chan error, 1)
+	go func() {
+		_, err := c.Receive()
+		done <- err
+	}()
+	c.Send("PING")
+	c.Flush()
+	err := <-done
+	if err != nil {
+		t.Fatalf("Receive() returned error %v", err)
+	}
+	_, err = c.Do("")
+	if err != nil {
+		t.Fatalf("Do() returned error %v", err)
+	}
+	c.Close()
+}
+
+func TestPoolBorrowCheck(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:      2,
+		Dial:         d.dial,
+		TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") },
+	}
+	defer p.Close()
+
+	for i := 0; i < 10; i++ {
+		c := p.Get()
+		c.Do("PING")
+		c.Close()
+	}
+	d.check("1", p, 10, 1, 0)
+}
+
+func TestPoolMaxActive(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   2,
+		MaxActive: 2,
+		Dial:      d.dial,
+	}
+	defer p.Close()
+
+	c1 := p.Get()
+	c1.Do("PING")
+	c2 := p.Get()
+	c2.Do("PING")
+
+	d.check("1", p, 2, 2, 2)
+
+	c3 := p.Get()
+	if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted {
+		t.Errorf("expected pool exhausted")
+	}
+
+	c3.Close()
+	d.check("2", p, 2, 2, 2)
+	c2.Close()
+	d.check("3", p, 2, 2, 1)
+
+	c3 = p.Get()
+	if _, err := c3.Do("PING"); err != nil {
+		t.Errorf("expected good channel, err=%v", err)
+	}
+	c3.Close()
+
+	d.check("4", p, 2, 2, 1)
+}
+
+func TestPoolWaitStats(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		Wait:      true,
+		MaxIdle:   2,
+		MaxActive: 2,
+		Dial:      d.dial,
+	}
+	defer p.Close()
+
+	c1 := p.Get()
+	c1.Do("PING")
+	c2 := p.Get()
+	c2.Do("PING")
+
+	d.checkAll("1", p, 2, 2, 2, 0, 0)
+
+	start := time.Now()
+	go func() {
+		time.Sleep(time.Millisecond * 100)
+		c1.Close()
+	}()
+
+	c3 := p.Get()
+	d.checkAll("2", p, 2, 2, 2, 1, time.Since(start))
+
+	if _, err := c3.Do("PING"); err != nil {
+		t.Errorf("expected good channel, err=%v", err)
+	}
+}
+
+func TestPoolMonitorCleanup(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   2,
+		MaxActive: 2,
+		Dial:      d.dial,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	c.Send("MONITOR")
+	c.Close()
+
+	d.check("", p, 1, 0, 0)
+}
+
+func TestPoolPubSubCleanup(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   2,
+		MaxActive: 2,
+		Dial:      d.dial,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	c.Send("SUBSCRIBE", "x")
+	c.Close()
+
+	want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+
+	c = p.Get()
+	c.Send("PSUBSCRIBE", "x*")
+	c.Close()
+
+	want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+}
+
+func TestPoolTransactionCleanup(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   2,
+		MaxActive: 2,
+		Dial:      d.dial,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	c.Do("WATCH", "key")
+	c.Do("PING")
+	c.Close()
+
+	want := []string{"WATCH", "PING", "UNWATCH"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+
+	c = p.Get()
+	c.Do("WATCH", "key")
+	c.Do("UNWATCH")
+	c.Do("PING")
+	c.Close()
+
+	want = []string{"WATCH", "UNWATCH", "PING"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+
+	c = p.Get()
+	c.Do("WATCH", "key")
+	c.Do("MULTI")
+	c.Do("PING")
+	c.Close()
+
+	want = []string{"WATCH", "MULTI", "PING", "DISCARD"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+
+	c = p.Get()
+	c.Do("WATCH", "key")
+	c.Do("MULTI")
+	c.Do("DISCARD")
+	c.Do("PING")
+	c.Close()
+
+	want = []string{"WATCH", "MULTI", "DISCARD", "PING"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+
+	c = p.Get()
+	c.Do("WATCH", "key")
+	c.Do("MULTI")
+	c.Do("EXEC")
+	c.Do("PING")
+	c.Close()
+
+	want = []string{"WATCH", "MULTI", "EXEC", "PING"}
+	if !reflect.DeepEqual(d.commands, want) {
+		t.Errorf("got commands %v, want %v", d.commands, want)
+	}
+	d.commands = nil
+}
+
+func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error {
+	errs := make(chan error, testGoRoutines)
+	for i := 0; i < cap(errs); i++ {
+		go func() {
+			c := p.Get()
+			_, err := c.Do(cmd, args...)
+			c.Close()
+			errs <- err
+		}()
+	}
+
+	return errs
+}
+
+func TestWaitPool(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	start := time.Now()
+	errs := startGoroutines(p, "PING")
+	d.check("before close", p, 1, 1, 1)
+	c.Close()
+	timeout := time.After(2 * time.Second)
+	for i := 0; i < cap(errs); i++ {
+		select {
+		case err := <-errs:
+			if err != nil {
+				t.Fatal(err)
+			}
+		case <-timeout:
+			t.Fatalf("timeout waiting for blocked goroutine %d", i)
+		}
+	}
+	d.checkAll("done", p, 1, 1, 0, testGoRoutines, time.Since(start)*testGoRoutines)
+}
+
+func TestWaitPoolClose(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	if _, err := c.Do("PING"); err != nil {
+		t.Fatal(err)
+	}
+	start := time.Now()
+	errs := startGoroutines(p, "PING")
+	d.check("before close", p, 1, 1, 1)
+	p.Close()
+	timeout := time.After(2 * time.Second)
+	for i := 0; i < cap(errs); i++ {
+		select {
+		case err := <-errs:
+			switch err {
+			case nil:
+				t.Fatal("blocked goroutine did not get error")
+			case redis.ErrPoolExhausted:
+				t.Fatal("blocked goroutine got pool exhausted error")
+			}
+		case <-timeout:
+			t.Fatal("timeout waiting for blocked goroutine")
+		}
+	}
+	c.Close()
+	d.checkAll("done", p, 1, 0, 0, testGoRoutines, time.Since(start)*testGoRoutines)
+}
+
+func TestWaitPoolCommandError(t *testing.T) {
+	testErr := errors.New("test")
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	start := time.Now()
+	errs := startGoroutines(p, "ERR", testErr)
+	d.check("before close", p, 1, 1, 1)
+	c.Close()
+	timeout := time.After(2 * time.Second)
+	for i := 0; i < cap(errs); i++ {
+		select {
+		case err := <-errs:
+			if err != nil {
+				t.Fatal(err)
+			}
+		case <-timeout:
+			t.Fatalf("timeout waiting for blocked goroutine %d", i)
+		}
+	}
+	d.checkAll("done", p, cap(errs), 0, 0, testGoRoutines, time.Since(start)*testGoRoutines)
+}
+
+func TestWaitPoolDialError(t *testing.T) {
+	testErr := errors.New("test")
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+
+	c := p.Get()
+	start := time.Now()
+	errs := startGoroutines(p, "ERR", testErr)
+	d.check("before close", p, 1, 1, 1)
+
+	d.dialErr = errors.New("dial")
+	c.Close()
+
+	nilCount := 0
+	errCount := 0
+	timeout := time.After(2 * time.Second)
+	for i := 0; i < cap(errs); i++ {
+		select {
+		case err := <-errs:
+			switch err {
+			case nil:
+				nilCount++
+			case d.dialErr:
+				errCount++
+			default:
+				t.Fatalf("expected dial error or nil, got %v", err)
+			}
+		case <-timeout:
+			t.Fatalf("timeout waiting for blocked goroutine %d", i)
+		}
+	}
+	if nilCount != 1 {
+		t.Errorf("expected one nil error, got %d", nilCount)
+	}
+	if errCount != cap(errs)-1 {
+		t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount)
+	}
+	d.checkAll("done", p, cap(errs), 0, 0, testGoRoutines, time.Since(start)*testGoRoutines)
+}
+
+// Borrowing requires us to iterate over the idle connections, unlock the pool,
+// and perform a blocking operation to check the connection still works. If
+// TestOnBorrow fails, we must reacquire the lock and continue iteration. This
+// test ensures that iteration will work correctly if multiple threads are
+// iterating simultaneously.
+func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) {
+	const count = 100
+
+	// First we'll Create a pool where the pilfering of idle connections fails.
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   count,
+		MaxActive: count,
+		Dial:      d.dial,
+		TestOnBorrow: func(c redis.Conn, t time.Time) error {
+			return errors.New("No way back into the real world.")
+		},
+	}
+	defer p.Close()
+
+	// Fill the pool with idle connections.
+	conns := make([]redis.Conn, count)
+	for i := range conns {
+		conns[i] = p.Get()
+	}
+	for i := range conns {
+		conns[i].Close()
+	}
+
+	// Spawn a bunch of goroutines to thrash the pool.
+	var wg sync.WaitGroup
+	wg.Add(count)
+	for i := 0; i < count; i++ {
+		go func() {
+			c := p.Get()
+			if c.Err() != nil {
+				t.Errorf("pool get failed: %v", c.Err())
+			}
+			c.Close()
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	if d.dialed != count*2 {
+		t.Errorf("Expected %d dials, got %d", count*2, d.dialed)
+	}
+}
+
+func BenchmarkPoolGet(b *testing.B) {
+	b.StopTimer()
+	p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2}
+	c := p.Get()
+	if err := c.Err(); err != nil {
+		b.Fatal(err)
+	}
+	c.Close()
+	defer p.Close()
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		c = p.Get()
+		c.Close()
+	}
+}
+
+func BenchmarkPoolGetErr(b *testing.B) {
+	b.StopTimer()
+	p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2}
+	c := p.Get()
+	if err := c.Err(); err != nil {
+		b.Fatal(err)
+	}
+	c.Close()
+	defer p.Close()
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		c = p.Get()
+		if err := c.Err(); err != nil {
+			b.Fatal(err)
+		}
+		c.Close()
+	}
+}
+
+func BenchmarkPoolGetPing(b *testing.B) {
+	b.StopTimer()
+	p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2}
+	c := p.Get()
+	if err := c.Err(); err != nil {
+		b.Fatal(err)
+	}
+	c.Close()
+	defer p.Close()
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		c = p.Get()
+		if _, err := c.Do("PING"); err != nil {
+			b.Fatal(err)
+		}
+		c.Close()
+	}
+}
+
+func TestWaitPoolGetContext(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+	c, err := p.GetContext(context.Background())
+	if err != nil {
+		t.Fatalf("GetContext returned %v", err)
+	}
+	defer c.Close()
+}
+
+func TestWaitPoolGetContextWithDialContext(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:     1,
+		MaxActive:   1,
+		DialContext: d.dialContext,
+		Wait:        true,
+	}
+	defer p.Close()
+	c, err := p.GetContext(context.Background())
+	if err != nil {
+		t.Fatalf("GetContext returned %v", err)
+	}
+	defer c.Close()
+}
+
+func TestWaitPoolGetAfterClose(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	p.Close()
+	_, err := p.GetContext(context.Background())
+	if err == nil {
+		t.Fatal("expected error")
+	}
+}
+
+func TestWaitPoolGetCanceledContext(t *testing.T) {
+	d := poolDialer{t: t}
+	p := &redis.Pool{
+		MaxIdle:   1,
+		MaxActive: 1,
+		Dial:      d.dial,
+		Wait:      true,
+	}
+	defer p.Close()
+	ctx, f := context.WithCancel(context.Background())
+	f()
+	c := p.Get()
+	defer c.Close()
+	_, err := p.GetContext(ctx)
+	if err != context.Canceled {
+		t.Fatalf("got error %v, want %v", err, context.Canceled)
+	}
+}

+ 148 - 0
src/github.com/gomodule/redigo/redis/pubsub.go

@@ -0,0 +1,148 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"time"
+)
+
+// Subscription represents a subscribe or unsubscribe notification.
+type Subscription struct {
+	// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
+	Kind string
+
+	// The channel that was changed.
+	Channel string
+
+	// The current number of subscriptions for connection.
+	Count int
+}
+
+// Message represents a message notification.
+type Message struct {
+	// The originating channel.
+	Channel string
+
+	// The matched pattern, if any
+	Pattern string
+
+	// The message data.
+	Data []byte
+}
+
+// Pong represents a pubsub pong notification.
+type Pong struct {
+	Data string
+}
+
+// PubSubConn wraps a Conn with convenience methods for subscribers.
+type PubSubConn struct {
+	Conn Conn
+}
+
+// Close closes the connection.
+func (c PubSubConn) Close() error {
+	return c.Conn.Close()
+}
+
+// Subscribe subscribes the connection to the specified channels.
+func (c PubSubConn) Subscribe(channel ...interface{}) error {
+	c.Conn.Send("SUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// PSubscribe subscribes the connection to the given patterns.
+func (c PubSubConn) PSubscribe(channel ...interface{}) error {
+	c.Conn.Send("PSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// Unsubscribe unsubscribes the connection from the given channels, or from all
+// of them if none is given.
+func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
+	c.Conn.Send("UNSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// PUnsubscribe unsubscribes the connection from the given patterns, or from all
+// of them if none is given.
+func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
+	c.Conn.Send("PUNSUBSCRIBE", channel...)
+	return c.Conn.Flush()
+}
+
+// Ping sends a PING to the server with the specified data.
+//
+// The connection must be subscribed to at least one channel or pattern when
+// calling this method.
+func (c PubSubConn) Ping(data string) error {
+	c.Conn.Send("PING", data)
+	return c.Conn.Flush()
+}
+
+// Receive returns a pushed message as a Subscription, Message, Pong or error.
+// The return value is intended to be used directly in a type switch as
+// illustrated in the PubSubConn example.
+func (c PubSubConn) Receive() interface{} {
+	return c.receiveInternal(c.Conn.Receive())
+}
+
+// ReceiveWithTimeout is like Receive, but it allows the application to
+// override the connection's default timeout.
+func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} {
+	return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout))
+}
+
+func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} {
+	reply, err := Values(replyArg, errArg)
+	if err != nil {
+		return err
+	}
+
+	var kind string
+	reply, err = Scan(reply, &kind)
+	if err != nil {
+		return err
+	}
+
+	switch kind {
+	case "message":
+		var m Message
+		if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
+			return err
+		}
+		return m
+	case "pmessage":
+		var m Message
+		if _, err := Scan(reply, &m.Pattern, &m.Channel, &m.Data); err != nil {
+			return err
+		}
+		return m
+	case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
+		s := Subscription{Kind: kind}
+		if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
+			return err
+		}
+		return s
+	case "pong":
+		var p Pong
+		if _, err := Scan(reply, &p.Data); err != nil {
+			return err
+		}
+		return p
+	}
+	return errors.New("redigo: unknown pubsub notification")
+}

+ 165 - 0
src/github.com/gomodule/redigo/redis/pubsub_example_test.go

@@ -0,0 +1,165 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// +build go1.7
+
+package redis_test
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+// listenPubSubChannels listens for messages on Redis pubsub channels. The
+// onStart function is called after the channels are subscribed. The onMessage
+// function is called for each message.
+func listenPubSubChannels(ctx context.Context, redisServerAddr string,
+	onStart func() error,
+	onMessage func(channel string, data []byte) error,
+	channels ...string) error {
+	// A ping is set to the server with this period to test for the health of
+	// the connection and server.
+	const healthCheckPeriod = time.Minute
+
+	c, err := redis.Dial("tcp", redisServerAddr,
+		// Read timeout on server should be greater than ping period.
+		redis.DialReadTimeout(healthCheckPeriod+10*time.Second),
+		redis.DialWriteTimeout(10*time.Second))
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+
+	psc := redis.PubSubConn{Conn: c}
+
+	if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil {
+		return err
+	}
+
+	done := make(chan error, 1)
+
+	// Start a goroutine to receive notifications from the server.
+	go func() {
+		for {
+			switch n := psc.Receive().(type) {
+			case error:
+				done <- n
+				return
+			case redis.Message:
+				if err := onMessage(n.Channel, n.Data); err != nil {
+					done <- err
+					return
+				}
+			case redis.Subscription:
+				switch n.Count {
+				case len(channels):
+					// Notify application when all channels are subscribed.
+					if err := onStart(); err != nil {
+						done <- err
+						return
+					}
+				case 0:
+					// Return from the goroutine when all channels are unsubscribed.
+					done <- nil
+					return
+				}
+			}
+		}
+	}()
+
+	ticker := time.NewTicker(healthCheckPeriod)
+	defer ticker.Stop()
+loop:
+	for err == nil {
+		select {
+		case <-ticker.C:
+			// Send ping to test health of connection and server. If
+			// corresponding pong is not received, then receive on the
+			// connection will timeout and the receive goroutine will exit.
+			if err = psc.Ping(""); err != nil {
+				break loop
+			}
+		case <-ctx.Done():
+			break loop
+		case err := <-done:
+			// Return error from the receive goroutine.
+			return err
+		}
+	}
+
+	// Signal the receiving goroutine to exit by unsubscribing from all channels.
+	psc.Unsubscribe()
+
+	// Wait for goroutine to complete.
+	return <-done
+}
+
+func publish() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Do("PUBLISH", "c1", "hello")
+	c.Do("PUBLISH", "c2", "world")
+	c.Do("PUBLISH", "c1", "goodbye")
+}
+
+// This example shows how receive pubsub notifications with cancelation and
+// health checks.
+func ExamplePubSubConn() {
+	redisServerAddr, err := serverAddr()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	err = listenPubSubChannels(ctx,
+		redisServerAddr,
+		func() error {
+			// The start callback is a good place to backfill missed
+			// notifications. For the purpose of this example, a goroutine is
+			// started to send notifications.
+			go publish()
+			return nil
+		},
+		func(channel string, message []byte) error {
+			fmt.Printf("channel: %s, message: %s\n", channel, message)
+
+			// For the purpose of this example, cancel the listener's context
+			// after receiving last message sent by publish().
+			if string(message) == "goodbye" {
+				cancel()
+			}
+			return nil
+		},
+		"c1", "c2")
+
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Output:
+	// channel: c1, message: hello
+	// channel: c2, message: world
+	// channel: c1, message: goodbye
+}

+ 74 - 0
src/github.com/gomodule/redigo/redis/pubsub_test.go

@@ -0,0 +1,74 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) {
+	actual := c.Receive()
+	if !reflect.DeepEqual(actual, expected) {
+		t.Errorf("%s = %v, want %v", message, actual, expected)
+	}
+}
+
+func TestPushed(t *testing.T) {
+	pc, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer pc.Close()
+
+	sc, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer sc.Close()
+
+	c := redis.PubSubConn{Conn: sc}
+
+	c.Subscribe("c1")
+	expectPushed(t, c, "Subscribe(c1)", redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1})
+	c.Subscribe("c2")
+	expectPushed(t, c, "Subscribe(c2)", redis.Subscription{Kind: "subscribe", Channel: "c2", Count: 2})
+	c.PSubscribe("p1")
+	expectPushed(t, c, "PSubscribe(p1)", redis.Subscription{Kind: "psubscribe", Channel: "p1", Count: 3})
+	c.PSubscribe("p2")
+	expectPushed(t, c, "PSubscribe(p2)", redis.Subscription{Kind: "psubscribe", Channel: "p2", Count: 4})
+	c.PUnsubscribe()
+	expectPushed(t, c, "Punsubscribe(p1)", redis.Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3})
+	expectPushed(t, c, "Punsubscribe()", redis.Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2})
+
+	pc.Do("PUBLISH", "c1", "hello")
+	expectPushed(t, c, "PUBLISH c1 hello", redis.Message{Channel: "c1", Data: []byte("hello")})
+
+	c.Ping("hello")
+	expectPushed(t, c, `Ping("hello")`, redis.Pong{Data: "hello"})
+
+	c.Conn.Send("PING")
+	c.Conn.Flush()
+	expectPushed(t, c, `Send("PING")`, redis.Pong{})
+
+	c.Ping("timeout")
+	got := c.ReceiveWithTimeout(time.Minute)
+	if want := (redis.Pong{Data: "timeout"}); want != got {
+		t.Errorf("recv /w timeout got %v, want %v", got, want)
+	}
+}

+ 117 - 0
src/github.com/gomodule/redigo/redis/redis.go

@@ -0,0 +1,117 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"time"
+)
+
+// Error represents an error returned in a command reply.
+type Error string
+
+func (err Error) Error() string { return string(err) }
+
+// Conn represents a connection to a Redis server.
+type Conn interface {
+	// Close closes the connection.
+	Close() error
+
+	// Err returns a non-nil value when the connection is not usable.
+	Err() error
+
+	// Do sends a command to the server and returns the received reply.
+	Do(commandName string, args ...interface{}) (reply interface{}, err error)
+
+	// Send writes the command to the client's output buffer.
+	Send(commandName string, args ...interface{}) error
+
+	// Flush flushes the output buffer to the Redis server.
+	Flush() error
+
+	// Receive receives a single reply from the Redis server
+	Receive() (reply interface{}, err error)
+}
+
+// Argument is the interface implemented by an object which wants to control how
+// the object is converted to Redis bulk strings.
+type Argument interface {
+	// RedisArg returns a value to be encoded as a bulk string per the
+	// conversions listed in the section 'Executing Commands'.
+	// Implementations should typically return a []byte or string.
+	RedisArg() interface{}
+}
+
+// Scanner is implemented by an object which wants to control its value is
+// interpreted when read from Redis.
+type Scanner interface {
+	// RedisScan assigns a value from a Redis value. The argument src is one of
+	// the reply types listed in the section `Executing Commands`.
+	//
+	// An error should be returned if the value cannot be stored without
+	// loss of information.
+	RedisScan(src interface{}) error
+}
+
+// ConnWithTimeout is an optional interface that allows the caller to override
+// a connection's default read timeout. This interface is useful for executing
+// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the
+// server.
+//
+// A connection's default read timeout is set with the DialReadTimeout dial
+// option. Applications should rely on the default timeout for commands that do
+// not block at the server.
+//
+// All of the Conn implementations in this package satisfy the ConnWithTimeout
+// interface.
+//
+// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify
+// use of this interface.
+type ConnWithTimeout interface {
+	Conn
+
+	// Do sends a command to the server and returns the received reply.
+	// The timeout overrides the read timeout set when dialing the
+	// connection.
+	DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error)
+
+	// Receive receives a single reply from the Redis server. The timeout
+	// overrides the read timeout set when dialing the connection.
+	ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
+}
+
+var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout")
+
+// DoWithTimeout executes a Redis command with the specified read timeout. If
+// the connection does not satisfy the ConnWithTimeout interface, then an error
+// is returned.
+func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
+	cwt, ok := c.(ConnWithTimeout)
+	if !ok {
+		return nil, errTimeoutNotSupported
+	}
+	return cwt.DoWithTimeout(timeout, cmd, args...)
+}
+
+// ReceiveWithTimeout receives a reply with the specified read timeout. If the
+// connection does not satisfy the ConnWithTimeout interface, then an error is
+// returned.
+func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) {
+	cwt, ok := c.(ConnWithTimeout)
+	if !ok {
+		return nil, errTimeoutNotSupported
+	}
+	return cwt.ReceiveWithTimeout(timeout)
+}

+ 71 - 0
src/github.com/gomodule/redigo/redis/redis_test.go

@@ -0,0 +1,71 @@
+// Copyright 2017 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type timeoutTestConn int
+
+func (tc timeoutTestConn) Do(string, ...interface{}) (interface{}, error) {
+	return time.Duration(-1), nil
+}
+func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
+	return timeout, nil
+}
+
+func (tc timeoutTestConn) Receive() (interface{}, error) {
+	return time.Duration(-1), nil
+}
+func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
+	return timeout, nil
+}
+
+func (tc timeoutTestConn) Send(string, ...interface{}) error { return nil }
+func (tc timeoutTestConn) Err() error                        { return nil }
+func (tc timeoutTestConn) Close() error                      { return nil }
+func (tc timeoutTestConn) Flush() error                      { return nil }
+
+func testTimeout(t *testing.T, c redis.Conn) {
+	r, err := c.Do("PING")
+	if r != time.Duration(-1) || err != nil {
+		t.Errorf("Do() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
+	}
+	r, err = redis.DoWithTimeout(c, time.Minute, "PING")
+	if r != time.Minute || err != nil {
+		t.Errorf("DoWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
+	}
+	r, err = c.Receive()
+	if r != time.Duration(-1) || err != nil {
+		t.Errorf("Receive() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
+	}
+	r, err = redis.ReceiveWithTimeout(c, time.Minute)
+	if r != time.Minute || err != nil {
+		t.Errorf("ReceiveWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
+	}
+}
+
+func TestConnTimeout(t *testing.T) {
+	testTimeout(t, timeoutTestConn(0))
+}
+
+func TestPoolConnTimeout(t *testing.T) {
+	p := &redis.Pool{Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil }}
+	testTimeout(t, p.Get())
+}

+ 479 - 0
src/github.com/gomodule/redigo/redis/reply.go

@@ -0,0 +1,479 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+)
+
+// ErrNil indicates that a reply value is nil.
+var ErrNil = errors.New("redigo: nil returned")
+
+// Int is a helper that converts a command reply to an integer. If err is not
+// equal to nil, then Int returns 0, err. Otherwise, Int converts the
+// reply to an int as follows:
+//
+//  Reply type    Result
+//  integer       int(reply), nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Int(reply interface{}, err error) (int, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		x := int(reply)
+		if int64(x) != reply {
+			return 0, strconv.ErrRange
+		}
+		return x, nil
+	case []byte:
+		n, err := strconv.ParseInt(string(reply), 10, 0)
+		return int(n), err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
+}
+
+// Int64 is a helper that converts a command reply to 64 bit integer. If err is
+// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
+// reply to an int64 as follows:
+//
+//  Reply type    Result
+//  integer       reply, nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Int64(reply interface{}, err error) (int64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		return reply, nil
+	case []byte:
+		n, err := strconv.ParseInt(string(reply), 10, 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
+}
+
+var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
+
+// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
+// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
+// reply to an int64 as follows:
+//
+//  Reply type    Result
+//  integer       reply, nil
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Uint64(reply interface{}, err error) (uint64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		if reply < 0 {
+			return 0, errNegativeInt
+		}
+		return uint64(reply), nil
+	case []byte:
+		n, err := strconv.ParseUint(string(reply), 10, 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
+}
+
+// Float64 is a helper that converts a command reply to 64 bit float. If err is
+// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
+// the reply to an int as follows:
+//
+//  Reply type    Result
+//  bulk string   parsed reply, nil
+//  nil           0, ErrNil
+//  other         0, error
+func Float64(reply interface{}, err error) (float64, error) {
+	if err != nil {
+		return 0, err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		n, err := strconv.ParseFloat(string(reply), 64)
+		return n, err
+	case nil:
+		return 0, ErrNil
+	case Error:
+		return 0, reply
+	}
+	return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
+}
+
+// String is a helper that converts a command reply to a string. If err is not
+// equal to nil, then String returns "", err. Otherwise String converts the
+// reply to a string as follows:
+//
+//  Reply type      Result
+//  bulk string     string(reply), nil
+//  simple string   reply, nil
+//  nil             "",  ErrNil
+//  other           "",  error
+func String(reply interface{}, err error) (string, error) {
+	if err != nil {
+		return "", err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		return string(reply), nil
+	case string:
+		return reply, nil
+	case nil:
+		return "", ErrNil
+	case Error:
+		return "", reply
+	}
+	return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
+}
+
+// Bytes is a helper that converts a command reply to a slice of bytes. If err
+// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
+// the reply to a slice of bytes as follows:
+//
+//  Reply type      Result
+//  bulk string     reply, nil
+//  simple string   []byte(reply), nil
+//  nil             nil, ErrNil
+//  other           nil, error
+func Bytes(reply interface{}, err error) ([]byte, error) {
+	if err != nil {
+		return nil, err
+	}
+	switch reply := reply.(type) {
+	case []byte:
+		return reply, nil
+	case string:
+		return []byte(reply), nil
+	case nil:
+		return nil, ErrNil
+	case Error:
+		return nil, reply
+	}
+	return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
+}
+
+// Bool is a helper that converts a command reply to a boolean. If err is not
+// equal to nil, then Bool returns false, err. Otherwise Bool converts the
+// reply to boolean as follows:
+//
+//  Reply type      Result
+//  integer         value != 0, nil
+//  bulk string     strconv.ParseBool(reply)
+//  nil             false, ErrNil
+//  other           false, error
+func Bool(reply interface{}, err error) (bool, error) {
+	if err != nil {
+		return false, err
+	}
+	switch reply := reply.(type) {
+	case int64:
+		return reply != 0, nil
+	case []byte:
+		return strconv.ParseBool(string(reply))
+	case nil:
+		return false, ErrNil
+	case Error:
+		return false, reply
+	}
+	return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
+}
+
+// MultiBulk is a helper that converts an array command reply to a []interface{}.
+//
+// Deprecated: Use Values instead.
+func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
+
+// Values is a helper that converts an array command reply to a []interface{}.
+// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
+// converts the reply as follows:
+//
+//  Reply type      Result
+//  array           reply, nil
+//  nil             nil, ErrNil
+//  other           nil, error
+func Values(reply interface{}, err error) ([]interface{}, error) {
+	if err != nil {
+		return nil, err
+	}
+	switch reply := reply.(type) {
+	case []interface{}:
+		return reply, nil
+	case nil:
+		return nil, ErrNil
+	case Error:
+		return nil, reply
+	}
+	return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
+}
+
+func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error {
+	if err != nil {
+		return err
+	}
+	switch reply := reply.(type) {
+	case []interface{}:
+		makeSlice(len(reply))
+		for i := range reply {
+			if reply[i] == nil {
+				continue
+			}
+			if err := assign(i, reply[i]); err != nil {
+				return err
+			}
+		}
+		return nil
+	case nil:
+		return ErrNil
+	case Error:
+		return reply
+	}
+	return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply)
+}
+
+// Float64s is a helper that converts an array command reply to a []float64. If
+// err is not equal to nil, then Float64s returns nil, err. Nil array items are
+// converted to 0 in the output slice. Floats64 returns an error if an array
+// item is not a bulk string or nil.
+func Float64s(reply interface{}, err error) ([]float64, error) {
+	var result []float64
+	err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error {
+		p, ok := v.([]byte)
+		if !ok {
+			return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v)
+		}
+		f, err := strconv.ParseFloat(string(p), 64)
+		result[i] = f
+		return err
+	})
+	return result, err
+}
+
+// Strings is a helper that converts an array command reply to a []string. If
+// err is not equal to nil, then Strings returns nil, err. Nil array items are
+// converted to "" in the output slice. Strings returns an error if an array
+// item is not a bulk string or nil.
+func Strings(reply interface{}, err error) ([]string, error) {
+	var result []string
+	err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error {
+		switch v := v.(type) {
+		case string:
+			result[i] = v
+			return nil
+		case []byte:
+			result[i] = string(v)
+			return nil
+		default:
+			return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v)
+		}
+	})
+	return result, err
+}
+
+// ByteSlices is a helper that converts an array command reply to a [][]byte.
+// If err is not equal to nil, then ByteSlices returns nil, err. Nil array
+// items are stay nil. ByteSlices returns an error if an array item is not a
+// bulk string or nil.
+func ByteSlices(reply interface{}, err error) ([][]byte, error) {
+	var result [][]byte
+	err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error {
+		p, ok := v.([]byte)
+		if !ok {
+			return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v)
+		}
+		result[i] = p
+		return nil
+	})
+	return result, err
+}
+
+// Int64s is a helper that converts an array command reply to a []int64.
+// If err is not equal to nil, then Int64s returns nil, err. Nil array
+// items are stay nil. Int64s returns an error if an array item is not a
+// bulk string or nil.
+func Int64s(reply interface{}, err error) ([]int64, error) {
+	var result []int64
+	err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error {
+		switch v := v.(type) {
+		case int64:
+			result[i] = v
+			return nil
+		case []byte:
+			n, err := strconv.ParseInt(string(v), 10, 64)
+			result[i] = n
+			return err
+		default:
+			return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v)
+		}
+	})
+	return result, err
+}
+
+// Ints is a helper that converts an array command reply to a []in.
+// If err is not equal to nil, then Ints returns nil, err. Nil array
+// items are stay nil. Ints returns an error if an array item is not a
+// bulk string or nil.
+func Ints(reply interface{}, err error) ([]int, error) {
+	var result []int
+	err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error {
+		switch v := v.(type) {
+		case int64:
+			n := int(v)
+			if int64(n) != v {
+				return strconv.ErrRange
+			}
+			result[i] = n
+			return nil
+		case []byte:
+			n, err := strconv.Atoi(string(v))
+			result[i] = n
+			return err
+		default:
+			return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v)
+		}
+	})
+	return result, err
+}
+
+// StringMap is a helper that converts an array of strings (alternating key, value)
+// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
+// Requires an even number of values in result.
+func StringMap(result interface{}, err error) (map[string]string, error) {
+	values, err := Values(result, err)
+	if err != nil {
+		return nil, err
+	}
+	if len(values)%2 != 0 {
+		return nil, errors.New("redigo: StringMap expects even number of values result")
+	}
+	m := make(map[string]string, len(values)/2)
+	for i := 0; i < len(values); i += 2 {
+		key, okKey := values[i].([]byte)
+		value, okValue := values[i+1].([]byte)
+		if !okKey || !okValue {
+			return nil, errors.New("redigo: StringMap key not a bulk string value")
+		}
+		m[string(key)] = string(value)
+	}
+	return m, nil
+}
+
+// IntMap is a helper that converts an array of strings (alternating key, value)
+// into a map[string]int. The HGETALL commands return replies in this format.
+// Requires an even number of values in result.
+func IntMap(result interface{}, err error) (map[string]int, error) {
+	values, err := Values(result, err)
+	if err != nil {
+		return nil, err
+	}
+	if len(values)%2 != 0 {
+		return nil, errors.New("redigo: IntMap expects even number of values result")
+	}
+	m := make(map[string]int, len(values)/2)
+	for i := 0; i < len(values); i += 2 {
+		key, ok := values[i].([]byte)
+		if !ok {
+			return nil, errors.New("redigo: IntMap key not a bulk string value")
+		}
+		value, err := Int(values[i+1], nil)
+		if err != nil {
+			return nil, err
+		}
+		m[string(key)] = value
+	}
+	return m, nil
+}
+
+// Int64Map is a helper that converts an array of strings (alternating key, value)
+// into a map[string]int64. The HGETALL commands return replies in this format.
+// Requires an even number of values in result.
+func Int64Map(result interface{}, err error) (map[string]int64, error) {
+	values, err := Values(result, err)
+	if err != nil {
+		return nil, err
+	}
+	if len(values)%2 != 0 {
+		return nil, errors.New("redigo: Int64Map expects even number of values result")
+	}
+	m := make(map[string]int64, len(values)/2)
+	for i := 0; i < len(values); i += 2 {
+		key, ok := values[i].([]byte)
+		if !ok {
+			return nil, errors.New("redigo: Int64Map key not a bulk string value")
+		}
+		value, err := Int64(values[i+1], nil)
+		if err != nil {
+			return nil, err
+		}
+		m[string(key)] = value
+	}
+	return m, nil
+}
+
+// Positions is a helper that converts an array of positions (lat, long)
+// into a [][2]float64. The GEOPOS command returns replies in this format.
+func Positions(result interface{}, err error) ([]*[2]float64, error) {
+	values, err := Values(result, err)
+	if err != nil {
+		return nil, err
+	}
+	positions := make([]*[2]float64, len(values))
+	for i := range values {
+		if values[i] == nil {
+			continue
+		}
+		p, ok := values[i].([]interface{})
+		if !ok {
+			return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i])
+		}
+		if len(p) != 2 {
+			return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p))
+		}
+		lat, err := Float64(p[0], nil)
+		if err != nil {
+			return nil, err
+		}
+		long, err := Float64(p[1], nil)
+		if err != nil {
+			return nil, err
+		}
+		positions[i] = &[2]float64{lat, long}
+	}
+	return positions, nil
+}

+ 209 - 0
src/github.com/gomodule/redigo/redis/reply_test.go

@@ -0,0 +1,209 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type valueError struct {
+	v   interface{}
+	err error
+}
+
+func ve(v interface{}, err error) valueError {
+	return valueError{v, err}
+}
+
+var replyTests = []struct {
+	name     interface{}
+	actual   valueError
+	expected valueError
+}{
+	{
+		"ints([[]byte, []byte])",
+		ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)),
+		ve([]int{4, 5}, nil),
+	},
+	{
+		"ints([nt64, int64])",
+		ve(redis.Ints([]interface{}{int64(4), int64(5)}, nil)),
+		ve([]int{4, 5}, nil),
+	},
+	{
+		"ints([[]byte, nil, []byte])",
+		ve(redis.Ints([]interface{}{[]byte("4"), nil, []byte("5")}, nil)),
+		ve([]int{4, 0, 5}, nil),
+	},
+	{
+		"ints(nil)",
+		ve(redis.Ints(nil, nil)),
+		ve([]int(nil), redis.ErrNil),
+	},
+	{
+		"int64s([[]byte, []byte])",
+		ve(redis.Int64s([]interface{}{[]byte("4"), []byte("5")}, nil)),
+		ve([]int64{4, 5}, nil),
+	},
+	{
+		"int64s([int64, int64])",
+		ve(redis.Int64s([]interface{}{int64(4), int64(5)}, nil)),
+		ve([]int64{4, 5}, nil),
+	},
+	{
+		"strings([[]byte, []bytev2])",
+		ve(redis.Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
+		ve([]string{"v1", "v2"}, nil),
+	},
+	{
+		"strings([string, string])",
+		ve(redis.Strings([]interface{}{"v1", "v2"}, nil)),
+		ve([]string{"v1", "v2"}, nil),
+	},
+	{
+		"byteslices([v1, v2])",
+		ve(redis.ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
+		ve([][]byte{[]byte("v1"), []byte("v2")}, nil),
+	},
+	{
+		"float64s([v1, v2])",
+		ve(redis.Float64s([]interface{}{[]byte("1.234"), []byte("5.678")}, nil)),
+		ve([]float64{1.234, 5.678}, nil),
+	},
+	{
+		"values([v1, v2])",
+		ve(redis.Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
+		ve([]interface{}{[]byte("v1"), []byte("v2")}, nil),
+	},
+	{
+		"values(nil)",
+		ve(redis.Values(nil, nil)),
+		ve([]interface{}(nil), redis.ErrNil),
+	},
+	{
+		"float64(1.0)",
+		ve(redis.Float64([]byte("1.0"), nil)),
+		ve(float64(1.0), nil),
+	},
+	{
+		"float64(nil)",
+		ve(redis.Float64(nil, nil)),
+		ve(float64(0.0), redis.ErrNil),
+	},
+	{
+		"uint64(1)",
+		ve(redis.Uint64(int64(1), nil)),
+		ve(uint64(1), nil),
+	},
+	{
+		"uint64(-1)",
+		ve(redis.Uint64(int64(-1), nil)),
+		ve(uint64(0), redis.ErrNegativeInt),
+	},
+	{
+		"positions([[1, 2], nil, [3, 4]])",
+		ve(redis.Positions([]interface{}{[]interface{}{[]byte("1"), []byte("2")}, nil, []interface{}{[]byte("3"), []byte("4")}}, nil)),
+		ve([]*[2]float64{{1.0, 2.0}, nil, {3.0, 4.0}}, nil),
+	},
+}
+
+func TestReply(t *testing.T) {
+	for _, rt := range replyTests {
+		if rt.actual.err != rt.expected.err {
+			t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err)
+			continue
+		}
+		if !reflect.DeepEqual(rt.actual.v, rt.expected.v) {
+			t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v)
+		}
+	}
+}
+
+// dial wraps DialDefaultServer() with a more suitable function name for examples.
+func dial() (redis.Conn, error) {
+	return redis.DialDefaultServer()
+}
+
+// serverAddr wraps DefaultServerAddr() with a more suitable function name for examples.
+func serverAddr() (string, error) {
+	return redis.DefaultServerAddr()
+}
+
+func ExampleBool() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Do("SET", "foo", 1)
+	exists, _ := redis.Bool(c.Do("EXISTS", "foo"))
+	fmt.Printf("%#v\n", exists)
+	// Output:
+	// true
+}
+
+func ExampleInt() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Do("SET", "k1", 1)
+	n, _ := redis.Int(c.Do("GET", "k1"))
+	fmt.Printf("%#v\n", n)
+	n, _ = redis.Int(c.Do("INCR", "k1"))
+	fmt.Printf("%#v\n", n)
+	// Output:
+	// 1
+	// 2
+}
+
+func ExampleInts() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Do("SADD", "set_with_integers", 4, 5, 6)
+	ints, _ := redis.Ints(c.Do("SMEMBERS", "set_with_integers"))
+	fmt.Printf("%#v\n", ints)
+	// Output:
+	// []int{4, 5, 6}
+}
+
+func ExampleString() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Do("SET", "hello", "world")
+	s, err := redis.String(c.Do("GET", "hello"))
+	fmt.Printf("%#v\n", s)
+	// Output:
+	// "world"
+}

+ 630 - 0
src/github.com/gomodule/redigo/redis/scan.go

@@ -0,0 +1,630 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+)
+
+func ensureLen(d reflect.Value, n int) {
+	if n > d.Cap() {
+		d.Set(reflect.MakeSlice(d.Type(), n, n))
+	} else {
+		d.SetLen(n)
+	}
+}
+
+func cannotConvert(d reflect.Value, s interface{}) error {
+	var sname string
+	switch s.(type) {
+	case string:
+		sname = "Redis simple string"
+	case Error:
+		sname = "Redis error"
+	case int64:
+		sname = "Redis integer"
+	case []byte:
+		sname = "Redis bulk string"
+	case []interface{}:
+		sname = "Redis array"
+	case nil:
+		sname = "Redis nil"
+	default:
+		sname = reflect.TypeOf(s).String()
+	}
+	return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
+}
+
+func convertAssignNil(d reflect.Value) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Slice, reflect.Interface:
+		d.Set(reflect.Zero(d.Type()))
+	default:
+		err = cannotConvert(d, nil)
+	}
+	return err
+}
+
+func convertAssignError(d reflect.Value, s Error) (err error) {
+	if d.Kind() == reflect.String {
+		d.SetString(string(s))
+	} else if d.Kind() == reflect.Slice && d.Type().Elem().Kind() == reflect.Uint8 {
+		d.SetBytes([]byte(s))
+	} else {
+		err = cannotConvert(d, s)
+	}
+	return
+}
+
+func convertAssignString(d reflect.Value, s string) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Float32, reflect.Float64:
+		var x float64
+		x, err = strconv.ParseFloat(s, d.Type().Bits())
+		d.SetFloat(x)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		var x int64
+		x, err = strconv.ParseInt(s, 10, d.Type().Bits())
+		d.SetInt(x)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		var x uint64
+		x, err = strconv.ParseUint(s, 10, d.Type().Bits())
+		d.SetUint(x)
+	case reflect.Bool:
+		var x bool
+		x, err = strconv.ParseBool(s)
+		d.SetBool(x)
+	case reflect.String:
+		d.SetString(s)
+	case reflect.Slice:
+		if d.Type().Elem().Kind() == reflect.Uint8 {
+			d.SetBytes([]byte(s))
+		} else {
+			err = cannotConvert(d, s)
+		}
+	default:
+		err = cannotConvert(d, s)
+	}
+	return
+}
+
+func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Slice:
+		// Handle []byte destination here to avoid unnecessary
+		// []byte -> string -> []byte converion.
+		if d.Type().Elem().Kind() == reflect.Uint8 {
+			d.SetBytes(s)
+		} else {
+			err = cannotConvert(d, s)
+		}
+	default:
+		err = convertAssignString(d, string(s))
+	}
+	return err
+}
+
+func convertAssignInt(d reflect.Value, s int64) (err error) {
+	switch d.Type().Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		d.SetInt(s)
+		if d.Int() != s {
+			err = strconv.ErrRange
+			d.SetInt(0)
+		}
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		if s < 0 {
+			err = strconv.ErrRange
+		} else {
+			x := uint64(s)
+			d.SetUint(x)
+			if d.Uint() != x {
+				err = strconv.ErrRange
+				d.SetUint(0)
+			}
+		}
+	case reflect.Bool:
+		d.SetBool(s != 0)
+	default:
+		err = cannotConvert(d, s)
+	}
+	return
+}
+
+func convertAssignValue(d reflect.Value, s interface{}) (err error) {
+	if d.Kind() != reflect.Ptr {
+		if d.CanAddr() {
+			d2 := d.Addr()
+			if d2.CanInterface() {
+				if scanner, ok := d2.Interface().(Scanner); ok {
+					return scanner.RedisScan(s)
+				}
+			}
+		}
+	} else if d.CanInterface() {
+		// Already a reflect.Ptr
+		if d.IsNil() {
+			d.Set(reflect.New(d.Type().Elem()))
+		}
+		if scanner, ok := d.Interface().(Scanner); ok {
+			return scanner.RedisScan(s)
+		}
+	}
+
+	switch s := s.(type) {
+	case nil:
+		err = convertAssignNil(d)
+	case []byte:
+		err = convertAssignBulkString(d, s)
+	case int64:
+		err = convertAssignInt(d, s)
+	case string:
+		err = convertAssignString(d, s)
+	case Error:
+		err = convertAssignError(d, s)
+	default:
+		err = cannotConvert(d, s)
+	}
+	return err
+}
+
+func convertAssignArray(d reflect.Value, s []interface{}) error {
+	if d.Type().Kind() != reflect.Slice {
+		return cannotConvert(d, s)
+	}
+	ensureLen(d, len(s))
+	for i := 0; i < len(s); i++ {
+		if err := convertAssignValue(d.Index(i), s[i]); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func convertAssign(d interface{}, s interface{}) (err error) {
+	if scanner, ok := d.(Scanner); ok {
+		return scanner.RedisScan(s)
+	}
+
+	// Handle the most common destination types using type switches and
+	// fall back to reflection for all other types.
+	switch s := s.(type) {
+	case nil:
+		// ignore
+	case []byte:
+		switch d := d.(type) {
+		case *string:
+			*d = string(s)
+		case *int:
+			*d, err = strconv.Atoi(string(s))
+		case *bool:
+			*d, err = strconv.ParseBool(string(s))
+		case *[]byte:
+			*d = s
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignBulkString(d.Elem(), s)
+			}
+		}
+	case int64:
+		switch d := d.(type) {
+		case *int:
+			x := int(s)
+			if int64(x) != s {
+				err = strconv.ErrRange
+				x = 0
+			}
+			*d = x
+		case *bool:
+			*d = s != 0
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignInt(d.Elem(), s)
+			}
+		}
+	case string:
+		switch d := d.(type) {
+		case *string:
+			*d = s
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			err = cannotConvert(reflect.ValueOf(d), s)
+		}
+	case []interface{}:
+		switch d := d.(type) {
+		case *[]interface{}:
+			*d = s
+		case *interface{}:
+			*d = s
+		case nil:
+			// skip value
+		default:
+			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
+				err = cannotConvert(d, s)
+			} else {
+				err = convertAssignArray(d.Elem(), s)
+			}
+		}
+	case Error:
+		err = s
+	default:
+		err = cannotConvert(reflect.ValueOf(d), s)
+	}
+	return
+}
+
+// Scan copies from src to the values pointed at by dest.
+//
+// Scan uses RedisScan if available otherwise:
+//
+// The values pointed at by dest must be an integer, float, boolean, string,
+// []byte, interface{} or slices of these types. Scan uses the standard strconv
+// package to convert bulk strings to numeric and boolean types.
+//
+// If a dest value is nil, then the corresponding src value is skipped.
+//
+// If a src element is nil, then the corresponding dest value is not modified.
+//
+// To enable easy use of Scan in a loop, Scan returns the slice of src
+// following the copied values.
+func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
+	if len(src) < len(dest) {
+		return nil, errors.New("redigo.Scan: array short")
+	}
+	var err error
+	for i, d := range dest {
+		err = convertAssign(d, src[i])
+		if err != nil {
+			err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
+			break
+		}
+	}
+	return src[len(dest):], err
+}
+
+type fieldSpec struct {
+	name      string
+	index     []int
+	omitEmpty bool
+}
+
+type structSpec struct {
+	m map[string]*fieldSpec
+	l []*fieldSpec
+}
+
+func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
+	return ss.m[string(name)]
+}
+
+func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
+		switch {
+		case f.PkgPath != "" && !f.Anonymous:
+			// Ignore unexported fields.
+		case f.Anonymous:
+			// TODO: Handle pointers. Requires change to decoder and
+			// protection against infinite recursion.
+			if f.Type.Kind() == reflect.Struct {
+				compileStructSpec(f.Type, depth, append(index, i), ss)
+			}
+		default:
+			fs := &fieldSpec{name: f.Name}
+			tag := f.Tag.Get("redis")
+			p := strings.Split(tag, ",")
+			if len(p) > 0 {
+				if p[0] == "-" {
+					continue
+				}
+				if len(p[0]) > 0 {
+					fs.name = p[0]
+				}
+				for _, s := range p[1:] {
+					switch s {
+					case "omitempty":
+						fs.omitEmpty = true
+					default:
+						panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name()))
+					}
+				}
+			}
+			d, found := depth[fs.name]
+			if !found {
+				d = 1 << 30
+			}
+			switch {
+			case len(index) == d:
+				// At same depth, remove from result.
+				delete(ss.m, fs.name)
+				j := 0
+				for i := 0; i < len(ss.l); i++ {
+					if fs.name != ss.l[i].name {
+						ss.l[j] = ss.l[i]
+						j += 1
+					}
+				}
+				ss.l = ss.l[:j]
+			case len(index) < d:
+				fs.index = make([]int, len(index)+1)
+				copy(fs.index, index)
+				fs.index[len(index)] = i
+				depth[fs.name] = len(index)
+				ss.m[fs.name] = fs
+				ss.l = append(ss.l, fs)
+			}
+		}
+	}
+}
+
+var (
+	structSpecMutex  sync.RWMutex
+	structSpecCache  = make(map[reflect.Type]*structSpec)
+	defaultFieldSpec = &fieldSpec{}
+)
+
+func structSpecForType(t reflect.Type) *structSpec {
+
+	structSpecMutex.RLock()
+	ss, found := structSpecCache[t]
+	structSpecMutex.RUnlock()
+	if found {
+		return ss
+	}
+
+	structSpecMutex.Lock()
+	defer structSpecMutex.Unlock()
+	ss, found = structSpecCache[t]
+	if found {
+		return ss
+	}
+
+	ss = &structSpec{m: make(map[string]*fieldSpec)}
+	compileStructSpec(t, make(map[string]int), nil, ss)
+	structSpecCache[t] = ss
+	return ss
+}
+
+var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
+
+// ScanStruct scans alternating names and values from src to a struct. The
+// HGETALL and CONFIG GET commands return replies in this format.
+//
+// ScanStruct uses exported field names to match values in the response. Use
+// 'redis' field tag to override the name:
+//
+//      Field int `redis:"myName"`
+//
+// Fields with the tag redis:"-" are ignored.
+//
+// Each field uses RedisScan if available otherwise:
+// Integer, float, boolean, string and []byte fields are supported. Scan uses the
+// standard strconv package to convert bulk string values to numeric and
+// boolean types.
+//
+// If a src element is nil, then the corresponding field is not modified.
+func ScanStruct(src []interface{}, dest interface{}) error {
+	d := reflect.ValueOf(dest)
+	if d.Kind() != reflect.Ptr || d.IsNil() {
+		return errScanStructValue
+	}
+	d = d.Elem()
+	if d.Kind() != reflect.Struct {
+		return errScanStructValue
+	}
+	ss := structSpecForType(d.Type())
+
+	if len(src)%2 != 0 {
+		return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
+	}
+
+	for i := 0; i < len(src); i += 2 {
+		s := src[i+1]
+		if s == nil {
+			continue
+		}
+		name, ok := src[i].([]byte)
+		if !ok {
+			return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i)
+		}
+		fs := ss.fieldSpec(name)
+		if fs == nil {
+			continue
+		}
+		if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
+			return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
+		}
+	}
+	return nil
+}
+
+var (
+	errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
+)
+
+// ScanSlice scans src to the slice pointed to by dest. The elements the dest
+// slice must be integer, float, boolean, string, struct or pointer to struct
+// values.
+//
+// Struct fields must be integer, float, boolean or string values. All struct
+// fields are used unless a subset is specified using fieldNames.
+func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
+	d := reflect.ValueOf(dest)
+	if d.Kind() != reflect.Ptr || d.IsNil() {
+		return errScanSliceValue
+	}
+	d = d.Elem()
+	if d.Kind() != reflect.Slice {
+		return errScanSliceValue
+	}
+
+	isPtr := false
+	t := d.Type().Elem()
+	if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
+		isPtr = true
+		t = t.Elem()
+	}
+
+	if t.Kind() != reflect.Struct {
+		ensureLen(d, len(src))
+		for i, s := range src {
+			if s == nil {
+				continue
+			}
+			if err := convertAssignValue(d.Index(i), s); err != nil {
+				return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
+			}
+		}
+		return nil
+	}
+
+	ss := structSpecForType(t)
+	fss := ss.l
+	if len(fieldNames) > 0 {
+		fss = make([]*fieldSpec, len(fieldNames))
+		for i, name := range fieldNames {
+			fss[i] = ss.m[name]
+			if fss[i] == nil {
+				return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
+			}
+		}
+	}
+
+	if len(fss) == 0 {
+		return errors.New("redigo.ScanSlice: no struct fields")
+	}
+
+	n := len(src) / len(fss)
+	if n*len(fss) != len(src) {
+		return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
+	}
+
+	ensureLen(d, n)
+	for i := 0; i < n; i++ {
+		d := d.Index(i)
+		if isPtr {
+			if d.IsNil() {
+				d.Set(reflect.New(t))
+			}
+			d = d.Elem()
+		}
+		for j, fs := range fss {
+			s := src[i*len(fss)+j]
+			if s == nil {
+				continue
+			}
+			if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
+				return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
+			}
+		}
+	}
+	return nil
+}
+
+// Args is a helper for constructing command arguments from structured values.
+type Args []interface{}
+
+// Add returns the result of appending value to args.
+func (args Args) Add(value ...interface{}) Args {
+	return append(args, value...)
+}
+
+// AddFlat returns the result of appending the flattened value of v to args.
+//
+// Maps are flattened by appending the alternating keys and map values to args.
+//
+// Slices are flattened by appending the slice elements to args.
+//
+// Structs are flattened by appending the alternating names and values of
+// exported fields to args. If v is a nil struct pointer, then nothing is
+// appended. The 'redis' field tag overrides struct field names. See ScanStruct
+// for more information on the use of the 'redis' field tag.
+//
+// Other types are appended to args as is.
+func (args Args) AddFlat(v interface{}) Args {
+	rv := reflect.ValueOf(v)
+	switch rv.Kind() {
+	case reflect.Struct:
+		args = flattenStruct(args, rv)
+	case reflect.Slice:
+		for i := 0; i < rv.Len(); i++ {
+			args = append(args, rv.Index(i).Interface())
+		}
+	case reflect.Map:
+		for _, k := range rv.MapKeys() {
+			args = append(args, k.Interface(), rv.MapIndex(k).Interface())
+		}
+	case reflect.Ptr:
+		if rv.Type().Elem().Kind() == reflect.Struct {
+			if !rv.IsNil() {
+				args = flattenStruct(args, rv.Elem())
+			}
+		} else {
+			args = append(args, v)
+		}
+	default:
+		args = append(args, v)
+	}
+	return args
+}
+
+func flattenStruct(args Args, v reflect.Value) Args {
+	ss := structSpecForType(v.Type())
+	for _, fs := range ss.l {
+		fv := v.FieldByIndex(fs.index)
+		if fs.omitEmpty {
+			var empty = false
+			switch fv.Kind() {
+			case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+				empty = fv.Len() == 0
+			case reflect.Bool:
+				empty = !fv.Bool()
+			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+				empty = fv.Int() == 0
+			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+				empty = fv.Uint() == 0
+			case reflect.Float32, reflect.Float64:
+				empty = fv.Float() == 0
+			case reflect.Interface, reflect.Ptr:
+				empty = fv.IsNil()
+			}
+			if empty {
+				continue
+			}
+		}
+		args = append(args, fs.name, fv.Interface())
+	}
+	return args
+}

+ 513 - 0
src/github.com/gomodule/redigo/redis/scan_test.go

@@ -0,0 +1,513 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"fmt"
+	"math"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type durationScan struct {
+	time.Duration `redis:"sd"`
+}
+
+func (t *durationScan) RedisScan(src interface{}) (err error) {
+	if t == nil {
+		return fmt.Errorf("nil pointer")
+	}
+	switch src := src.(type) {
+	case string:
+		t.Duration, err = time.ParseDuration(src)
+	case []byte:
+		t.Duration, err = time.ParseDuration(string(src))
+	case int64:
+		t.Duration = time.Duration(src)
+	default:
+		err = fmt.Errorf("cannot convert from %T to %T", src, t)
+	}
+	return err
+}
+
+var scanConversionTests = []struct {
+	src  interface{}
+	dest interface{}
+}{
+	{[]byte("-inf"), math.Inf(-1)},
+	{[]byte("+inf"), math.Inf(1)},
+	{[]byte("0"), float64(0)},
+	{[]byte("3.14159"), float64(3.14159)},
+	{[]byte("3.14"), float32(3.14)},
+	{[]byte("-100"), int(-100)},
+	{[]byte("101"), int(101)},
+	{int64(102), int(102)},
+	{[]byte("103"), uint(103)},
+	{int64(104), uint(104)},
+	{[]byte("105"), int8(105)},
+	{int64(106), int8(106)},
+	{[]byte("107"), uint8(107)},
+	{int64(108), uint8(108)},
+	{[]byte("0"), false},
+	{int64(0), false},
+	{[]byte("f"), false},
+	{[]byte("1"), true},
+	{int64(1), true},
+	{[]byte("t"), true},
+	{"hello", "hello"},
+	{[]byte("hello"), "hello"},
+	{[]byte("world"), []byte("world")},
+	{nil, ""},
+	{nil, []byte(nil)},
+
+	{[]interface{}{[]byte("b1")}, []interface{}{[]byte("b1")}},
+	{[]interface{}{[]byte("b2")}, []string{"b2"}},
+	{[]interface{}{[]byte("b3"), []byte("b4")}, []string{"b3", "b4"}},
+	{[]interface{}{[]byte("b5")}, [][]byte{[]byte("b5")}},
+	{[]interface{}{[]byte("1")}, []int{1}},
+	{[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}},
+	{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
+	{[]interface{}{[]byte("1")}, []byte{1}},
+	{[]interface{}{[]byte("1")}, []bool{true}},
+
+	{[]interface{}{"s1"}, []interface{}{"s1"}},
+	{[]interface{}{"s2"}, [][]byte{[]byte("s2")}},
+	{[]interface{}{"s3", "s4"}, []string{"s3", "s4"}},
+	{[]interface{}{"s5"}, [][]byte{[]byte("s5")}},
+	{[]interface{}{"1"}, []int{1}},
+	{[]interface{}{"1", "2"}, []int{1, 2}},
+	{[]interface{}{"1", "2"}, []float64{1, 2}},
+	{[]interface{}{"1"}, []byte{1}},
+	{[]interface{}{"1"}, []bool{true}},
+
+	{[]interface{}{nil, "2"}, []interface{}{nil, "2"}},
+	{[]interface{}{nil, []byte("2")}, [][]byte{nil, []byte("2")}},
+
+	{[]interface{}{redis.Error("e1")}, []interface{}{redis.Error("e1")}},
+	{[]interface{}{redis.Error("e2")}, [][]byte{[]byte("e2")}},
+	{[]interface{}{redis.Error("e3")}, []string{"e3"}},
+
+	{"1m", durationScan{Duration: time.Minute}},
+	{[]byte("1m"), durationScan{Duration: time.Minute}},
+	{time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}},
+	{[]interface{}{[]byte("1m")}, []durationScan{{Duration: time.Minute}}},
+	{[]interface{}{[]byte("1m")}, []*durationScan{{Duration: time.Minute}}},
+}
+
+func TestScanConversion(t *testing.T) {
+	for _, tt := range scanConversionTests {
+		values := []interface{}{tt.src}
+		dest := reflect.New(reflect.TypeOf(tt.dest))
+		values, err := redis.Scan(values, dest.Interface())
+		if err != nil {
+			t.Errorf("Scan(%v) returned error %v", tt, err)
+			continue
+		}
+		if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) {
+			t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest)
+		}
+	}
+}
+
+var scanConversionErrorTests = []struct {
+	src  interface{}
+	dest interface{}
+}{
+	{[]byte("1234"), byte(0)},
+	{int64(1234), byte(0)},
+	{[]byte("-1"), byte(0)},
+	{int64(-1), byte(0)},
+	{[]byte("junk"), false},
+	{redis.Error("blah"), false},
+	{redis.Error("blah"), durationScan{Duration: time.Minute}},
+	{"invalid", durationScan{Duration: time.Minute}},
+}
+
+func TestScanConversionError(t *testing.T) {
+	for _, tt := range scanConversionErrorTests {
+		values := []interface{}{tt.src}
+		dest := reflect.New(reflect.TypeOf(tt.dest))
+		values, err := redis.Scan(values, dest.Interface())
+		if err == nil {
+			t.Errorf("Scan(%v) did not return error", tt)
+		}
+	}
+}
+
+func ExampleScan() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
+	c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
+	c.Send("HMSET", "album:3", "title", "Beat")
+	c.Send("LPUSH", "albums", "1")
+	c.Send("LPUSH", "albums", "2")
+	c.Send("LPUSH", "albums", "3")
+	values, err := redis.Values(c.Do("SORT", "albums",
+		"BY", "album:*->rating",
+		"GET", "album:*->title",
+		"GET", "album:*->rating"))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	for len(values) > 0 {
+		var title string
+		rating := -1 // initialize to illegal value to detect nil.
+		values, err = redis.Scan(values, &title, &rating)
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+		if rating == -1 {
+			fmt.Println(title, "not-rated")
+		} else {
+			fmt.Println(title, rating)
+		}
+	}
+	// Output:
+	// Beat not-rated
+	// Earthbound 1
+	// Red 5
+}
+
+type s0 struct {
+	X  int
+	Y  int `redis:"y"`
+	Bt bool
+}
+
+type s1 struct {
+	X  int    `redis:"-"`
+	I  int    `redis:"i"`
+	U  uint   `redis:"u"`
+	S  string `redis:"s"`
+	P  []byte `redis:"p"`
+	B  bool   `redis:"b"`
+	Bt bool
+	Bf bool
+	s0
+	Sd  durationScan  `redis:"sd"`
+	Sdp *durationScan `redis:"sdp"`
+}
+
+var scanStructTests = []struct {
+	title string
+	reply []string
+	value interface{}
+}{
+	{"basic",
+		[]string{
+			"i", "-1234",
+			"u", "5678",
+			"s", "hello",
+			"p", "world",
+			"b", "t",
+			"Bt", "1",
+			"Bf", "0",
+			"X", "123",
+			"y", "456",
+			"sd", "1m",
+			"sdp", "1m",
+		},
+		&s1{
+			I:   -1234,
+			U:   5678,
+			S:   "hello",
+			P:   []byte("world"),
+			B:   true,
+			Bt:  true,
+			Bf:  false,
+			s0:  s0{X: 123, Y: 456},
+			Sd:  durationScan{Duration: time.Minute},
+			Sdp: &durationScan{Duration: time.Minute},
+		},
+	},
+	{"absent values",
+		[]string{},
+		&s1{},
+	},
+}
+
+func TestScanStruct(t *testing.T) {
+	for _, tt := range scanStructTests {
+
+		var reply []interface{}
+		for _, v := range tt.reply {
+			reply = append(reply, []byte(v))
+		}
+
+		value := reflect.New(reflect.ValueOf(tt.value).Type().Elem())
+
+		if err := redis.ScanStruct(reply, value.Interface()); err != nil {
+			t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err)
+		}
+
+		if !reflect.DeepEqual(value.Interface(), tt.value) {
+			t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value)
+		}
+	}
+}
+
+func TestBadScanStructArgs(t *testing.T) {
+	x := []interface{}{"A", "b"}
+	test := func(v interface{}) {
+		if err := redis.ScanStruct(x, v); err == nil {
+			t.Errorf("Expect error for ScanStruct(%T, %T)", x, v)
+		}
+	}
+
+	test(nil)
+
+	var v0 *struct{}
+	test(v0)
+
+	var v1 int
+	test(&v1)
+
+	x = x[:1]
+	v2 := struct{ A string }{}
+	test(&v2)
+}
+
+var scanSliceTests = []struct {
+	src        []interface{}
+	fieldNames []string
+	ok         bool
+	dest       interface{}
+}{
+	{
+		[]interface{}{[]byte("1"), nil, []byte("-1")},
+		nil,
+		true,
+		[]int{1, 0, -1},
+	},
+	{
+		[]interface{}{[]byte("1"), nil, []byte("2")},
+		nil,
+		true,
+		[]uint{1, 0, 2},
+	},
+	{
+		[]interface{}{[]byte("-1")},
+		nil,
+		false,
+		[]uint{1},
+	},
+	{
+		[]interface{}{[]byte("hello"), nil, []byte("world")},
+		nil,
+		true,
+		[][]byte{[]byte("hello"), nil, []byte("world")},
+	},
+	{
+		[]interface{}{[]byte("hello"), nil, []byte("world")},
+		nil,
+		true,
+		[]string{"hello", "", "world"},
+	},
+	{
+		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
+		nil,
+		true,
+		[]struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
+	},
+	{
+		[]interface{}{[]byte("a1"), []byte("b1")},
+		nil,
+		false,
+		[]struct{ A, B, C string }{{"a1", "b1", ""}},
+	},
+	{
+		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
+		nil,
+		true,
+		[]*struct{ A, B string }{{A: "a1", B: "b1"}, {A: "a2", B: "b2"}},
+	},
+	{
+		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
+		[]string{"A", "B"},
+		true,
+		[]struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}},
+	},
+	{
+		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
+		nil,
+		false,
+		[]struct{}{},
+	},
+}
+
+func TestScanSlice(t *testing.T) {
+	for _, tt := range scanSliceTests {
+
+		typ := reflect.ValueOf(tt.dest).Type()
+		dest := reflect.New(typ)
+
+		err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...)
+		if tt.ok != (err == nil) {
+			t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err)
+			continue
+		}
+		if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) {
+			t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest)
+		}
+	}
+}
+
+func ExampleScanSlice() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
+	c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
+	c.Send("HMSET", "album:3", "title", "Beat", "rating", 4)
+	c.Send("LPUSH", "albums", "1")
+	c.Send("LPUSH", "albums", "2")
+	c.Send("LPUSH", "albums", "3")
+	values, err := redis.Values(c.Do("SORT", "albums",
+		"BY", "album:*->rating",
+		"GET", "album:*->title",
+		"GET", "album:*->rating"))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	var albums []struct {
+		Title  string
+		Rating int
+	}
+	if err := redis.ScanSlice(values, &albums); err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Printf("%v\n", albums)
+	// Output:
+	// [{Earthbound 1} {Beat 4} {Red 5}]
+}
+
+var argsTests = []struct {
+	title    string
+	actual   redis.Args
+	expected redis.Args
+}{
+	{"struct ptr",
+		redis.Args{}.AddFlat(&struct {
+			I  int               `redis:"i"`
+			U  uint              `redis:"u"`
+			S  string            `redis:"s"`
+			P  []byte            `redis:"p"`
+			M  map[string]string `redis:"m"`
+			Bt bool
+			Bf bool
+		}{
+			-1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false,
+		}),
+		redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false},
+	},
+	{"struct",
+		redis.Args{}.AddFlat(struct{ I int }{123}),
+		redis.Args{"I", 123},
+	},
+	{"slice",
+		redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2),
+		redis.Args{1, "a", "b", "c", 2},
+	},
+	{"struct omitempty",
+		redis.Args{}.AddFlat(&struct {
+			Sdp *durationArg `redis:"Sdp,omitempty"`
+		}{
+			nil,
+		}),
+		redis.Args{},
+	},
+}
+
+func TestArgs(t *testing.T) {
+	for _, tt := range argsTests {
+		if !reflect.DeepEqual(tt.actual, tt.expected) {
+			t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected)
+		}
+	}
+}
+
+func ExampleArgs() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	var p1, p2 struct {
+		Title  string `redis:"title"`
+		Author string `redis:"author"`
+		Body   string `redis:"body"`
+	}
+
+	p1.Title = "Example"
+	p1.Author = "Gary"
+	p1.Body = "Hello"
+
+	if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	m := map[string]string{
+		"title":  "Example2",
+		"author": "Steve",
+		"body":   "Map",
+	}
+
+	if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	for _, id := range []string{"id1", "id2"} {
+
+		v, err := redis.Values(c.Do("HGETALL", id))
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+
+		if err := redis.ScanStruct(v, &p2); err != nil {
+			fmt.Println(err)
+			return
+		}
+
+		fmt.Printf("%+v\n", p2)
+	}
+
+	// Output:
+	// {Title:Example Author:Gary Body:Hello}
+	// {Title:Example2 Author:Steve Body:Map}
+}

+ 91 - 0
src/github.com/gomodule/redigo/redis/script.go

@@ -0,0 +1,91 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+	"io"
+	"strings"
+)
+
+// Script encapsulates the source, hash and key count for a Lua script. See
+// http://redis.io/commands/eval for information on scripts in Redis.
+type Script struct {
+	keyCount int
+	src      string
+	hash     string
+}
+
+// NewScript returns a new script object. If keyCount is greater than or equal
+// to zero, then the count is automatically inserted in the EVAL command
+// argument list. If keyCount is less than zero, then the application supplies
+// the count as the first value in the keysAndArgs argument to the Do, Send and
+// SendHash methods.
+func NewScript(keyCount int, src string) *Script {
+	h := sha1.New()
+	io.WriteString(h, src)
+	return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
+}
+
+func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
+	var args []interface{}
+	if s.keyCount < 0 {
+		args = make([]interface{}, 1+len(keysAndArgs))
+		args[0] = spec
+		copy(args[1:], keysAndArgs)
+	} else {
+		args = make([]interface{}, 2+len(keysAndArgs))
+		args[0] = spec
+		args[1] = s.keyCount
+		copy(args[2:], keysAndArgs)
+	}
+	return args
+}
+
+// Hash returns the script hash.
+func (s *Script) Hash() string {
+	return s.hash
+}
+
+// Do evaluates the script. Under the covers, Do optimistically evaluates the
+// script using the EVALSHA command. If the command fails because the script is
+// not loaded, then Do evaluates the script using the EVAL command (thus
+// causing the script to load).
+func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
+	v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
+	if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
+		v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
+	}
+	return v, err
+}
+
+// SendHash evaluates the script without waiting for the reply. The script is
+// evaluated with the EVALSHA command. The application must ensure that the
+// script is loaded by a previous call to Send, Do or Load methods.
+func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
+	return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
+}
+
+// Send evaluates the script without waiting for the reply.
+func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
+	return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
+}
+
+// Load loads the script without evaluating it.
+func (s *Script) Load(c Conn) error {
+	_, err := c.Do("SCRIPT", "LOAD", s.src)
+	return err
+}

+ 100 - 0
src/github.com/gomodule/redigo/redis/script_test.go

@@ -0,0 +1,100 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+var (
+	// These variables are declared at package level to remove distracting
+	// details from the examples.
+	c     redis.Conn
+	reply interface{}
+	err   error
+)
+
+func ExampleScript() {
+	// Initialize a package-level variable with a script.
+	var getScript = redis.NewScript(1, `return redis.call('get', KEYS[1])`)
+
+	// In a function, use the script Do method to evaluate the script. The Do
+	// method optimistically uses the EVALSHA command. If the script is not
+	// loaded, then the Do method falls back to the EVAL command.
+	reply, err = getScript.Do(c, "foo")
+}
+
+func TestScript(t *testing.T) {
+	c, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	// To test fall back in Do, we make script unique by adding comment with current time.
+	script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano())
+	s := redis.NewScript(2, script)
+	reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")}
+
+	v, err := s.Do(c, "key1", "key2", "arg1", "arg2")
+	if err != nil {
+		t.Errorf("s.Do(c, ...) returned %v", err)
+	}
+
+	if !reflect.DeepEqual(v, reply) {
+		t.Errorf("s.Do(c, ..); = %v, want %v", v, reply)
+	}
+
+	err = s.Load(c)
+	if err != nil {
+		t.Errorf("s.Load(c) returned %v", err)
+	}
+
+	err = s.SendHash(c, "key1", "key2", "arg1", "arg2")
+	if err != nil {
+		t.Errorf("s.SendHash(c, ...) returned %v", err)
+	}
+
+	err = c.Flush()
+	if err != nil {
+		t.Errorf("c.Flush() returned %v", err)
+	}
+
+	v, err = c.Receive()
+	if !reflect.DeepEqual(v, reply) {
+		t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply)
+	}
+
+	err = s.Send(c, "key1", "key2", "arg1", "arg2")
+	if err != nil {
+		t.Errorf("s.Send(c, ...) returned %v", err)
+	}
+
+	err = c.Flush()
+	if err != nil {
+		t.Errorf("c.Flush() returned %v", err)
+	}
+
+	v, err = c.Receive()
+	if !reflect.DeepEqual(v, reply) {
+		t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply)
+	}
+
+}

+ 183 - 0
src/github.com/gomodule/redigo/redis/test_test.go

@@ -0,0 +1,183 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis
+
+import (
+	"bufio"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+)
+
+func SetNowFunc(f func() time.Time) {
+	nowFunc = f
+}
+
+var (
+	ErrNegativeInt = errNegativeInt
+
+	serverPath     = flag.String("redis-server", "redis-server", "Path to redis server binary")
+	serverAddress  = flag.String("redis-address", "127.0.0.1", "The address of the server")
+	serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers")
+	serverLogName  = flag.String("redis-log", "", "Write Redis server logs to `filename`")
+	serverLog      = ioutil.Discard
+
+	defaultServerMu  sync.Mutex
+	defaultServer    *Server
+	defaultServerErr error
+)
+
+type Server struct {
+	name string
+	cmd  *exec.Cmd
+	done chan struct{}
+}
+
+func NewServer(name string, args ...string) (*Server, error) {
+	s := &Server{
+		name: name,
+		cmd:  exec.Command(*serverPath, args...),
+		done: make(chan struct{}),
+	}
+
+	r, err := s.cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+
+	err = s.cmd.Start()
+	if err != nil {
+		return nil, err
+	}
+
+	ready := make(chan error, 1)
+	go s.watch(r, ready)
+
+	select {
+	case err = <-ready:
+	case <-time.After(time.Second * 10):
+		err = errors.New("timeout waiting for server to start")
+	}
+
+	if err != nil {
+		s.Stop()
+		return nil, err
+	}
+
+	return s, nil
+}
+
+func (s *Server) watch(r io.Reader, ready chan error) {
+	fmt.Fprintf(serverLog, "%d START %s \n", s.cmd.Process.Pid, s.name)
+	var listening bool
+	var text string
+	scn := bufio.NewScanner(r)
+	for scn.Scan() {
+		text = scn.Text()
+		fmt.Fprintf(serverLog, "%s\n", text)
+		if !listening {
+			if strings.Contains(text, " * Ready to accept connections") ||
+				strings.Contains(text, " * The server is now ready to accept connections on port") {
+				listening = true
+				ready <- nil
+			}
+		}
+	}
+	if !listening {
+		ready <- fmt.Errorf("server exited: %s", text)
+	}
+	s.cmd.Wait()
+	fmt.Fprintf(serverLog, "%d STOP %s \n", s.cmd.Process.Pid, s.name)
+	close(s.done)
+}
+
+func (s *Server) Stop() {
+	s.cmd.Process.Signal(os.Interrupt)
+	<-s.done
+}
+
+// stopDefaultServer stops the server created by DialDefaultServer.
+func stopDefaultServer() {
+	defaultServerMu.Lock()
+	defer defaultServerMu.Unlock()
+	if defaultServer != nil {
+		defaultServer.Stop()
+		defaultServer = nil
+	}
+}
+
+// DefaultServerAddr starts the test server if not already started and returns
+// the address of that server.
+func DefaultServerAddr() (string, error) {
+	defaultServerMu.Lock()
+	defer defaultServerMu.Unlock()
+	addr := fmt.Sprintf("%v:%d", *serverAddress, *serverBasePort)
+	if defaultServer != nil || defaultServerErr != nil {
+		return addr, defaultServerErr
+	}
+	defaultServer, defaultServerErr = NewServer(
+		"default",
+		"--port", strconv.Itoa(*serverBasePort),
+		"--bind", *serverAddress,
+		"--save", "",
+		"--appendonly", "no")
+	return addr, defaultServerErr
+}
+
+// DialDefaultServer starts the test server if not already started and dials a
+// connection to the server.
+func DialDefaultServer(options ...DialOption) (Conn, error) {
+	addr, err := DefaultServerAddr()
+	if err != nil {
+		return nil, err
+	}
+	c, err := Dial("tcp", addr, append([]DialOption{DialReadTimeout(1 * time.Second), DialWriteTimeout(1 * time.Second)}, options...)...)
+	if err != nil {
+		return nil, err
+	}
+	c.Do("FLUSHDB")
+	return c, nil
+}
+
+func TestMain(m *testing.M) {
+	os.Exit(func() int {
+		flag.Parse()
+
+		var f *os.File
+		if *serverLogName != "" {
+			var err error
+			f, err = os.OpenFile(*serverLogName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Error opening redis-log: %v\n", err)
+				return 1
+			}
+			defer f.Close()
+			serverLog = f
+		}
+
+		defer stopDefaultServer()
+
+		return m.Run()
+	}())
+}

+ 114 - 0
src/github.com/gomodule/redigo/redis/zpop_example_test.go

@@ -0,0 +1,114 @@
+// Copyright 2013 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redis_test
+
+import (
+	"fmt"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+// zpop pops a value from the ZSET key using WATCH/MULTI/EXEC commands.
+func zpop(c redis.Conn, key string) (result string, err error) {
+
+	defer func() {
+		// Return connection to normal state on error.
+		if err != nil {
+			c.Do("DISCARD")
+		}
+	}()
+
+	// Loop until transaction is successful.
+	for {
+		if _, err := c.Do("WATCH", key); err != nil {
+			return "", err
+		}
+
+		members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0))
+		if err != nil {
+			return "", err
+		}
+		if len(members) != 1 {
+			return "", redis.ErrNil
+		}
+
+		c.Send("MULTI")
+		c.Send("ZREM", key, members[0])
+		queued, err := c.Do("EXEC")
+		if err != nil {
+			return "", err
+		}
+
+		if queued != nil {
+			result = members[0]
+			break
+		}
+	}
+
+	return result, nil
+}
+
+// zpopScript pops a value from a ZSET.
+var zpopScript = redis.NewScript(1, `
+    local r = redis.call('ZRANGE', KEYS[1], 0, 0)
+    if r ~= nil then
+        r = r[1]
+        redis.call('ZREM', KEYS[1], r)
+    end
+    return r
+`)
+
+// This example implements ZPOP as described at
+// http://redis.io/topics/transactions using WATCH/MULTI/EXEC and scripting.
+func Example_zpop() {
+	c, err := dial()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer c.Close()
+
+	// Add test data using a pipeline.
+
+	for i, member := range []string{"red", "blue", "green"} {
+		c.Send("ZADD", "zset", i, member)
+	}
+	if _, err := c.Do(""); err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// Pop using WATCH/MULTI/EXEC
+
+	v, err := zpop(c, "zset")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(v)
+
+	// Pop using a script.
+
+	v, err = redis.String(zpopScript.Do(c, "zset"))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(v)
+
+	// Output:
+	// red
+	// blue
+}

+ 54 - 0
src/github.com/gomodule/redigo/redisx/commandinfo.go

@@ -0,0 +1,54 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redisx
+
+import (
+	"strings"
+)
+
+const (
+	connectionWatchState = 1 << iota
+	connectionMultiState
+	connectionSubscribeState
+	connectionMonitorState
+)
+
+type commandInfo struct {
+	notMuxable bool
+}
+
+var commandInfos = map[string]commandInfo{
+	"WATCH":      {notMuxable: true},
+	"UNWATCH":    {notMuxable: true},
+	"MULTI":      {notMuxable: true},
+	"EXEC":       {notMuxable: true},
+	"DISCARD":    {notMuxable: true},
+	"PSUBSCRIBE": {notMuxable: true},
+	"SUBSCRIBE":  {notMuxable: true},
+	"MONITOR":    {notMuxable: true},
+}
+
+func init() {
+	for n, ci := range commandInfos {
+		commandInfos[strings.ToLower(n)] = ci
+	}
+}
+
+func lookupCommandInfo(commandName string) commandInfo {
+	if ci, ok := commandInfos[commandName]; ok {
+		return ci
+	}
+	return commandInfos[strings.ToUpper(commandName)]
+}

+ 11 - 0
src/github.com/gomodule/redigo/redisx/commandinfo_test.go

@@ -0,0 +1,11 @@
+package redisx
+
+import "testing"
+
+func TestLookupCommandInfo(t *testing.T) {
+	for _, n := range []string{"watch", "WATCH", "wAtch"} {
+		if lookupCommandInfo(n) == (commandInfo{}) {
+			t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n)
+		}
+	}
+}

+ 151 - 0
src/github.com/gomodule/redigo/redisx/connmux.go

@@ -0,0 +1,151 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redisx
+
+import (
+	"errors"
+	"sync"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+// ConnMux multiplexes one or more connections to a single underlying
+// connection. The ConnMux connections do not support concurrency, commands
+// that associate server side state with the connection or commands that put
+// the connection in a special mode.
+type ConnMux struct {
+	c redis.Conn
+
+	sendMu sync.Mutex
+	sendID uint
+
+	recvMu   sync.Mutex
+	recvID   uint
+	recvWait map[uint]chan struct{}
+}
+
+func NewConnMux(c redis.Conn) *ConnMux {
+	return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})}
+}
+
+// Get gets a connection. The application must close the returned connection.
+func (p *ConnMux) Get() redis.Conn {
+	c := &muxConn{p: p}
+	c.ids = c.buf[:0]
+	return c
+}
+
+// Close closes the underlying connection.
+func (p *ConnMux) Close() error {
+	return p.c.Close()
+}
+
+type muxConn struct {
+	p   *ConnMux
+	ids []uint
+	buf [8]uint
+}
+
+func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error {
+	if lookupCommandInfo(cmd).notMuxable {
+		return errors.New("command not supported by mux pool")
+	}
+	p := c.p
+	p.sendMu.Lock()
+	id := p.sendID
+	c.ids = append(c.ids, id)
+	p.sendID++
+	err := p.c.Send(cmd, args...)
+	if flush {
+		err = p.c.Flush()
+	}
+	p.sendMu.Unlock()
+	return err
+}
+
+func (c *muxConn) Send(cmd string, args ...interface{}) error {
+	return c.send(false, cmd, args...)
+}
+
+func (c *muxConn) Flush() error {
+	p := c.p
+	p.sendMu.Lock()
+	err := p.c.Flush()
+	p.sendMu.Unlock()
+	return err
+}
+
+func (c *muxConn) Receive() (interface{}, error) {
+	if len(c.ids) == 0 {
+		return nil, errors.New("mux pool underflow")
+	}
+
+	id := c.ids[0]
+	c.ids = c.ids[1:]
+	if len(c.ids) == 0 {
+		c.ids = c.buf[:0]
+	}
+
+	p := c.p
+	p.recvMu.Lock()
+	if p.recvID != id {
+		ch := make(chan struct{})
+		p.recvWait[id] = ch
+		p.recvMu.Unlock()
+		<-ch
+		p.recvMu.Lock()
+		if p.recvID != id {
+			panic("out of sync")
+		}
+	}
+
+	v, err := p.c.Receive()
+
+	id++
+	p.recvID = id
+	ch, ok := p.recvWait[id]
+	if ok {
+		delete(p.recvWait, id)
+	}
+	p.recvMu.Unlock()
+	if ok {
+		ch <- struct{}{}
+	}
+
+	return v, err
+}
+
+func (c *muxConn) Close() error {
+	var err error
+	if len(c.ids) == 0 {
+		return nil
+	}
+	c.Flush()
+	for _ = range c.ids {
+		_, err = c.Receive()
+	}
+	return err
+}
+
+func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) {
+	if err := c.send(true, cmd, args...); err != nil {
+		return nil, err
+	}
+	return c.Receive()
+}
+
+func (c *muxConn) Err() error {
+	return c.p.c.Err()
+}

+ 258 - 0
src/github.com/gomodule/redigo/redisx/connmux_test.go

@@ -0,0 +1,258 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package redisx_test
+
+import (
+	"net/textproto"
+	"sync"
+	"testing"
+
+	"github.com/gomodule/redigo/redis"
+	"github.com/gomodule/redigo/redisx"
+)
+
+func TestConnMux(t *testing.T) {
+	c, err := redisx.DialTest()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	m := redisx.NewConnMux(c)
+	defer m.Close()
+
+	c1 := m.Get()
+	c2 := m.Get()
+	c1.Send("ECHO", "hello")
+	c2.Send("ECHO", "world")
+	c1.Flush()
+	c2.Flush()
+	s, err := redis.String(c1.Receive())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s != "hello" {
+		t.Fatalf("echo returned %q, want %q", s, "hello")
+	}
+	s, err = redis.String(c2.Receive())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s != "world" {
+		t.Fatalf("echo returned %q, want %q", s, "world")
+	}
+	c1.Close()
+	c2.Close()
+}
+
+func TestConnMuxClose(t *testing.T) {
+	c, err := redisx.DialTest()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	m := redisx.NewConnMux(c)
+	defer m.Close()
+
+	c1 := m.Get()
+	c2 := m.Get()
+
+	if err := c1.Send("ECHO", "hello"); err != nil {
+		t.Fatal(err)
+	}
+	if err := c1.Close(); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := c2.Send("ECHO", "world"); err != nil {
+		t.Fatal(err)
+	}
+	if err := c2.Flush(); err != nil {
+		t.Fatal(err)
+	}
+
+	s, err := redis.String(c2.Receive())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s != "world" {
+		t.Fatalf("echo returned %q, want %q", s, "world")
+	}
+	c2.Close()
+}
+
+func BenchmarkConn(b *testing.B) {
+	b.StopTimer()
+	c, err := redisx.DialTest()
+	if err != nil {
+		b.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		if _, err := c.Do("PING"); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkConnMux(b *testing.B) {
+	b.StopTimer()
+	c, err := redisx.DialTest()
+	if err != nil {
+		b.Fatalf("error connection to database, %v", err)
+	}
+	m := redisx.NewConnMux(c)
+	defer m.Close()
+
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		c := m.Get()
+		if _, err := c.Do("PING"); err != nil {
+			b.Fatal(err)
+		}
+		c.Close()
+	}
+}
+
+func BenchmarkPool(b *testing.B) {
+	b.StopTimer()
+
+	p := redis.Pool{Dial: redisx.DialTest, MaxIdle: 1}
+	defer p.Close()
+
+	// Fill the pool.
+	c := p.Get()
+	if err := c.Err(); err != nil {
+		b.Fatal(err)
+	}
+	c.Close()
+
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		c := p.Get()
+		if _, err := c.Do("PING"); err != nil {
+			b.Fatal(err)
+		}
+		c.Close()
+	}
+}
+
+const numConcurrent = 10
+
+func BenchmarkConnMuxConcurrent(b *testing.B) {
+	b.StopTimer()
+	c, err := redisx.DialTest()
+	if err != nil {
+		b.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	m := redisx.NewConnMux(c)
+
+	var wg sync.WaitGroup
+	wg.Add(numConcurrent)
+
+	b.StartTimer()
+
+	for i := 0; i < numConcurrent; i++ {
+		go func() {
+			defer wg.Done()
+			for i := 0; i < b.N; i++ {
+				c := m.Get()
+				if _, err := c.Do("PING"); err != nil {
+					b.Fatal(err)
+				}
+				c.Close()
+			}
+		}()
+	}
+	wg.Wait()
+}
+
+func BenchmarkPoolConcurrent(b *testing.B) {
+	b.StopTimer()
+
+	p := redis.Pool{Dial: redisx.DialTest, MaxIdle: numConcurrent}
+	defer p.Close()
+
+	// Fill the pool.
+	conns := make([]redis.Conn, numConcurrent)
+	for i := range conns {
+		c := p.Get()
+		if err := c.Err(); err != nil {
+			b.Fatal(err)
+		}
+		conns[i] = c
+	}
+	for _, c := range conns {
+		c.Close()
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(numConcurrent)
+
+	b.StartTimer()
+
+	for i := 0; i < numConcurrent; i++ {
+		go func() {
+			defer wg.Done()
+			for i := 0; i < b.N; i++ {
+				c := p.Get()
+				if _, err := c.Do("PING"); err != nil {
+					b.Fatal(err)
+				}
+				c.Close()
+			}
+		}()
+	}
+	wg.Wait()
+}
+
+func BenchmarkPipelineConcurrency(b *testing.B) {
+	b.StopTimer()
+	c, err := redisx.DialTest()
+	if err != nil {
+		b.Fatalf("error connection to database, %v", err)
+	}
+	defer c.Close()
+
+	var wg sync.WaitGroup
+	wg.Add(numConcurrent)
+
+	var pipeline textproto.Pipeline
+
+	b.StartTimer()
+
+	for i := 0; i < numConcurrent; i++ {
+		go func() {
+			defer wg.Done()
+			for i := 0; i < b.N; i++ {
+				id := pipeline.Next()
+				pipeline.StartRequest(id)
+				c.Send("PING")
+				c.Flush()
+				pipeline.EndRequest(id)
+				pipeline.StartResponse(id)
+				_, err := c.Receive()
+				if err != nil {
+					b.Fatal(err)
+				}
+				pipeline.EndResponse(id)
+			}
+		}()
+	}
+	wg.Wait()
+}

+ 68 - 0
src/github.com/gomodule/redigo/redisx/db_test.go

@@ -0,0 +1,68 @@
+// Copyright 2014 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redistest contains utilities for writing Redigo tests.
+package redisx
+
+import (
+	"errors"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type testConn struct {
+	redis.Conn
+}
+
+func (t testConn) Close() error {
+	_, err := t.Conn.Do("SELECT", "9")
+	if err != nil {
+		return nil
+	}
+	_, err = t.Conn.Do("FLUSHDB")
+	if err != nil {
+		return err
+	}
+	return t.Conn.Close()
+}
+
+// Dial dials the local Redis server and selects database 9. To prevent
+// stomping on real data, DialTestDB fails if database 9 contains data. The
+// returned connection flushes database 9 on close.
+func DialTest() (redis.Conn, error) {
+	c, err := redis.DialTimeout("tcp", ":6379", 0, 1*time.Second, 1*time.Second)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = c.Do("SELECT", "9")
+	if err != nil {
+		c.Close()
+		return nil, err
+	}
+
+	n, err := redis.Int(c.Do("DBSIZE"))
+	if err != nil {
+		c.Close()
+		return nil, err
+	}
+
+	if n != 0 {
+		c.Close()
+		return nil, errors.New("database #9 is not empty, test can not continue")
+	}
+
+	return testConn{c}, nil
+}

+ 17 - 0
src/github.com/gomodule/redigo/redisx/doc.go

@@ -0,0 +1,17 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package redisx contains experimental features for Redigo. Features in this
+// package may be modified or deleted at any time.
+package redisx

+ 70 - 0
src/redis/redisloginutil.go

@@ -0,0 +1,70 @@
+package redis
+
+import (
+	"log"
+	"strings"
+	"time"
+
+	redisLogin "github.com/gomodule/redigo/redis"
+)
+
+var RedisLoginPool *redisLogin.Pool
+
+func InitRedisLogin(addrs string) {
+	addr := strings.Split(addrs, ",")
+	for _, v := range addr {
+		saddr := strings.Split(v, "=")
+		if saddr[0] == "login" {
+			RedisLoginPool = &redisLogin.Pool{MaxActive: 10, MaxIdle: 5,
+				IdleTimeout: time.Duration(10) * time.Second, Dial: func() (redisLogin.Conn, error) {
+					c, err := redisLogin.Dial("tcp", saddr[1])
+					if err != nil {
+						return nil, err
+					}
+					return c, nil
+				}}
+		}
+	}
+
+}
+
+//
+func SetLoginVal(key, value string) {
+	conn := RedisLoginPool.Get()
+	defer conn.Close()
+	conn.Do("PUBLISH", key, value)
+}
+
+//
+func GetLoginVal(key string, wxFunc func(wxParams []string) bool) {
+	for {
+		defer catch()
+	L:
+		for {
+			conn := RedisLoginPool.Get()
+			defer conn.Close()
+			if conn.Err() == nil {
+				psc := redisLogin.PubSubConn{Conn: conn}
+				if err := psc.Subscribe(redisLogin.Args{}.AddFlat(key)...); err != nil {
+					log.Println(err)
+				}
+				for {
+					msg := psc.Receive()
+					//					go func(msg interface{}) {
+					switch n := msg.(type) {
+					case error:
+						log.Println("wxlogin err", msg)
+						break L
+					case redisLogin.Message:
+						res := string(n.Data)
+						param := strings.Split(res, ",")
+						go wxFunc(param)
+					}
+					//					}(msg)
+				}
+			}
+			time.Sleep(2 * time.Second)
+		}
+		time.Sleep(1 * time.Second)
+	}
+}

+ 518 - 0
src/redis/redisutil.go

@@ -0,0 +1,518 @@
+package redis
+
+import (
+	"encoding/json"
+	"errors"
+	"log"
+	"runtime"
+	"strings"
+	"time"
+
+	redigo "github.com/garyburd/redigo/redis"
+)
+
+var RedisPool map[string]*redigo.Pool
+
+//初始化redis 1为多个连接池,2为共用一个连接池
+func InitRedis(addrs string) {
+	InitRedisBySize(addrs, 300, 30, 240)
+}
+
+func InitRedisBySize(addrs string, maxSize, maxIdle, timeout int) {
+	RedisPool = map[string]*redigo.Pool{}
+	addr := strings.Split(addrs, ",")
+	for _, v := range addr {
+		saddr := strings.Split(v, "=")
+		RedisPool[saddr[0]] = &redigo.Pool{MaxActive: maxSize, MaxIdle: maxIdle,
+			IdleTimeout: time.Duration(timeout) * time.Second, Dial: func() (redigo.Conn, error) {
+				c, err := redigo.Dial("tcp", saddr[1])
+				if err != nil {
+					return nil, err
+				}
+				return c, nil
+			}}
+	}
+}
+
+//分流redis
+//并存入字符串缓存
+func PutKV(key string, obj interface{}) bool {
+	return Put("other", key, obj, -1)
+}
+func PutCKV(code, key string, obj interface{}) bool {
+	return Put(code, key, obj, -1)
+}
+func Put(code, key string, obj interface{}, timeout int) bool {
+	b := false
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	var err error
+	_obj, _err := json.Marshal(obj)
+	if _err != nil {
+		log.Println("redisutil-SET-序列化出错Error", _err)
+		return b
+	}
+	if timeout < 1 {
+		_, err = conn.Do("SET", key, _obj)
+	} else {
+		_, err = conn.Do("SET", key, _obj, "EX", timeout)
+	}
+	if nil != err {
+		log.Println("redisutil-SETError-put", err)
+	} else {
+		b = true
+	}
+	return b
+}
+
+func BulkPut(code string, timeout int, obj ...interface{}) bool {
+	b := false
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	var err error
+	for _, _tmp := range obj {
+		tmp, ok := _tmp.([]interface{})
+		if ok && len(tmp) == 2 {
+			key, kok := tmp[0].(string)
+			if kok && key != "" {
+				_obj, _err := json.Marshal(tmp[1])
+				if _err != nil {
+					log.Println("redisutil-SET-序列化出错Error", _err)
+					return b
+				}
+				if timeout < 1 {
+					_, err = conn.Do("SET", key, _obj)
+				} else {
+					_, err = conn.Do("SET", key, _obj, "EX", timeout)
+				}
+			}
+		}
+	}
+	if nil != err {
+		b = false
+		log.Println("redisutil-SETError-put", err)
+	} else {
+		b = b && true
+	}
+	return b
+}
+
+//直接存字节流
+func PutBytes(code, key string, data *[]byte, timeout int) (err error) {
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+
+	if timeout < 1 {
+		_, err = conn.Do("SET", key, *data)
+	} else {
+		_, err = conn.Do("SET", key, *data, "EX", timeout)
+	}
+	if nil != err {
+		log.Println("redisutil-SETError", err)
+	}
+	return
+}
+
+//设置超时时间,单位秒
+func SetExpire(code, key string, expire int) error {
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	_, err := conn.Do("expire", key, expire)
+	return err
+}
+
+//判断一个key是否存在
+func Exists(code, key string) (bool, error) {
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	repl, err := conn.Do("exists", key)
+	ret, _ := redigo.Int(repl, err)
+	return ret == 1, err
+}
+
+//获取string
+func GetStr(code, key string) string {
+	res := Get(code, key)
+	str, _ := res.(string)
+	return str
+}
+
+//获取int
+func GetInt(code, key string) int {
+	result, _ := GetNewInt(code, key)
+	return result
+}
+
+func GetNewInt(code, key string) (int, error) {
+	var res interface{}
+	err := GetNewInterface(code, key, &res)
+	var result int
+	if str, ok := res.(float64); ok {
+		result = int(str)
+	}
+	return result, err
+}
+
+//取得字符串,支持变参,2个 (key,code),返回后自己断言
+func Get(code, key string) (result interface{}) {
+	GetInterface(code, key, &result)
+	return
+}
+
+func GetInterface(code, key string, result interface{}) {
+	GetNewInterface(code, key, result)
+}
+
+func GetNewInterface(code, key string, result interface{}) error {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("GET", key)
+	if nil != err {
+		log.Println("redisutil-GetError", err)
+	} else {
+		var ok bool
+		var res []byte
+		if res, ok = ret.([]byte); ok {
+			err = json.Unmarshal(res, result)
+			if err != nil {
+				log.Println("Get ERROR:", err.Error())
+			}
+		}
+	}
+	return err
+}
+
+//直接返回字节流
+func GetBytes(code, key string) (ret *[]byte, err error) {
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	var r interface{}
+	r, err = conn.Do("GET", key)
+	if err != nil {
+		log.Println("redisutil-GetBytesError", err)
+	} else {
+		if tmp, ok := r.([]byte); ok {
+			ret = &tmp
+		} else {
+			err = errors.New("redis返回数据格式不对")
+		}
+	}
+	return
+}
+func GetNewBytes(code, key string) (ret *[]byte, err error) {
+	defer catch()
+	redisPool := RedisPool[code]
+	if redisPool == nil {
+		err = errors.New("redis code " + code + " is nil")
+		log.Println("redisutil-GetNewBytesError", err)
+		return
+	}
+	conn := redisPool.Get()
+	defer conn.Close()
+	var r interface{}
+	r, err = conn.Do("GET", key)
+	if err != nil {
+		log.Println("redisutil-GetNewBytesError", err)
+	} else if r != nil {
+		if tmp, ok := r.([]byte); ok {
+			ret = &tmp
+		}
+	}
+	return
+}
+
+//删所有key
+func FlushDB(code string) bool {
+	b := false
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+
+	var err error
+	_, err = conn.Do("FLUSHDB")
+	if nil != err {
+		log.Println("redisutil-FLUSHDBError", err)
+	} else {
+		b = true
+	}
+	return b
+}
+
+//支持删除多个key
+func Del(code string, key ...interface{}) bool {
+	defer catch()
+	b := false
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+
+	var err error
+	_, err = conn.Do("DEL", key...)
+	if nil != err {
+		log.Println("redisutil-DELError", err)
+	} else {
+		b = true
+	}
+	return b
+}
+
+/**
+func DelKey(key ...interface{}) {
+	defer func() {
+		if r := recover(); r != nil {
+			log.Println("[E]", r)
+			for skip := 1; ; skip++ {
+				_, file, line, ok := runtime.Caller(skip)
+				if !ok {
+					break
+				}
+				go log.Printf("%v,%v\n", file, line)
+			}
+		}
+	}()
+	for i := 0; i < len(RedisPool); i++ {
+		delByNum(i, key...)
+	}
+
+}
+**/
+
+/**
+func delByNum(n int, key ...interface{}) {
+	defer func() {
+		if r := recover(); r != nil {
+			log.Println("[E]", r)
+			for skip := 1; ; skip++ {
+				_, file, line, ok := runtime.Caller(skip)
+				if !ok {
+					break
+				}
+				go log.Printf("%v,%v\n", file, line)
+			}
+		}
+	}()
+	i := 0
+	for _, v := range RedisPool {
+		if i == n {
+			conn := v.Get()
+			defer conn.Close()
+			conn.Do("DEL", key...)
+			break
+		}
+		i++
+	}
+}
+**/
+
+//根据代码和前辍key删除多个
+func DelByCodePattern(code, key string) {
+	defer catch()
+
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("KEYS", key)
+	var result []interface{}
+	if nil != err {
+		log.Println("redisutil-GetError", err)
+	} else {
+		result = ret.([]interface{})
+		for k := 0; k < len(result); k++ {
+			conn.Do("DEL", string(result[k].([]uint8)))
+		}
+	}
+}
+
+/**
+func DelByPattern(key string) {
+	defer func() {
+		if r := recover(); r != nil {
+			log.Println("[E]", r)
+			for skip := 1; ; skip++ {
+				_, file, line, ok := runtime.Caller(skip)
+				if !ok {
+					break
+				}
+				go log.Printf("%v,%v\n", file, line)
+			}
+		}
+	}()
+	i := 0
+	for _, v := range RedisPool {
+		conn := v.Get()
+		defer conn.Close()
+		ret, err := conn.Do("KEYS", key)
+		var result []interface{}
+		if nil != err {
+			log.Println("redisutil-GetError", err)
+		} else {
+			result = ret.([]interface{})
+			for k := 0; k < len(result); k++ {
+				delByNum(i, string(result[k].([]uint8)))
+			}
+		}
+		i++
+	}
+
+}
+**/
+//自增计数器
+func Incr(code, key string) int64 {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("INCR", key)
+	if nil != err {
+		log.Println("redisutil-INCR-Error", err)
+	} else {
+		if res, ok := ret.(int64); ok {
+			return res
+		} else {
+			return 0
+		}
+	}
+	return 0
+}
+
+//自减
+func Decrby(code, key string, val int) int64 {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("DECRBY", key, val)
+	if nil != err {
+		log.Println("redisutil-DECR-Error", err)
+	} else {
+		if res, ok := ret.(int64); ok {
+			return res
+		} else {
+			return 0
+		}
+	}
+	return 0
+}
+
+//根据正则去取
+func GetKeysByPattern(code, key string) []interface{} {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("KEYS", key)
+	if nil != err {
+		log.Println("redisutil-GetKeysError", err)
+		return nil
+	} else {
+		res, _ := ret.([]interface{})
+		return res
+	}
+}
+
+//批量取多个key
+func Mget(code string, key []string) []interface{} {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	interfaceKeys := make([]interface{}, len(key))
+	for n, k := range key {
+		interfaceKeys[n] = k
+	}
+	ret, err := conn.Do("MGET", interfaceKeys...)
+	if nil != err {
+		log.Println("redisutil-MgetError", err)
+		return nil
+	} else {
+		res, _ := ret.([]interface{})
+		return res
+	}
+}
+
+//取出并删除Key
+func Pop(code string, key string) (result interface{}) {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("GET", key)
+	if nil != err {
+		log.Println("redisutil-PopError", err)
+	} else {
+		var ok bool
+		var res []byte
+		if res, ok = ret.([]byte); ok {
+			err = json.Unmarshal(res, &result)
+			if err != nil {
+				log.Println("Poperr", err)
+			}
+		}
+		conn.Do("DEL", key)
+	}
+	return
+}
+
+//list操作
+func LPOP(code, list string) (result interface{}) {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("LPOP", list)
+	if nil != err {
+		log.Println("redisutil-LPopError", err)
+	} else {
+		if res, ok := ret.([]byte); ok {
+			err = json.Unmarshal(res, &result)
+			log.Println(err)
+		}
+	}
+	return
+}
+
+func RPUSH(code, list string, val interface{}) bool {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	_obj, _ := json.Marshal(val)
+	_, err := conn.Do("RPUSH", list, _obj)
+	if nil != err {
+		log.Println("redisutil-RPUSHError", err)
+		return false
+	}
+	return true
+}
+
+func LLEN(code, list string) int64 {
+	defer catch()
+	conn := RedisPool[code].Get()
+	defer conn.Close()
+	ret, err := conn.Do("LLEN", list)
+	if nil != err {
+		log.Println("redisutil-LLENError", err)
+		return 0
+	}
+	if res, ok := ret.(int64); ok {
+		return res
+	} else {
+		return 0
+	}
+}
+
+func catch() {
+	if r := recover(); r != nil {
+		log.Println(r)
+		for skip := 0; ; skip++ {
+			_, file, line, ok := runtime.Caller(skip)
+			if !ok {
+				break
+			}
+			go log.Printf("%v,%v\n", file, line)
+		}
+	}
+}

+ 86 - 0
src/redis/redisutil_test.go

@@ -0,0 +1,86 @@
+package redis
+
+import (
+	"fmt"
+	"log"
+	"testing"
+	"time"
+)
+
+func Test_redis(t *testing.T) {
+	InitRedis("192.168.3.142:6379,192.168.3.142:7379")
+
+	log.Println(PutKV("key-BBB", "v-oooo"))
+	log.Println(GetStr("", "key-BBB"))
+
+	log.Println(PutCKV("code-K", "key-BBB2", 123))
+	log.Println(GetInt("code-K", "key-BBB2"))
+
+	log.Println(Put("codeA", "BBB", "oooo", 0))
+	//log.Println(Del("codeA", "AAA"))
+	log.Println(GetStr("codeA", "AAA"))
+
+	log.Println(Put("code", "Key", &map[string]string{"name": "张三"}, 0))
+	log.Println(Get("code", "Key"))
+}
+
+func Test_redisMorePut(t *testing.T) {
+	//by := []byte("dddddddddddd555d")
+
+	//InitRedis("enterprise=192.168.3.14:1379,service=192.168.3.14:2379,other=192.168.3.14:3379")
+	InitRedis("sso=192.168.3.14:1379")
+	//PutBytes("enterprise", "A", &by, 0)
+	//Put("sso", "new_1070776706", "obEpLuMMDUYUM-zlzCbQ0MbuQOzc", 180)
+	Put("sso", "1903540616", "oJULtwzXo6EFV1Ah-XeyRBimXGM8", 180)
+	//log.Println(Get("enterprise", "A"))
+	//GetMoreKey("ent*")
+}
+
+func Test_other(t *testing.T) {
+	//s := `[{"S_remark":"kokopkopkop","s_pic":"/upload/2015/10/16/2015101611194101018681.jpg","o_extend":{"k1":"ioio","k7":"000"}}]`
+	//mo := []map[string]interface{}{}
+	//json.Unmarshal([]byte(s), &mo)
+	//log.Println(mo)
+
+	pools := make(chan bool, 1)
+	pool := make(chan bool, 2)
+	for i := 0; i < 1000; i++ {
+		go func(i int) {
+			pool <- true
+			defer func() {
+				<-pool
+			}()
+			log.Println("--", i)
+			time.Sleep(5 * time.Millisecond)
+
+		}(i)
+	}
+	<-pools
+}
+
+func TestIncr(t *testing.T) {
+	InitRedis("other=192.168.3.14:1379")
+	//log.Println(GetBytes("sso", "p_share_3200000006"))
+	Put("other", "test", "12212212", 50000)
+}
+
+func Test_Pop(t *testing.T) {
+	InitRedis("sso=192.168.3.207:1601")
+	m := "aaaa"
+	Put("sso", "aaa", m, 1000)
+	log.Println(Pop("sso", "aaa"))
+}
+
+func Test_MGET(t *testing.T) {
+	InitRedis("sso=192.168.3.207:3378")
+	InitRedis("sso=192.168.3.207:1711")
+	n := 0
+	for {
+		n++
+		Put("sso", fmt.Sprintf("%d_1", n), n, 3600)
+		time.Sleep(8 * time.Second)
+
+	}
+	res := Mget("sso", []string{"pn_金业街(西自由街-虎泉路)道路建设工程"})
+	log.Println(res)
+}