本文详解 redigo 使用过程中因未校验连接错误就直接调用 con.do() 导致的 panic:“invalid memory address or nil pointer dereference”,并提供安全初始化 redis 连接池的完整实践方案。
本文详解 redigo 使用过程中因未校验连接错误就直接调用 con.do() 导致的 panic:“invalid memory address or nil pointer dereference”,并提供安全初始化 redis 连接池的完整实践方案。
在使用 Redigo 构建 Go 应用的 Redis 客户端时,一个高频且隐蔽的崩溃原因是:在未检查 redis.Dial() 返回错误的情况下,直接对可能为 nil 的连接对象执行操作(如 con.Do("SELECT", 1))。该行为会触发运行时 panic —— invalid memory address or nil pointer dereference,正如问题堆栈所示,错误精确发生在 pool.go:250 的 get() 内部调用链中,根源却在用户自定义的 Dial 函数逻辑缺陷。
? 根本原因分析
观察原始代码中的连接工厂函数:
redisPool := redis.NewPool(func() (redis.Conn, error) {
con, err := redis.Dial("tcp", *redisAddress)
con.Do("SELECT", 1) // ⚠️ 危险!此处 con 可能为 nil
if err != nil {
return nil, err
}
return con, err
}, *maxConnections)问题在于:redis.Dial() 在失败时返回 (nil, err),但代码在 if err != nil 判断之前就调用了 con.Do(...)。一旦网络不可达、Redis 未启动或地址配置错误,con 为 nil,此时调用 con.Do() 必然导致 panic。
✅ 正确顺序永远是:先检查错误 → 再使用连接。
✅ 正确写法:安全初始化连接池
以下是修复后的完整、健壮示例(含超时、密码认证、错误处理与资源清理建议):
package main
import (
"flag"
"fmt"
"log"
"time"
"github.com/garyburd/redigo/redis"
)
var (
redisAddress = flag.String("addr", "10.12.2.121:6379", "Redis server address")
maxConnections = flag.Int("max-connections", 10, "Max idle connections in pool")
redisPassword = flag.String("password", "", "Redis password (optional)")
)
func newRedisPool() *redis.Pool {
return &redis.Pool{
MaxIdle: *maxConnections,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
conn, err := redis.Dial("tcp", *redisAddress,
redis.DialConnectTimeout(5*time.Second),
redis.DialReadTimeout(5*time.Second),
redis.DialWriteTimeout(5*time.Second),
)
if err != nil {
return nil, fmt.Errorf("failed to dial Redis: %w", err)
}
// ✅ 安全:仅在 conn 非 nil 且无 err 时执行认证与 SELECT
if *redisPassword != "" {
if _, authErr := conn.Do("AUTH", *redisPassword); authErr != nil {
conn.Close() // 关闭无效连接
return nil, fmt.Errorf("Redis AUTH failed: %w", authErr)
}
}
if _, selectErr := conn.Do("SELECT", 1); selectErr != nil {
conn.Close()
return nil, fmt.Errorf("Redis SELECT db 1 failed: %w", selectErr)
}
return conn, nil
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
}
}
func main() {
flag.Parse()
pool := newRedisPool()
defer pool.Close() // 程序退出前释放连接池资源
fmt.Println("Connecting to Redis...")
conn := pool.Get()
defer conn.Close() // ✅ 获取后务必显式 Close 或 Put
if _, err := conn.Do("SET", "Name", "BookMyShow"); err != nil {
log.Fatal("SET failed:", err)
}
fmt.Println("Redis Connected and SET succeeded")
val, err := redis.String(conn.Do("GET", "Name"))
if err != nil {
log.Fatal("GET failed:", err)
}
fmt.Printf("Retrieved value: %s\n", val)
}⚠️ 关键注意事项
- defer conn.Close() 不可省略:即使使用连接池,Get() 返回的连接需手动 Close()(实际归还至池),否则将导致连接泄漏。
- TestOnBorrow 提升健壮性:定期探测空闲连接有效性,避免使用已断开的连接。
- 超时控制必不可少:生产环境必须设置 DialConnectTimeout/ReadTimeout/WriteTimeout,防止阻塞 goroutine。
- SELECT 和 AUTH 失败后必须 conn.Close():避免将无效连接放回池中,污染后续请求。
- 避免全局变量误用:确保 redisAddress 等 flag 参数在 flag.Parse() 后读取,原代码中 flag.String("10.12.2.121", ...) 的第一个参数实为 flag 名(应为 "addr"),属典型配置错误。
✅ 总结
Redigo 的 nil pointer dereference panic 绝大多数源于连接初始化阶段的错误处理缺失。牢记黄金法则:任何可能返回 nil 的操作(尤其是 Dial, Get, Do),其返回值必须在使用前完成错误校验。结合连接池配置最佳实践(超时、健康检测、资源回收),即可构建高可用、低风险的 Redis 客户端集成方案。










