背景
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