Repository: gomodule/redigo
Pull request #563 (2021-05-31 04:21:53)

Fix do script issue on convert error failed


Patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index e59fd3c..eb7a22f 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -16,7 +16,7 @@ jobs:
         uses: golangci/golangci-lint-action@v2
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
-          version: v1.36
+          version: v1.46.2
 
           # Optional: working directory, useful for monorepos
           # working-directory: somedir
diff --git a/go.mod b/go.mod
index a86a793..4efebf8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,10 @@
 module github.com/gomodule/redigo
 
-go 1.14
+go 1.16
 
-require github.com/stretchr/testify v1.5.1
+require github.com/stretchr/testify v1.7.0
+
+retract (
+	v2.0.0+incompatible // Old development version not maintained or published.
+	v0.0.0-do-not-use // Never used only present due to lack of retract.
+)
diff --git a/go.sum b/go.sum
index 331fa69..acb88a4 100644
--- a/go.sum
+++ b/go.sum
@@ -3,9 +3,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/redis/conn.go b/redis/conn.go
index 1398b4d..753644b 100644
--- a/redis/conn.go
+++ b/redis/conn.go
@@ -168,6 +168,7 @@ func DialPassword(password string) DialOption {
 
 // DialUsername specifies the username to use when connecting to
 // the Redis server when Redis ACLs are used.
+// A DialPassword must also be passed otherwise this option will have no effect.
 func DialUsername(username string) DialOption {
 	return DialOption{func(do *dialOptions) {
 		do.username = username
@@ -245,7 +246,7 @@ func DialContext(ctx context.Context, network, address string, options ...DialOp
 		if do.tlsConfig == nil {
 			tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify}
 		} else {
-			tlsConfig = cloneTLSConfig(do.tlsConfig)
+			tlsConfig = do.tlsConfig.Clone()
 		}
 		if tlsConfig.ServerName == "" {
 			host, _, err := net.SplitHostPort(address)
@@ -290,21 +291,21 @@ func DialContext(ctx context.Context, network, address string, options ...DialOp
 			authArgs = append(authArgs, do.username)
 		}
 		authArgs = append(authArgs, do.password)
-		if _, err := c.Do("AUTH", authArgs...); err != nil {
+		if _, err := c.DoContext(ctx, "AUTH", authArgs...); err != nil {
 			netConn.Close()
 			return nil, err
 		}
 	}
 
 	if do.clientName != "" {
-		if _, err := c.Do("CLIENT", "SETNAME", do.clientName); err != nil {
+		if _, err := c.DoContext(ctx, "CLIENT", "SETNAME", do.clientName); err != nil {
 			netConn.Close()
 			return nil, err
 		}
 	}
 
 	if do.db != 0 {
-		if _, err := c.Do("SELECT", do.db); err != nil {
+		if _, err := c.DoContext(ctx, "SELECT", do.db); err != nil {
 			netConn.Close()
 			return nil, err
 		}
@@ -315,10 +316,17 @@ func DialContext(ctx context.Context, network, address string, options ...DialOp
 
 var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
 
-// DialURL connects to a Redis server at the given URL using the Redis
+// DialURL wraps DialURLContext using context.Background.
+func DialURL(rawurl string, options ...DialOption) (Conn, error) {
+	ctx := context.Background()
+
+	return DialURLContext(ctx, rawurl, options...)
+}
+
+// DialURLContext 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) {
+func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) {
 	u, err := url.Parse(rawurl)
 	if err != nil {
 		return nil, err
@@ -347,8 +355,18 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) {
 
 	if u.User != nil {
 		password, isSet := u.User.Password()
+		username := u.User.Username()
 		if isSet {
-			options = append(options, DialUsername(u.User.Username()), DialPassword(password))
+			if username != "" {
+				// ACL
+				options = append(options, DialUsername(username), DialPassword(password))
+			} else {
+				// requirepass - user-info username:password with blank username
+				options = append(options, DialPassword(password))
+			}
+		} else if username != "" {
+			// requirepass - redis-cli compatibility which treats as single arg in user-info as a password
+			options = append(options, DialPassword(username))
 		}
 	}
 
@@ -370,7 +388,7 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) {
 
 	options = append(options, DialUseTLS(u.Scheme == "rediss"))
 
-	return Dial("tcp", address, options...)
+	return DialContext(ctx, "tcp", address, options...)
 }
 
 // NewConn returns a new Redigo connection for the given net connection.
@@ -692,6 +710,36 @@ func (c *conn) Receive() (interface{}, error) {
 	return c.ReceiveWithTimeout(c.readTimeout)
 }
 
+func (c *conn) ReceiveContext(ctx context.Context) (interface{}, error) {
+	var realTimeout time.Duration
+	if dl, ok := ctx.Deadline(); ok {
+		timeout := time.Until(dl)
+		if timeout >= c.readTimeout && c.readTimeout != 0 {
+			realTimeout = c.readTimeout
+		} else if timeout <= 0 {
+			return nil, c.fatal(context.DeadlineExceeded)
+		} else {
+			realTimeout = timeout
+		}
+	} else {
+		realTimeout = c.readTimeout
+	}
+	endch := make(chan struct{})
+	var r interface{}
+	var e error
+	go func() {
+		defer close(endch)
+
+		r, e = c.ReceiveWithTimeout(realTimeout)
+	}()
+	select {
+	case <-ctx.Done():
+		return nil, c.fatal(ctx.Err())
+	case <-endch:
+		return r, e
+	}
+}
+
 func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
 	var deadline time.Time
 	if timeout != 0 {
@@ -726,6 +774,36 @@ func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
 	return c.DoWithTimeout(c.readTimeout, cmd, args...)
 }
 
+func (c *conn) DoContext(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) {
+	var realTimeout time.Duration
+	if dl, ok := ctx.Deadline(); ok {
+		timeout := time.Until(dl)
+		if timeout >= c.readTimeout && c.readTimeout != 0 {
+			realTimeout = c.readTimeout
+		} else if timeout <= 0 {
+			return nil, c.fatal(context.DeadlineExceeded)
+		} else {
+			realTimeout = timeout
+		}
+	} else {
+		realTimeout = c.readTimeout
+	}
+	endch := make(chan struct{})
+	var r interface{}
+	var e error
+	go func() {
+		defer close(endch)
+
+		r, e = c.DoWithTimeout(realTimeout, cmd, args...)
+	}()
+	select {
+	case <-ctx.Done():
+		return nil, c.fatal(ctx.Err())
+	case <-endch:
+		return r, e
+	}
+}
+
 func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
 	c.mu.Lock()
 	pending := c.pending
diff --git a/redis/go17.go b/redis/go17.go
deleted file mode 100644
index 5f36379..0000000
--- a/redis/go17.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// +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,
-	}
-}
diff --git a/redis/go18.go b/redis/go18.go
deleted file mode 100644
index 558363b..0000000
--- a/redis/go18.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// +build go1.8
-
-package redis
-
-import "crypto/tls"
-
-func cloneTLSConfig(cfg *tls.Config) *tls.Config {
-	return cfg.Clone()
-}
diff --git a/redis/log.go b/redis/log.go
index ef8cd7a..72e054f 100644
--- a/redis/log.go
+++ b/redis/log.go
@@ -16,6 +16,7 @@ package redis
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"log"
 	"time"
@@ -121,6 +122,12 @@ func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{},
 	return reply, err
 }
 
+func (c *loggingConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (interface{}, error) {
+	reply, err := DoContext(c.Conn, ctx, commandName, args...)
+	c.print("DoContext", 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)
@@ -139,6 +146,12 @@ func (c *loggingConn) Receive() (interface{}, error) {
 	return reply, err
 }
 
+func (c *loggingConn) ReceiveContext(ctx context.Context) (interface{}, error) {
+	reply, err := ReceiveContext(c.Conn, ctx)
+	c.print("ReceiveContext", "", 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)
diff --git a/redis/pool.go b/redis/pool.go
index c7a2f19..d7bb71e 100644
--- a/redis/pool.go
+++ b/redis/pool.go
@@ -512,6 +512,20 @@ func (ac *activeConn) Err() error {
 	return pc.c.Err()
 }
 
+func (ac *activeConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	cwt, ok := pc.c.(ConnWithContext)
+	if !ok {
+		return nil, errContextNotSupported
+	}
+	ci := lookupCommandInfo(commandName)
+	ac.state = (ac.state | ci.Set) &^ ci.Clear
+	return cwt.DoContext(ctx, commandName, args...)
+}
+
 func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
 	pc := ac.pc
 	if pc == nil {
@@ -562,6 +576,18 @@ func (ac *activeConn) Receive() (reply interface{}, err error) {
 	return pc.c.Receive()
 }
 
+func (ac *activeConn) ReceiveContext(ctx context.Context) (reply interface{}, err error) {
+	pc := ac.pc
+	if pc == nil {
+		return nil, errConnClosed
+	}
+	cwt, ok := pc.c.(ConnWithContext)
+	if !ok {
+		return nil, errContextNotSupported
+	}
+	return cwt.ReceiveContext(ctx)
+}
+
 func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
 	pc := ac.pc
 	if pc == nil {
@@ -577,6 +603,9 @@ func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface
 type errorConn struct{ err error }
 
 func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
+func (ec errorConn) DoContext(context.Context, string, ...interface{}) (interface{}, error) {
+	return nil, ec.err
+}
 func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) {
 	return nil, ec.err
 }
@@ -585,6 +614,7 @@ func (ec errorConn) Err() error                                            { ret
 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) ReceiveContext(context.Context) (interface{}, error)   { return nil, ec.err }
 func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err }
 
 type idleList struct {
diff --git a/redis/pubsub.go b/redis/pubsub.go
index cc58575..67b885b 100644
--- a/redis/pubsub.go
+++ b/redis/pubsub.go
@@ -15,6 +15,7 @@
 package redis
 
 import (
+	"context"
 	"errors"
 	"time"
 )
@@ -116,6 +117,13 @@ func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} {
 	return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout))
 }
 
+// ReceiveContext is like Receive, but it allows termination of the receive
+// via a Context. If the call returns due to closure of the context's Done
+// channel the underlying Conn will have been closed.
+func (c PubSubConn) ReceiveContext(ctx context.Context) interface{} {
+	return c.receiveInternal(ReceiveContext(c.Conn, ctx))
+}
+
 func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} {
 	reply, err := Values(replyArg, errArg)
 	if err != nil {
diff --git a/redis/redis.go b/redis/redis.go
index e446487..faf574e 100644
--- a/redis/redis.go
+++ b/redis/redis.go
@@ -15,6 +15,7 @@
 package redis
 
 import (
+	"context"
 	"errors"
 	"time"
 )
@@ -33,6 +34,7 @@ type Conn interface {
 	Err() error
 
 	// Do sends a command to the server and returns the received reply.
+	// This function will use the timeout which was set when the connection is created
 	Do(commandName string, args ...interface{}) (reply interface{}, err error)
 
 	// Send writes the command to the client's output buffer.
@@ -82,17 +84,52 @@ type Scanner 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 sends a command to the server and returns the received reply.
+	// The timeout overrides the readtimeout 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 receives a single reply from the Redis server.
+	// The timeout overrides the readtimeout set when dialing the connection.
 	ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
 }
 
+// ConnWithContext is an optional interface that allows the caller to control the command's life with context.
+type ConnWithContext interface {
+	Conn
+
+	// DoContext sends a command to server and returns the received reply.
+	// min(ctx,DialReadTimeout()) will be used as the deadline.
+	// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
+	// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
+	// ctx timeout return err context.DeadlineExceeded.
+	// ctx canceled return err context.Canceled.
+	DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error)
+
+	// ReceiveContext receives a single reply from the Redis server.
+	// min(ctx,DialReadTimeout()) will be used as the deadline.
+	// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
+	// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
+	// ctx timeout return err context.DeadlineExceeded.
+	// ctx canceled return err context.Canceled.
+	ReceiveContext(ctx context.Context) (reply interface{}, err error)
+}
+
 var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout")
+var errContextNotSupported = errors.New("redis: connection does not support ConnWithContext")
+
+// DoContext sends a command to server and returns the received reply.
+// min(ctx,DialReadTimeout()) will be used as the deadline.
+// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
+// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
+// ctx timeout return err context.DeadlineExceeded.
+// ctx canceled return err context.Canceled.
+func DoContext(c Conn, ctx context.Context, cmd string, args ...interface{}) (interface{}, error) {
+	cwt, ok := c.(ConnWithContext)
+	if !ok {
+		return nil, errContextNotSupported
+	}
+	return cwt.DoContext(ctx, cmd, args...)
+}
 
 // DoWithTimeout executes a Redis command with the specified read timeout. If
 // the connection does not satisfy the ConnWithTimeout interface, then an error
@@ -105,6 +142,20 @@ func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{
 	return cwt.DoWithTimeout(timeout, cmd, args...)
 }
 
+// ReceiveContext receives a single reply from the Redis server.
+// min(ctx,DialReadTimeout()) will be used as the deadline.
+// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
+// DialReadTimeout() timeout return err can be checked by strings.Contains(e.Error(), "io/timeout").
+// ctx timeout return err context.DeadlineExceeded.
+// ctx canceled return err context.Canceled.
+func ReceiveContext(c Conn, ctx context.Context) (interface{}, error) {
+	cwt, ok := c.(ConnWithContext)
+	if !ok {
+		return nil, errContextNotSupported
+	}
+	return cwt.ReceiveContext(ctx)
+}
+
 // ReceiveWithTimeout receives a reply with the specified read timeout. If the
 // connection does not satisfy the ConnWithTimeout interface, then an error is
 // returned.
diff --git a/redis/reply.go b/redis/reply.go
index dfe6aff..7a2b3fe 100644
--- a/redis/reply.go
+++ b/redis/reply.go
@@ -277,13 +277,16 @@ func sliceHelper(reply interface{}, err error, name string, makeSlice func(int),
 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)
+		switch v := v.(type) {
+		case []byte:
+			f, err := strconv.ParseFloat(string(v), 64)
+			result[i] = f
+			return err
+		case Error:
+			return v
+		default:
+			return fmt.Errorf("redigo: unexpected element type for Float64s, got type %T", v)
 		}
-		f, err := strconv.ParseFloat(string(p), 64)
-		result[i] = f
-		return err
 	})
 	return result, err
 }
@@ -302,6 +305,8 @@ func Strings(reply interface{}, err error) ([]string, error) {
 		case []byte:
 			result[i] = string(v)
 			return nil
+		case Error:
+			return v
 		default:
 			return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v)
 		}
@@ -316,12 +321,15 @@ func Strings(reply interface{}, err error) ([]string, error) {
 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 {
+		switch v := v.(type) {
+		case []byte:
+			result[i] = v
+			return nil
+		case Error:
+			return v
+		default:
 			return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v)
 		}
-		result[i] = p
-		return nil
 	})
 	return result, err
 }
@@ -341,6 +349,8 @@ func Int64s(reply interface{}, err error) ([]int64, error) {
 			n, err := strconv.ParseInt(string(v), 10, 64)
 			result[i] = n
 			return err
+		case Error:
+			return v
 		default:
 			return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v)
 		}
