使用Golang、Redis和时间进行测试

huangapple go评论85阅读模式
英文:

Testing with Golang, redis and time

问题

我第一次尝试使用Redis进行测试,并在HGET/HSET/HGETALL方面遇到了一些困惑。我的主要问题是我需要存储时间,并且我希望使用哈希表,因为我将不断更新时间。

起初,我了解到一个名为MarshalBinary的函数可以帮助我:

func (f Foo) MarshalBinary() ([]byte, error) {
	return json.Marshal(f)
}

这个函数将结构体保存为JSON字符串,但只是字符串,而不是实际的Redis哈希表。最终,我编写了一段相当冗长的代码,将要保存的结构体转换为一个映射,然后将该映射作为哈希表存储在Redis中。

type Foo struct {
	Number int       `json:"number"`
	ATime  time.Time `json:"atime"`
	String string    `json:"astring"`
}

func (f Foo) toRedis() map[string]interface{} {
	res := make(map[string]interface{})
	rt := reflect.TypeOf(f)
	rv := reflect.ValueOf(f)
	if rt.Kind() == reflect.Ptr {
		rt = rt.Elem()
		rv = rv.Elem()
	}
	for i := 0; i < rt.NumField(); i++ {
		f := rt.Field(i)
		v := rv.Field(i)
		switch t := v.Interface().(type) {
		case time.Time:
			res[f.Tag.Get("json")] = t.Format(time.RFC3339)
		default:
			res[f.Tag.Get("json")] = t
		}
	}
	return res
}

然后,在调用HGetAll(..).Result()时,将结果作为map[string]string获取,并使用以下函数将其解析回我的Foo结构体:

func setRequestParam(arg *Foo, i int, value interface{}) {
	v := reflect.ValueOf(arg).Elem()
	f := v.Field(i)
	if f.IsValid() {
		if f.CanSet() {
			if f.Kind() == reflect.String {
				f.SetString(value.(string))
				return
			} else if f.Kind() == reflect.Int {
				f.Set(reflect.ValueOf(value))
				return
			} else if f.Kind() == reflect.Struct {
				f.Set(reflect.ValueOf(value))
			}
		}
	}
}

func fromRedis(data map[string]string) (f Foo) {
	rt := reflect.TypeOf(f)
	rv := reflect.ValueOf(f)

	for i := 0; i < rt.NumField(); i++ {
		field := rt.Field(i)
		v := rv.Field(i)
		switch v.Interface().(type) {
		case time.Time:
			if val, ok := data[field.Tag.Get("json")]; ok {
				if ti, err := time.Parse(time.RFC3339, val); err == nil {
					setRequestParam(&f, i, ti)
				}
			}
		case int:
			if val, ok := data[field.Tag.Get("json")]; ok {
				in, _ := strconv.ParseInt(val, 10, 32)
				setRequestParam(&f, i, int(in))

			}
		default:
			if val, ok := data[field.Tag.Get("json")]; ok {
				setRequestParam(&f, i, val)
			}
		}
	}
	return
}

完整的代码可以在这里找到。

我在想是否有更简洁的方法来解决这个问题?还是我必须像上面那样做?我需要存储的结构体只包含整数、字符串和time.Time类型。

*编辑
评论字段有点短,所以我进行了编辑:

我最初是按照评论和答案中"The Fool"建议的方式解决的。我之所以改用上面的方法,虽然解决方案更复杂,但我认为它对于变化更加健壮。如果我选择硬编码的映射解决方案,我将不得不:

  • 为字段创建哈希键的常量,因为它们将在至少两个地方使用(从Redis和到Redis),这将是一个容易出错但编译器无法检测到的地方。当然可以跳过这一步,但考虑到我的拼写能力,可能会发生错误。
  • 如果有人想要添加一个新字段,但对代码不太熟悉,它将能够编译通过,但新字段将不会添加到Redis中。这是一个容易犯的错误,尤其是对于天真的初级开发人员或过于自信的高级开发人员。
  • 我可以将这些辅助函数放在一个库中,当需要时间或复杂类型时,所有的代码都会自动工作。

我想问的问题是:我真的需要像这样费力地将时间存储在Redis哈希表中吗?确实,time.Time不是一个原始类型,Redis也不是一个(非)SQL数据库,但我认为在缓存中使用时间戳是一个非常常见的用例(在我的情况下,是用于跟踪超时会话的心跳,以及足够存储它的元数据,因此需要更新它们)。但也许我误用了Redis,我应该为数据和时间戳分别使用两个条目,然后只需编写两个简单的get/set函数,接受time.Time并返回time.Time。

英文:

