【问题标题】:Decode JSON into Interface Value将 JSON 解码为接口值
【发布时间】:2016-02-17 03:42:03
【问题描述】:

由于 encoding/json 需要一个非 nil 接口来解组:我如何可靠地制作用户提供的指针类型的(完整)副本,将其存储在我的 User 接口中,然后将 JSON 解码为该类型特别指定?

注意:这里的目标是“无人值守”——即从 Redis/BoltDB 拉取字节,解码为接口类型,然后检查接口定义的 GetID() 方法是否返回非空字符串,带有请求中间件。

游乐场:http://play.golang.org/p/rYODiNrfWw

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "time"
)

type session struct {
    ID      string
    User    User
    Expires int64
}

type User interface {
    GetID() string
}

type LocalUser struct {
    ID      string
    Name    string
    Created time.Time
}

func (u *LocalUser) GetID() string {
    return u.ID
}

type Auth struct {
    key []byte
    // We store an instance of userType here so we can unmarshal into it when
    // deserializing from JSON (or other non-gob decoders) into *session.User.
    // Attempting to unmarshal into a nil session.User would otherwise fail.
    // We do this so we can unmarshal from our data-store per-request *without
    // the package user having to do so manually* in the HTTP middleware. We can't
    // rely on the user passing in an fresh instance of their User satisfying type.
    userType User
}

func main() {
    // auth is a pointer to avoid copying the struct per-request: although small
    // here, it contains a 32-byte key, options fields, etc. outside of this example.
    var auth = &Auth{key: []byte("abc")}
    local := &LocalUser{"19313", "Matt", time.Now()}

    b, _, _, err := auth.SetUser(local)
    if err != nil {
        log.Fatalf("SetUser: %v", err)
    }

    user, err := auth.GetUser(b)
    if err != nil {
        log.Fatalf("GetUser: %#v", err)
    }

    fmt.Fprintf(os.Stdout, "%v\n", user)

}

func (auth *Auth) SetUser(user User) (buf []byte, id string, expires int64, err error) {
    sess := newSession(user)

    // Shallow copy the user into our config. struct so we can copy and then unmarshal
    // into it in our middleware without requiring the user to provide it themselves
    // at the start of every request
    auth.userType = user

    b := bytes.NewBuffer(make([]byte, 0))
    err = json.NewEncoder(b).Encode(sess)
    if err != nil {
        return nil, id, expires, err
    }

    return b.Bytes(), sess.ID, sess.Expires, err
}

func (auth *Auth) GetUser(b []byte) (User, error) {
    sess := &session{}

    // Another shallow copy, which means we're re-using the same auth.userType
    // across requests (bad).
    // Also note that we need to copy into it session.User so that encoding/json
    // can unmarshal into its fields.
    sess.User = auth.userType

    err := json.NewDecoder(bytes.NewBuffer(b)).Decode(sess)
    if err != nil {
        return nil, err
    }

    return sess.User, err
}

func (auth *Auth) RequireAuth(h http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        // e.g. user, err := store.Get(r, auth.store, auth.userType)
        // This would require us to have a fresh instance of userType to unmarshal into
        // on each request.

        // Alternative might be to have:
        // func (auth *Auth) RequireAuth(userType User) func(h http.Handler) http.Handler
        // e.g. called like http.Handle("/monitor", RequireAuth(&LocalUser{})(SomeHandler)
        // ... which is clunky and using closures like that is uncommon/non-obvious.
    }

    return http.HandlerFunc(fn)
}

func newSession(u User) *session {
    return &session{
        ID:      "12345",
        User:    u,
        Expires: time.Now().Unix() + 3600,
    }
}

【问题讨论】:

  • 你能解释一下你写的第一句话中的“完整”是什么意思,你想制作一个指针类型的(完整)副本吗?
  • 一个简单的 a := auth.userType 只是一个浅拷贝;它仍然指向相同的基础价值。
  • 你需要一个新的零值来解码,还是你真的需要一个副本到某个级别(底层值的浅拷贝,整个值的深拷贝,...)?
  • 解码的零值(这样 encoding/json 对 reflect.TypeOf 的使用允许它解码之前存储的副本)。

标签: go


【解决方案1】:

如果您需要深度复制接口,请将该方法添加到您的接口中。

type User interface {
  GetID() string
  Copy() User
}

type LocalUser struct {
  ID string
  Name string
  Created time.Time
}

// Copy receives a copy of LocalUser and returns a pointer to it.
func (u LocalUser) Copy() User {
  return &u
}

【讨论】:

  • 谢谢。我试图保持界面小,最终用户正确使用(实际上返回一个指针)需要清晰的文档,但我更喜欢界面来反映魔法(我认为我们大多数人也是如此)。
  • @elithrar 我有很多机会通过反射来减少 LOC。我认为反射的最佳用途是它实际上可以降低复杂性,而不是 LOC 或冗长。
【解决方案2】:

因为应用程序将解码为User,并且JSON 解码器的参数必须是指针值,我们可以假设User 值是指针值。鉴于此假设,以下代码可用于为解码创建新的零值:

uzero := reflect.New(reflect.TypeOf(u).Elem()).Interface().(User)

playground example

【讨论】:

  • 虽然出于“API 表面积”的原因我更喜欢这个,但我(像很多人一样!)想尽可能地避免反射。不过,这是解决问题的好方法,所以谢谢!
  • 为什么要避免反射包?如果关注性能,那么您可能也应该避免使用 encoding/json。
  • encoding/json 对于一个合理的不干涉序列化程序来说仍然“足够快”。特别是如果你的类型是兼容的,与 encoding/gob 相比。 msgpack 等序列化程序。 al 不是(默认)选项,因为它们需要(至少)实现非平凡的接口方法或代码生成。这对于可选的编码器来说完全没问题,但在大多数情况下,瓶颈不是编码/json,而是 RTT 到 Redis/Postgres/等。我的库。支持自定义编码器,但需要方便的默认值。它在与securecookie类似的上下文中使用。
  • 好的,性能不是问题。您对使用反射包有何反对?
  • 如果有更惯用的(即非运行时)方法来解决问题,包括我自己在内的许多 Gophers 更愿意避免使用反射。 not 以保存反射没有用(没有它就不会存在序列化库、gorilla/schema、sqlx 等)。旨在解决与我不同的问题的人可以使用它。正如我在对另一个答案的评论中所说的那样, 使用反射的代价是对正确实现该接口方法的包用户有一定程度的依赖(反射可以避免这种情况)。跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-28
  • 2021-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-16
相关资源
最近更新 更多