登录
首页 >  Golang >  Go问答

"我能复制golang中具有不确定类型标签的接口指针吗?"

来源:stackoverflow

时间:2024-03-23 13:09:38 386浏览 收藏

在 Go 中,无法直接复制具有未知类型标签的接口指针的值。然而,有几种方法可以实现类似的功能: * **使用自定义编组器:**创建一个自定义编组器类型,它可以处理所有需要审查的类型。在编组器中实现一个方法来审查敏感字段。 * **使用泛型(Go 1.18 及更高版本):**创建泛型函数,该函数接受一个接口指针作为参数并实现指针接收器方法来审查敏感字段。 * **使用嵌入类型:**创建一个嵌入敏感字段类型的类型。在主类型中实现一个方法来审查敏感字段,该方法将调用嵌入类型的审查方法。

问题内容

我可以复制golang中类型未知的标签的接口指针的值吗

我有一个名为“censorpayload”的函数,它将接收一个指针并解组并审查一些具有名为“censored”标签的字段,但我不想更新名为“target”的参数中的值,这意味着我必须复制新接口{}的目标值。我尝试了很多方法,例如reflect.new,但是标签没有通过unmarshal或do函数。

type HelloWorld struct {
    Hello string `json:"hello"`
    Password string `json:"password" censored:"*****"`
}

a := HelloWorld{}

CensorPayload(&a, `{"hello": "world", "password": "12345"}`)

func CensorPayload(target interface{}, payload string) (string, error) {
    err := json.Unmarshal([]byte(payload), target)
    if err != nil {
        return "", err
    }
    err = Do(target)
    if err != nil {
        return "", err
    }
    censoredPayload, err := json.Marshal(target)
    if err != nil {
        return "", err
    }
    return string(censoredPayload), nil
}

正确答案


有很多方法可以改进和实现这一点。我将从一些改进开始,然后逐步实现您正在寻找的内容。

改进

首先,我会避免使用 target 接口{} 作为参数。绝对没有任何东西可以保证调用者将传入指针。至少,我会考虑使用 protobuf 用来确保接口 proto.message 的参数是指针值的技巧。这是通过向类型添加带有指针接收器的空方法来完成的。在这种情况下,这将是:

type jsonptr interface {
    jsonptr()
}

// implemented:
func (*helloworld) jsonptr() {} // empty

func censorpayload(target jsonptr, payload string) (string, error) {
    // pretty much the same as you have here
}

另一种选择是使用泛型,具体取决于您实际需要处理的类型数量,这可能是它们的有效用例:

type censortypes interface {
    *helloworld | *anothertype | *yetanother
}

func censorpayload[t censortypes](target t, payload string) (string, error) {
    // same as what you have now
}

通过嵌入减少重复

如果您有多种带有 password 字段的类型,所有这些类型都需要能够被审查,您可以将 password 字段拆分为自己的类型并将其嵌入到需要的位置:

type password struct {
    password string `json:"password"`
}
type helloworld struct {
    // regular fields here
    password // embed the password field
}

如果您需要经常使用此密码审查,我肯定会采用这种方法。最后,我将提供一个示例,其中我使用嵌入类型来完成您尝试以最少的代码重复完成的任务...

可能的实现

然而,这一切都是为了确保传递一个指针,无论如何接口方法已经做到了这一点。您希望能够审查密码,并在代码中进行特定的调用来执行此操作,那么为什么不在相关类型上实现方法,并将其添加到接口中。添加泛型,我们可以进行编译时检查,以确保相关方法正确实现(即使用指针接收器):

type censored interface {
    *helloworld | *anothertype // and so on
    censorpass()
}

func (h *helloworld) censorpass() { // pointer receiver
    h.password = "******"
}
// in case `password` is an embedded type, no need to implement it on `helloworld`
func (p *password) censorpass() {
    p.password = "******"
}

func censsorpayload[t censored](target t, payload string) (string, error) {
    // same as before until:
    // do() is replaced with:
    target.censorpass()
    // the rest is the same
}

现在,如果您的 helloworld 类型使用值接收器 (func (h helloworld) censorpass) 实现 censorpass,或者调用者无法传入指针值作为 target 参数,您将收到错误。

选项 2:自定义编组器

这里的另一个选择是创建一个包装类型,嵌入您需要处理的所有类型,并在所述类型上实现自定义编组器接口:

type wrapper struct {
    *helloworld
    *anothertype
    *yestanother
}

func (w wrapper) marshaljson() ([]byte, error) {
    if w.helloworld != nil {
        w.helloworld.password = "*****"
        return json.marshal(w.helloworld)
    }
    if w.anothertype != nil {
        w.anothertype.pass = "*****"
        w.anothertype.anothersensitivefield = "*******"
        return json.marshal(w.anothertype)
    }
    func w.yetanother != nil {
        w.yetanother.user = ""
        return json.marshal(w.yetanother)
    }
}

