背景

redis中存储着一个hash类型的键:

键名:redis:test:hash

字段名:0

字段值:0

代码中操作redis的函数定义为

Do(cmd string, args ...interface{}) (reply interface{}, err error) {
// TODO
}

 

proto文件中有如下定义

enum RoomTag
{
    matchType = 0;
}

 

看一下下面的代码

// rep预期类型是[]uint8, 转换成string是"0",实际结果rep是nil
rep, err := Do("hget", "redis:test:hash", RoomTag_matchType)
 
// 这样执行rep就符合预期了
rep, err := Do("hget", "redis:test:hash", int32(RoomTag_matchType))
 
// 这样执行rep也符合预期
rep, err := Do("hget", "redis:test:hash", 0)

 

很奇怪为什么第一个语句执行的结果不符合预期,本着打破砂锅问到底的精神,好好探询了一番。

知识点

类型别名和类型定义

从这两个概念基本也能看出来区别。类型别名就是一个类型的别名,声明一个类型别名后并没有增加类型,新声明的类型别名和原类型本质上是同一个类型,两者可以直接互相赋值,在type switch里两者也只能共用一个分支;而类型定义就是定义了一个全新的类型,它只是继承了原类型了属性,但是和原类型是完全不同的。两者在代码上的差别就是一个等于号。示例:

type NewInt int    // 我执行了类型定义,NewInt是一个全新的类型
type AliasInt = int    // 我就是声明了一个类型别名,AliasInt和int是同一个类型
 
var ni NewInt = 1
var nit int = ni    // 错误:不能把'ni'(NewInt类型)当做int类型使用
 
var ai AliasInt = 1
var ait int = ai    // 正确:我们是同一类型
 
switch ni.(type) {
case int:    // 正确:int和AliasInt均会走此分支
case NewInt:    // 正确:NewInt会走此分支
case AliasInt:    // 错误:和int是同一类型,不能重复
}

 

Stringer interface

看一下Stringer的定义

// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
   String() string
}

 

也就是说,如果一个类型实现了Stringer接口里的String方法,那么它就定义了一个'原生'的格式化方法,当一个该类型的值作为参数传递给一个未指明格式的printer(比如说Print)或者一个接受string的格式化printer时,String方法将会被调用,打印出的值即为该String方法返回的值。

type NewInt int
type NewIntS int32
 
func (n NewIntS) String() string {
   return fmt.Sprintf("%d - kidding", n + 1)
}
 
 
var ni NewInt = 1
fmt.Printf("%s\n", ni)    // %!s(main.NewInt=1)
fmt.Print(ni, "\n")    //  1
 
var nis NewIntS = 1
fmt.Printf("%s\n", nis)    // 2 - kidding
fmt.Print(nis, "\n")    // 2 - kidding

 

原因探究

了解了上面的知识点后,前面的问题就好理解了。先看一下相关的源代码。

首先是redigo/redis/conn.go里的相关代码段:

// 这个函数就是负责把用户传过来的interface{}参数写入请求中
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())
    }
}

 

然后再看一下protobuf把proto文件转换成pb文件后,枚举类型的代码

type RoomTag int32
 
const (
    RoomTag_matchType RoomTag = 0
)
 
var RoomTag_name = map[int32]string{
    0: "matchType",
}
 
var RoomTag_value = map[string]int32{
    "matchType": 0,
}
 
func (x RoomTag) String() string {
    return proto.EnumName(RoomTag_name, int32(x))
}

 

结合源代码,前面的问题原因就清晰了。RoomTag是一个新的类型定义,并不是int32的别名(PS: 就算是别名,因为switch里没有定义int32,所以最终也是进入default分支里),所以在writeArg方法里进入了default分支,然后default分支里使用了fmt.Fprint(&buf, arg)来赋值,由于RoomTag类型定义了String方法,所以fmt.Fprint使用了RoomTag.String方法的返回值,即"matchType", 所以最终发出去的hget请求等价于

Do("hget", "redis:test:hash", "matchType")

 

由于这个键中并没有matchType这个字段,所以返回了nil