I was trying to test a bit with Redis for the first time and I bumped into some confusion with HGET/HSET/HGETALL. My main problem was that I needed to store time, and I wanted to use a hash as I'll continuously update the time.

At first I read about how a MarshalBinary function such as this would save me:

func (f Foo) MarshalBinary() ([]byte, error) {
	return json.Marshal(f)
}

What that did was that it saved the struct as a json string, but only as a string and not as an actual Redis hash. What I ended up doing in the end was a fairly large boilerplate code that makes my struct I want to save into a map, and that one is properly stored as a hash in Redis.

type Foo struct {
	Number int       `json:&quot;number&quot;`
	ATime  time.Time `json:&quot;atime&quot;`
	String string    `json:&quot;astring&quot;`
}

func (f Foo) toRedis() map[string]interface{} {
	res := make(map[string]interface{})
	rt := reflect.TypeOf(f)
	rv := reflect.ValueOf(f)
	if rt.Kind() == reflect.Ptr {
		rt = rt.Elem()
		rv = rv.Elem()
	}
	for i := 0; i &lt; rt.NumField(); i++ {
		f := rt.Field(i)
		v := rv.Field(i)
		switch t := v.Interface().(type) {
		case time.Time:
			res[f.Tag.Get(&quot;json&quot;)] = t.Format(time.RFC3339)
		default:
			res[f.Tag.Get(&quot;json&quot;)] = t
		}
	}
	return res
}

Then to parse back into my Foo struct when calling HGetAll(..).Result(), I'm getting the result as a map[string]string and create a new Foo with these functions:

func setRequestParam(arg *Foo, i int, value interface{}) {
	v := reflect.ValueOf(arg).Elem()
	f := v.Field(i)
	if f.IsValid() {
		if f.CanSet() {
			if f.Kind() == reflect.String {
				f.SetString(value.(string))
				return
			} else if f.Kind() == reflect.Int {
				f.Set(reflect.ValueOf(value))
				return
			} else if f.Kind() == reflect.Struct {
				f.Set(reflect.ValueOf(value))
			}
		}
	}
}

func fromRedis(data map[string]string) (f Foo) {
	rt := reflect.TypeOf(f)
	rv := reflect.ValueOf(f)

	for i := 0; i &lt; rt.NumField(); i++ {
		field := rt.Field(i)
		v := rv.Field(i)
		switch v.Interface().(type) {
		case time.Time:
			if val, ok := data[field.Tag.Get(&quot;json&quot;)]; ok {
				if ti, err := time.Parse(time.RFC3339, val); err == nil {
					setRequestParam(&amp;f, i, ti)
				}
			}
		case int:
			if val, ok := data[field.Tag.Get(&quot;json&quot;)]; ok {
				in, _ := strconv.ParseInt(val, 10, 32)
				setRequestParam(&amp;f, i, int(in))

			}
		default:
			if val, ok := data[field.Tag.Get(&quot;json&quot;)]; ok {
				setRequestParam(&amp;f, i, val)
			}
		}
	}
	return
}

The whole code in its ungloryness is here

I'm thinking that there must be a saner way to solve this problem? Or am I forced to do something like this? The struct I need to store only contains ints, strings and time.Times.

*edit
The comment field is a bit short so doing an edit instead:

I did originally solve it like 'The Fool' suggested in comments and as an answer. The reason I changed to the above part, while more complex a solution, I think it's more robust for changes. If I go with a hard coded map solution, I'd "have to" have:

  • Constants with hash keys for the fields, since they'll be used at least in two places (from and to Redis), it'll be a place for silly mistakes not picked up by the compiler. Can of course skip that but knowing my own spelling it's likely to happen
  • If someone just wants to add a new field and doesn't know the code well, it will compile just fine but the new field won't be added in Redis. An easy mistake to do, especially for junior developers being a bit naive, or seniors with too much confidence.
  • I can put these helper functions in a library, and things will just magically work for all our code when a time or complex type is needed.

My intended question/hope though was: Do I really have to jump through hoops like this to store time in Redis hashes with go? Fair, time.Time isn't a primitive and Redis isn't a (no)sql database, but I would consider timestamps in cache a very common use case (in my case a heartbeat to keep track of timed out sessions together with metadata enough to permanently store it, thus the need to update them). But maybe I'm misusing Redis, and I should rather have two entries, one for the data and one for the timestamp, which would then leave me with two simple get/set functions taking in time.Time and returning time.Time.

答案1

得分: 0

你可以使用redigo/redis#Args.AddFlat将结构体转换为Redis哈希表,可以使用redis标签映射值。

package main