填充此 wrapper 类型都可以通过单个函数和类型开关来处理。我正在使用 any (下面是 interface{} 的缩写,但如果您想从其他包调用此方法,如果您单独从 censorpayload 调用它,我建议您出于上面给出的原因使用接口或泛型,并且您已经将其更改为使用接口/泛型,下面的代码就可以了:

func wrap(target any) (*wrapper, error) {
    switch tt := target.(type) {
    case *helloworld:
        return &wrapper{
            helloworld: tt,
        }, nil
    case *anothertype:
        return &wrapper{
            anothertype: tt,
        }, nil
    case *yetanother:
        return &wrapper{
            yetanother: tt,
        }, nil
    }
    // if we reach this point, the type passed to this function is unknown/can't be censored
    return nil, errors.new("type not supported by wrapper")
}

完成此操作后,您只需将 censorpayload 函数更改为如下所示:

// censorpayload using interface to ensure pointer argument
func censorpayload(target jsonptr, payload string) (string, error) {
     if err := json.unmarshal(target, []byte(payload)); err != nil {
         return "", err
     }
     wrapped, err := wrap(target)
     if err != nil {
         // type not handled by wrapper, either return an error
         return "", err // target was not supported
         // or in case that means this data should not be censored, just return the marhsalled raw data:
        raw, err := json.marshal(target)
        if err != nil {
            return "", err
        }
        return string(raw), nil
    }
    raw, err := json.marshal(wrapped) // this will call wrapper.marshaljson and censor what needs to be censored
    if err != nil {
        return "", err
    }
    return string(raw), nil
}

我想要什么

我可能会在 censorpayload 函数中使用 censored 类型。它的好处是根本不需要包装器,将了解要审查哪些字段/值的责任转移到数据类型,并确保 censorpass 方法正确实现(指针接收器),并且实际值是传递的是一个指针。为了将总 loc 降至最低,我也将其留给调用者将 []byte 返回值转换为字符串。 payload 参数也是如此。原始 json 数据表示为字节切片,因此 payload 参数(imo)应该反映这一点:

func censorpayload[t censored](target t, payload []byte) ([]byte, error) {
    if err := json.unmarshal(target, payload); err != nil {
        return nil, err
    }
    target.censorpass() // type knows what fields to censor
    return json.marshal(target) // return the data censored & marshalled
}

如上所述,我会将这种方法与嵌入类型的常见审查字段结合起来:

type password struct {
    password `json:"password"`
}
type anothersensitivefield struct {
    anothersensitivefield string `json:"example_db_ip"`
}
// password implements censorpass, is embedded so we don't need to do anything here
type helloworld struct {
    // fields
    password
}

// this one has 2 fields that implement censorpass
// we need to ensure both are called
type anothertype struct {
    // fields
    password
    anothersensitivefield
}

// this is a one-off, so we need to implement censorpass on the type
type yetanother struct {
   // all fields - no password
    user string `json:"user"` // this one needs to be censored
}

func (a *anothertype) censorpass() {
    a.password.censorpass()
    a.anothersensitivefield.censorpass()
}

func (y *yetanother) censorpass() {
    y.user = ""
}

func (p *password) censorpass() {
    p.password = "******"
}

func (a *anothersensitivefield) censorpass() {
    a.anothersensitivefield = "******"
}

任何嵌入一种类型(如 password)的类型都会自动具有 censorpass 方法。这些类型是嵌入的,并且没有其他嵌入类型有名称冲突,因此顶级类型可以将 helloworld.censorpass 解析为 helloworld.password.censorpass()。这不适用于 anothertype,因为它有 2 个提供此方法的嵌入类型。为了解决这个问题,我们只需在顶层实现该方法,并将调用传递给嵌入类型。第三个示例是我们想要审查尚未在其他任何地方使用的特定字段的示例。不需要创建单独的嵌入类型,因此我们可以直接在 yetanother 类型上实现该方法。如果我们有存在用户凭据的数据,并且我们希望审查不同位置的多个字段,我们可以轻松地为此创建一个类型:

type credentials struct {
    user string `json:"user"`
    pass string `json:"pass"`
}
type fullname struct {
    name  string `json:"name"`
    first string `json:"first_name"`
}

func (c *credentials) censorpass() {
    c.user = ""
    c.pass = "******"
}

func (f *fullname) censorpass() {
    if len(f.first) > 0 {
        f.first = string([]rune(f.first)[0])
    }
    if len(f.last) == 0 {
        return
    }
    last := make([]string, 0, 2)
    for _, v := range strings.split(f.last) {
        if len(v) > 0 {
            last = append(last, string([]rune(v)[0]))
        }
    }
    last = append(last, "") // for the final .
    f.last = strings.join(last, ". ") // initials only
}

我们所要做的就是将这种类型嵌入到我们需要的地方。完成后,我们的主要功能仍然如下所示:

func censorpayload[t censored](target t, payload []byte) ([]byte, error) {
    if err := json.unmarshal(target, payload); err != nil {
        return nil, err
    }
    target.censorpass() // type knows what fields to censor
    return json.marshal(target) // return the data censored & marshalled
}

但是现在,我们可以非常轻松地向此 censored 约束添加新类型:

type login struct {
    credentials // username pass
    fullname
}
type person struct {
    fullname
    status   string `json:"status"`
}
func (n *newtype) censorpass() {
    n.credentials.censorpass()
    n.fullname.censorpass()
}

通过这几行代码,我们现在还有 2 个类型可以传递到 censorpayload 函数(当然,通过更新约束)。给定 loginperson 有效负载,例如:

// login
{
    "user": "mylogin",
    "pass": "weakpass",
    "name": "pothering smythe",
    "first_name": "emanual",
}
// person
{
    "name": "doe",
    "first_name": "john",
    "status": "missing",
}

我们应该得到输出:

{
    "user": "",
    "pass": "******",
    "name": "P. S. "
    "first_name": "E",
}
{
    "name": "D. ",
    "first_name": "J",
    "status": "missing",
}

注意:

我已经编写了上面的所有代码,但没有先对其进行测试。可能会有拼写错误和错误,但它应该包含足够的有用信息和示例来帮助您入门。如果我有空闲时间,我可能会尝试一下并在需要时更新片段

文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《"我能复制golang中具有不确定类型标签的接口指针吗?"》文章吧,也可关注golang学习网公众号了解相关技术文章。

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>