@@ -367,6 +377,8 @@ func Ints(reply interface{}, err error) ([]int, error) {
 			n, err := strconv.Atoi(string(v))
 			result[i] = n
 			return err
+		case Error:
+			return v
 		default:
 			return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v)
 		}
@@ -374,79 +386,122 @@ func Ints(reply interface{}, err error) ([]int, error) {
 	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)
+// mapHelper builds a map from the data in reply.
+func mapHelper(reply interface{}, err error, name string, makeMap func(int), assign func(key string, value interface{}) error) error {
+	values, err := Values(reply, err)
 	if err != nil {
-		return nil, err
+		return err
 	}
+
 	if len(values)%2 != 0 {
-		return nil, errors.New("redigo: StringMap expects even number of values result")
+		return fmt.Errorf("redigo: %s expects even number of values result, got %d", name, len(values))
 	}
-	m := make(map[string]string, len(values)/2)
+
+	makeMap(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")
+		key, ok := values[i].([]byte)
+		if !ok {
+			return fmt.Errorf("redigo: %s key[%d] not a bulk string value, got %T", name, i, values[i])
+		}
+
+		if err := assign(string(key), values[i+1]); err != nil {
+			return err
 		}
-		m[string(key)] = string(value)
 	}
-	return m, nil
+
+	return 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(reply interface{}, err error) (map[string]string, error) {
+	var result map[string]string
+	err = mapHelper(reply, err, "StringMap",
+		func(n int) {
+			result = make(map[string]string, n)
+		}, func(key string, v interface{}) error {
+			value, ok := v.([]byte)
+			if !ok {
+				return fmt.Errorf("redigo: StringMap for %q not a bulk string value, got %T", key, v)
+			}
+
+			result[key] = string(value)
+
+			return nil
+		},
+	)
+
+	return result, err
 }
 
 // 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
+	var m map[string]int
+	err = mapHelper(result, err, "IntMap",
+		func(n int) {
+			m = make(map[string]int, n)
+		}, func(key string, v interface{}) error {
+			value, err := Int(v, nil)
+			if err != nil {
+				return err
+			}
+
+			m[key] = value
+
+			return nil
+		},
+	)
+
+	return m, err
 }
 
 // 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