import (
  "fmt"
  "time"
  "github.com/gomodule/redigo/redis"
)

type Foo struct {
    Number  int64     `json:"number"  redis:"number"`
    ATime   time.Time `json:"atime"   redis:"atime"`
    AString string    `json:"astring" redis:"astring"`
}

func main() {
  c, err := redis.Dial("tcp", ":6379")
  if err != nil {
    fmt.Println(err)
    return
  }
  defer c.Close()

  t1 := time.Now().UTC()
  var foo Foo
  foo.Number = 10000000000
  foo.ATime = t1
  foo.AString = "Hello"

  tmp := redis.Args{}.Add("id1").AddFlat(&foo)
  if _, err := c.Do("HMSET", tmp...); err != nil {
    fmt.Println(err)
    return
  }

  v, err := redis.StringMap(c.Do("HGETALL", "id1"))
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf("%#v\n", v)
}

然后,要更新ATime,可以使用Redis的HSET命令:

if _, err := c.Do("HMSET", "id1", "atime", t1.Add(-time.Hour*(60*60*24))); err != nil {
  fmt.Println(err)
  return
}

要将其检索回结构体,我们需要使用一些reflect魔法:

func structFromMap(src map[string]string, dst interface{}) error {
  dt := reflect.TypeOf(dst).Elem()
  dv := reflect.ValueOf(dst).Elem()

  for i := 0; i < dt.NumField(); i++ {
    sf := dt.Field(i)
    sv := dv.Field(i)
    if v, ok := src[strings.ToLower(sf.Name)]; ok {
      switch sv.Interface().(type) {
        case time.Time:
          format := "2006-01-02 15:04:05 -0700 MST"
          ti, err := time.Parse(format, v)
          if err != nil {
            return err
          }
          sv.Set(reflect.ValueOf(ti))
        case int, int64:
          x, err := strconv.ParseInt(v, 10, sv.Type().Bits())
          if err != nil {
            return err
          }
          sv.SetInt(x)
        default:
          sv.SetString(v)
      }
    }
  }

  return nil
}

最终代码

package main

import (
  "fmt"
  "time"
  "reflect"
  "strings"
  "strconv"
  "github.com/gomodule/redigo/redis"
)

type Foo struct {
    Number  int64     `json:"number"  redis:"number"`
    ATime   time.Time `json:"atime"   redis:"atime"`
    AString string    `json:"astring" redis:"astring"`
}

func main() {
  c, err := redis.Dial("tcp", ":6379")
  if err != nil {
    fmt.Println(err)
    return
  }
  defer c.Close()

  t1 := time.Now().UTC()
  var foo Foo
  foo.Number = 10000000000
  foo.ATime = t1
  foo.AString = "Hello"

  tmp := redis.Args{}.Add("id1").AddFlat(&foo)
  if _, err := c.Do("HMSET", tmp...); err != nil {
    fmt.Println(err)
    return
  }

  v, err := redis.StringMap(c.Do("HGETALL", "id1"))
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf("%#v\n", v)

  if _, err := c.Do("HMSET", "id1", "atime", t1.Add(-time.Hour*(60*60*24))); err != nil {
    fmt.Println(err)
    return
  }

  var foo2 Foo
  structFromMap(v, &foo2)
  fmt.Printf("%#v\n", foo2)
}

func structFromMap(src map[string]string, dst interface{}) error {
  dt := reflect.TypeOf(dst).Elem()
  dv := reflect.ValueOf(dst).Elem()

  for i := 0; i < dt.NumField(); i++ {
    sf := dt.Field(i)
    sv := dv.Field(i)
    if v, ok := src[strings.ToLower(sf.Name)]; ok {
      switch sv.Interface().(type) {
        case time.Time:
          format := "2006-01-02 15:04:05 -0700 MST"
          ti, err := time.Parse(format, v)
          if err != nil {
            return err
          }
          sv.Set(reflect.ValueOf(ti))
        case int, int64:
          x, err := strconv.ParseInt(v, 10, sv.Type().Bits())
          if err != nil {
            return err
          }
          sv.SetInt(x)
        default:
          sv.SetString(v)
      }
    }
  }

  return nil
}

注意: 结构体字段名与redis标签匹配

英文:

You can use redigo/redis#Args.AddFlat to convert struct to redis hash we can map the value using redis tag.

package main

import (
  &quot;fmt&quot;

  &quot;time&quot;
  &quot;github.com/gomodule/redigo/redis&quot;
)

type Foo struct {
    Number  int64     `json:&quot;number&quot;  redis:&quot;number&quot;`
    ATime   time.Time `json:&quot;atime&quot;   redis:&quot;atime&quot;`
    AString string    `json:&quot;astring&quot; redis:&quot;astring&quot;`
}