+	var m map[string]int64
+	err = mapHelper(result, err, "Int64Map",
+		func(n int) {
+			m = make(map[string]int64, n)
+		}, func(key string, v interface{}) error {
+			value, err := Int64(v, nil)
+			if err != nil {
+				return err
+			}
+
+			m[key] = value
+
+			return nil
+		},
+	)
+
+	return m, err
+}
+
+// Float64Map is a helper that converts an array of strings (alternating key, value)
+// into a map[string]float64. The HGETALL commands return replies in this format.
+// Requires an even number of values in result.
+func Float64Map(result interface{}, err error) (map[string]float64, error) {
+	var m map[string]float64
+	err = mapHelper(result, err, "Float64Map",
+		func(n int) {
+			m = make(map[string]float64, n)
+		}, func(key string, v interface{}) error {
+			value, err := Float64(v, nil)
+			if err != nil {
+				return err
+			}
+
+			m[key] = value
+
+			return nil
+		},
+	)
+
+	return m, err
 }
 
 // Positions is a helper that converts an array of positions (lat, long)
@@ -461,21 +516,26 @@ func Positions(result interface{}, err error) ([]*[2]float64, error) {
 		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
@@ -496,6 +556,8 @@ func Uint64s(reply interface{}, err error) ([]uint64, error) {
 			n, err := strconv.ParseUint(string(v), 10, 64)
 			result[i] = n
 			return err
+		case Error:
+			return v
 		default:
 			return fmt.Errorf("redigo: unexpected element type for Uint64s, got type %T", v)
 		}
@@ -507,26 +569,23 @@ func Uint64s(reply interface{}, err error) ([]uint64, error) {
 // into a map[string]uint64. The HGETALL commands return replies in this format.
 // Requires an even number of values in result.
 func Uint64Map(result interface{}, err error) (map[string]uint64, error) {
-	values, err := Values(result, err)
-	if err != nil {
-		return nil, err
-	}
-	if len(values)%2 != 0 {
-		return nil, errors.New("redigo: Uint64Map expects even number of values result")
-	}
-	m := make(map[string]uint64, len(values)/2)
-	for i := 0; i < len(values); i += 2 {
-		key, ok := values[i].([]byte)
-		if !ok {
-			return nil, errors.New("redigo: Uint64Map key not a bulk string value")
-		}
-		value, err := Uint64(values[i+1], nil)
-		if err != nil {
-			return nil, err
-		}
-		m[string(key)] = value
-	}
-	return m, nil
+	var m map[string]uint64
+	err = mapHelper(result, err, "Uint64Map",
+		func(n int) {
+			m = make(map[string]uint64, n)
+		}, func(key string, v interface{}) error {
+			value, err := Uint64(v, nil)
+			if err != nil {
+				return err
+			}
+
+			m[key] = value
+
+			return nil
+		},
+	)
+
+	return m, err
 }
 
 // SlowLogs is a helper that parse the SLOWLOG GET command output and
@@ -537,44 +596,49 @@ func SlowLogs(result interface{}, err error) ([]SlowLog, error) {
 		return nil, err
 	}
 	logs := make([]SlowLog, len(rawLogs))
-	for i, rawLog := range rawLogs {
-		rawLog, ok := rawLog.([]interface{})
+	for i, e := range rawLogs {
+		rawLog, ok := e.([]interface{})
 		if !ok {
-			return nil, errors.New("redigo: slowlog element is not an array")
+			return nil, fmt.Errorf("redigo: slowlog element is not an array, got %T", e)
 		}
 
 		var log SlowLog
-
 		if len(rawLog) < 4 {
-			return nil, errors.New("redigo: slowlog element has less than four elements")
+			return nil, fmt.Errorf("redigo: slowlog element has %d elements, expected at least 4", len(rawLog))
 		}
+
 		log.ID, ok = rawLog[0].(int64)
 		if !ok {
-			return nil, errors.New("redigo: slowlog element[0] not an int64")
+			return nil, fmt.Errorf("redigo: slowlog element[0] not an int64, got %T", rawLog[0])
 		}
+
 		timestamp, ok := rawLog[1].(int64)
 		if !ok {
-			return nil, errors.New("redigo: slowlog element[1] not an int64")
+			return nil, fmt.Errorf("redigo: slowlog element[1] not an int64, got %T", rawLog[1])
 		}
+
 		log.Time = time.Unix(timestamp, 0)
 		duration, ok := rawLog[2].(int64)
 		if !ok {
-			return nil, errors.New("redigo: slowlog element[2] not an int64")
+			return nil, fmt.Errorf("redigo: slowlog element[2] not an int64, got %T", rawLog[2])
 		}
+
 		log.ExecutionTime = time.Duration(duration) * time.Microsecond
 
 		log.Args, err = Strings(rawLog[3], nil)
 		if err != nil {
-			return nil, fmt.Errorf("redigo: slowlog element[3] is not array of string. actual error is : %s", err.Error())
+			return nil, fmt.Errorf("redigo: slowlog element[3] is not array of strings: %w", err)
 		}
+
 		if len(rawLog) >= 6 {
 			log.ClientAddr, err = String(rawLog[4], nil)
 			if err != nil {
-				return nil, fmt.Errorf("redigo: slowlog element[4] is not a string. actual error is : %s", err.Error())
+				return nil, fmt.Errorf("redigo: slowlog element[4] is not a string: %w", err)
 			}
+
 			log.ClientName, err = String(rawLog[5], nil)
 			if err != nil {
-				return nil, fmt.Errorf("redigo: slowlog element[5] is not a string. actual error is : %s", err.Error())
+				return nil, fmt.Errorf("redigo: slowlog element[5] is not a string: %w", err)
 			}
 		}
 		logs[i] = log
diff --git a/redis/script.go b/redis/script.go
index d0cec1e..c585518 100644
--- a/redis/script.go
+++ b/redis/script.go
@@ -15,6 +15,7 @@
 package redis
 
 import (
+	"context"
 	"crypto/sha1"
 	"encoding/hex"
 	"io"
@@ -60,13 +61,25 @@ func (s *Script) Hash() string {
 	return s.hash
 }
 
+func (s *Script) DoContext(ctx context.Context, c Conn, keysAndArgs ...interface{}) (interface{}, error) {
+	cwt, ok := c.(ConnWithContext)
+	if !ok {
+		return nil, errContextNotSupported
+	}
+	v, err := cwt.DoContext(ctx, "EVALSHA", s.args(s.hash, keysAndArgs)...)
+	if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
+		v, err = cwt.DoContext(ctx, "EVAL", s.args(s.src, keysAndArgs)...)
+	}
+	return v, err
+}
+
 // 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 ") {
+	if err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") {
 		v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
 	}
 	return v, err


Test patch:
diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml
new file mode 100644
index 0000000..1086bf3
--- /dev/null
+++ b/.github/workflows/go-test.yml
@@ -0,0 +1,58 @@
+name: go-test
+on:
+  push:
+    tags:
+      - v*
+    branches:
+      - master
+  pull_request:
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        go-version:
+        - '1.17.x'
+        - '1.18.x'
+        os:
+        - 'ubuntu-latest'
+        redis:
+        - '6.2'
+        - '6.0'
+        - '5.0'
+        - '4.0'
+    name: Test go ${{ matrix.go-version }} redis ${{ matrix.redis }} on ${{ matrix.os }}
+    steps:
+      - name: Setup redis
+        uses: shogo82148/actions-setup-redis@v1
+        with:
+          redis-version: ${{ matrix.redis }}
+
+      - name: Install Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Go Cache Paths
+        id: go-cache-paths
+        run: |
+          echo "::set-output name=go-build::$(go env GOCACHE)"
+          echo "::set-output name=go-mod::$(go env GOMODCACHE)"
+
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Go Build Cache
+        uses: actions/cache@v3
+        with:
+          path: ${{ steps.go-cache-paths.outputs.go-build }}
+          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
+
+      - name: Go Mod Cache
+        uses: actions/cache@v3
+        with:
+          path: ${{ steps.go-cache-paths.outputs.go-mod }}
+          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
+
+      - name: Go Test
+        run: go test -v ./...
diff --git a/redis/conn_test.go b/redis/conn_test.go
index 425c4f0..b33cc41 100644
--- a/redis/conn_test.go
+++ b/redis/conn_test.go
@@ -640,6 +640,13 @@ var dialURLTests = []struct {
 	w           string
 }{
 	{"password", "redis://:abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
+	{"password redis-cli compat", "redis://abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
+	{"password db1", "redis://:abc123@localhost/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"},
+	{"password db1 redis-cli compat", "redis://abc123@localhost/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"},
+	{"password no host db0", "redis://:abc123@/0", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
+	{"password no host db0 redis-cli compat", "redis://abc123@/0", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
+	{"password no host db1", "redis://:abc123@/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"},
+	{"password no host db1 redis-cli compat", "redis://abc123@/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"},
 	{"username and password", "redis://user:password@localhost", "+OK\r\n", "*3\r\n$4\r\nAUTH\r\n$4\r\nuser\r\n$8\r\npassword\r\n"},
 	{"username", "redis://x:@localhost", "+OK\r\n", ""},
 	{"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"},
@@ -649,16 +656,18 @@ var dialURLTests = []struct {
 
 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)
-		}
+		t.Run(tt.description, func(t *testing.T) {
+			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, buf: %v", tt.description, err, buf.String())
+				return
+			}
+			if w := buf.String(); w != tt.w {
+				t.Errorf("%s commands = %q, want %q", tt.description, w, tt.w)
+			}
+		})
 	}
 }
 
diff --git a/redis/pubsub_test.go b/redis/pubsub_test.go
index 83b9158..63e0869 100644
--- a/redis/pubsub_test.go
+++ b/redis/pubsub_test.go
@@ -15,6 +15,8 @@
 package redis_test
 
 import (
+	"context"
+	"errors"
 	"reflect"
 	"testing"
 	"time"
@@ -74,3 +76,25 @@ func TestPushed(t *testing.T) {
 		t.Errorf("recv /w timeout got %v, want %v", got, want)
 	}
 }
+
+func TestPubSubReceiveContext(t *testing.T) {
+	sc, err := redis.DialDefaultServer()
+	if err != nil {
+		t.Fatalf("error connection to database, %v", err)
+	}
+	defer sc.Close()
+
+	c := redis.PubSubConn{Conn: sc}
+
+	require.NoError(t, c.Subscribe("c1"))
+	expectPushed(t, c, "Subscribe(c1)", redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1})
+
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+	got := c.ReceiveContext(ctx)
+	if err, ok := got.(error); !ok {
+		t.Errorf("recv w/canceled expected Canceled got non-error type %T", got)
+	} else if !errors.Is(err, context.Canceled) {
+		t.Errorf("recv w/canceled expected Canceled got %v", err)
+	}
+}
diff --git a/redis/redis_test.go b/redis/redis_test.go
index 5a98f53..e0f4538 100644
--- a/redis/redis_test.go
+++ b/redis/redis_test.go
@@ -15,6 +15,7 @@
 package redis_test
 
 import (
+	"context"
 	"testing"
 	"time"
 
@@ -26,6 +27,7 @@ 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
 }
@@ -33,6 +35,7 @@ func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args
 func (tc timeoutTestConn) Receive() (interface{}, error) {
 	return time.Duration(-1), nil
 }
+
 func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
 	return timeout, nil
 }
@@ -69,3 +72,52 @@ func TestPoolConnTimeout(t *testing.T) {
 	p := &redis.Pool{Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil }}
 	testTimeout(t, p.Get())
 }
+
+type contextDeadTestConn int
+
+func (cc contextDeadTestConn) Do(string, ...interface{}) (interface{}, error) {
+	return -1, nil
+}
+func (cc contextDeadTestConn) DoContext(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) {
+	return 1, nil
+}
+func (cc contextDeadTestConn) Receive() (interface{}, error) {
+	return -1, nil
+}
+func (cc contextDeadTestConn) ReceiveContext(ctx context.Context) (interface{}, error) {
+	return 1, nil
+}
+func (cc contextDeadTestConn) Send(string, ...interface{}) error { return nil }
+func (cc contextDeadTestConn) Err() error                        { return nil }
+func (cc contextDeadTestConn) Close() error                      { return nil }
+func (cc contextDeadTestConn) Flush() error                      { return nil }
+
+func testcontext(t *testing.T, c redis.Conn) {
+	r, e := c.Do("PING")
+	if r != -1 || e != nil {
+		t.Errorf("Do() = %v, %v, want %v, %v", r, e, -1, nil)
+	}
+	ctx, f := context.WithTimeout(context.Background(), time.Minute)
+	defer f()
+	r, e = redis.DoContext(c, ctx, "PING")
+	if r != 1 || e != nil {
+		t.Errorf("DoContext() = %v, %v, want %v, %v", r, e, 1, nil)
+	}
+	r, e = c.Receive()
+	if r != -1 || e != nil {
+		t.Errorf("Receive() = %v, %v, want %v, %v", r, e, -1, nil)
+	}
+	r, e = redis.ReceiveContext(c, ctx)
+	if r != 1 || e != nil {
+		t.Errorf("ReceiveContext() = %v, %v, want %v, %v", r, e, 1, nil)
+	}
+}
+
+func TestConnContext(t *testing.T) {
+	testcontext(t, contextDeadTestConn(0))
+}
+
+func TestPoolConnContext(t *testing.T) {
+	p := redis.Pool{Dial: func() (redis.Conn, error) { return contextDeadTestConn(0), nil }}
+	testcontext(t, p.Get())
+}
diff --git a/redis/reply_test.go b/redis/reply_test.go
index 8993bad..8f43828 100644
--- a/redis/reply_test.go
+++ b/redis/reply_test.go
@@ -123,6 +123,11 @@ var replyTests = []struct {
 		ve(redis.Float64(nil, nil)),
 		ve(float64(0.0), redis.ErrNil),
 	},
+	{
+		"float64Map([[]byte, []byte])",
+		ve(redis.Float64Map([]interface{}{[]byte("key1"), []byte("1.234"), []byte("key2"), []byte("5.678")}, nil)),
+		ve(map[string]float64{"key1": 1.234, "key2": 5.678}, nil),
+	},
 	{
 		"uint64(1)",
 		ve(redis.Uint64(int64(1), nil)),