func main() {
  c, err := redis.Dial(&quot;tcp&quot;, &quot;:6379&quot;)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer c.Close()

  t1 := time.Now().UTC()
  var foo Foo
  foo.Number = 10000000000
  foo.ATime = t1
  foo.AString = &quot;Hello&quot;

  tmp := redis.Args{}.Add(&quot;id1&quot;).AddFlat(&amp;foo)
  if _, err := c.Do(&quot;HMSET&quot;, tmp...); err != nil {
    fmt.Println(err)
    return
  }

  v, err := redis.StringMap(c.Do(&quot;HGETALL&quot;, &quot;id1&quot;))
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf(&quot;%#v\n&quot;, v)
}

Then to update ATime you can use redis HSET

if _, err := c.Do(&quot;HMSET&quot;, &quot;id1&quot;, &quot;atime&quot;, t1.Add(-time.Hour * (60 * 60 * 24))); err != nil {
  fmt.Println(err)
  return
}

And to retrieve it back to struct we have to do some reflect magic

func structFromMap(src map[string]string, dst interface{}) error {
  dt := reflect.TypeOf(dst).Elem()
  dv := reflect.ValueOf(dst).Elem()

  for i := 0; i &lt; dt.NumField(); i++ {
    sf := dt.Field(i)
    sv := dv.Field(i)
    if v, ok := src[strings.ToLower(sf.Name)]; ok {
      switch sv.Interface().(type) {
        case time.Time:
          format := &quot;2006-01-02 15:04:05 -0700 MST&quot;
          ti, err := time.Parse(format, v)
          if err != nil {
            return err
          }
          sv.Set(reflect.ValueOf(ti))
        case int, int64:
          x, err := strconv.ParseInt(v, 10, sv.Type().Bits())
          if err != nil {
            return err
          }
          sv.SetInt(x)
        default:
          sv.SetString(v)
      }
    }
  }

  return nil
}

Final Code

package main

import (
  &quot;fmt&quot;

  &quot;time&quot;
  &quot;reflect&quot;
  &quot;strings&quot;
  &quot;strconv&quot;

  &quot;github.com/gomodule/redigo/redis&quot;
)

type Foo struct {
    Number  int64     `json:&quot;number&quot;  redis:&quot;number&quot;`
    ATime   time.Time `json:&quot;atime&quot;   redis:&quot;atime&quot;`
    AString string    `json:&quot;astring&quot; redis:&quot;astring&quot;`
}

func main() {
  c, err := redis.Dial(&quot;tcp&quot;, &quot;:6379&quot;)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer c.Close()

  t1 := time.Now().UTC()
  var foo Foo
  foo.Number = 10000000000
  foo.ATime = t1
  foo.AString = &quot;Hello&quot;

  tmp := redis.Args{}.Add(&quot;id1&quot;).AddFlat(&amp;foo)
  if _, err := c.Do(&quot;HMSET&quot;, tmp...); err != nil {
    fmt.Println(err)
    return
  }

  v, err := redis.StringMap(c.Do(&quot;HGETALL&quot;, &quot;id1&quot;))
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf(&quot;%#v\n&quot;, v)

  if _, err := c.Do(&quot;HMSET&quot;, &quot;id1&quot;, &quot;atime&quot;, t1.Add(-time.Hour * (60 * 60 * 24))); err != nil {
    fmt.Println(err)
    return
  }

  var foo2 Foo
  structFromMap(v, &amp;foo2)
  fmt.Printf(&quot;%#v\n&quot;, foo2)
}

func structFromMap(src map[string]string, dst interface{}) error {
  dt := reflect.TypeOf(dst).Elem()
  dv := reflect.ValueOf(dst).Elem()

  for i := 0; i &lt; dt.NumField(); i++ {
    sf := dt.Field(i)
    sv := dv.Field(i)
    if v, ok := src[strings.ToLower(sf.Name)]; ok {
      switch sv.Interface().(type) {
        case time.Time:
          format := &quot;2006-01-02 15:04:05 -0700 MST&quot;
          ti, err := time.Parse(format, v)
          if err != nil {
            return err
          }
          sv.Set(reflect.ValueOf(ti))
        case int, int64:
          x, err := strconv.ParseInt(v, 10, sv.Type().Bits())
          if err != nil {
            return err
          }
          sv.SetInt(x)
        default:
          sv.SetString(v)
      }
    }
  }

  return nil
}

Note: The struct field name is matched with the redis tag

huangapple
  • 本文由 发表于 2022年4月24日 02:45:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/71982698.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定