golang 实现 deepcopy 的两种实现方式

简介: golang 实现 deepcopy 的两种实现方式

golang deepcopy 的两种实现方式

最近在基于 gin 封装 rum-gonic - github web 框架的过程中,遇到了一个问题。

在注册路由的时候传递是 指针对象, 因此造成所有的 request 请求使用相同的 CreateUser 对象, 出现并发冲突。

func init() {
    RouterGroup_User.Register(&CreateUser{})
}

type CreateUser struct {
    httpx.MethodPost `path:""`
    Name     string `query:"name"`
    Password string `query:"password"`
}

struct 结构体 deepcopy 的实现

基于 sturct 的实现, 由于有 明确struct 对象结构, 通常直接创建一个全新对象, 同时把老数据复制进去。

例如 gin-gonic 的 Copy() 方法

// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
    cp := Context{
        writermem: c.writermem,
        Request:   c.Request,
        Params:    c.Params,
        engine:    c.engine,
    }
    cp.writermem.ResponseWriter = nil
    cp.Writer = &cp.writermem
    cp.index = abortIndex
    cp.handlers = nil
    cp.Keys = map[string]interface{}{}
    for k, v := range c.Keys {
        cp.Keys[k] = v
    }
    paramCopy := make([]Param, len(cp.Params))
    copy(paramCopy, cp.Params)
    cp.Params = paramCopy
    return &cp
}

interface 接口 deepcopy 的实现

对于 接口 interface{} 就稍微麻烦一点了。 由于 接口 是一组方法的集合, 也就意味着

  1. 接口的 底层结构体 是不定的。
  2. 无法直接获取 底层结构体 的字段数据。

这时可以通过使用 反射 reflect.New() 创建对象。

mohae/deepcopy - github 就是使用的这种方式

deepcopy 库中一样通过 反射递归 实现复制, 是为了兼容更多的情况。
而在自己实现编码的时候, 大部分情况的是可控的, 实现方式可以适当简化, 不用与 deepcopy 完全相同。

1. 通过反射创建零值接口对象

func deepcoper(op Operator) Operator {
    // 1. 获取 反射类型
    rt := reflect.TypeOf(op)

    // 2. 获取真实底层结构体的类型
    rtype := deRefType(rt)

    // 3. reflect.New() 创建反射对象,并使用 Interface() 转真实对象
    opc := reflect.New(rtype).Interface()

    // 4. 断言为 operator
    return opc.(Operator)
}

func deRefType(typ reflect.Type) reflect.Type {
    for typ.Kind() == reflect.Ptr {
        typ = typ.Elem()
    }

    return typ
}

需要注意的是, 通过上述方式创建的出来的新对象

  1. 依然 不知道新接口底层结构体是什么, 也并不需要关心, 接口中心本身就是在 相同的方法实现 上。
  2. 接口底层结构体中所有字段值为 零值, 可能需要必要的初始化,否则直接使用可能 panic, 例如结构体中存在 指针类型对象

通常这种情况, 在自己写代码的时候,可以增加一个 初始化方法

2. 使用接口断言进行初始化

在实现了初始化方法之后, 可以再定义一个接口。 通过断言转化为新接口, 调用初始化方法。

func deepcoper(op Operator) Operator {
    rt := reflect.TypeOf(op)

    rtype := deRefType(rt)

    opc := reflect.New(rtype).Interface()

    // 3.1. 使用断言转化新接口, 初始化底层对象
    if opcInit, ok := opc.(OperatorIniter); ok {
        opcInit.SetDefaults()
    }

    return opc.(Operator)
}

type OperatorIniter interface {
    SetDefaults()
}

3. 使用反射调用方法进行初始化

在不增加新接口的情况下, 可以在反射创建的过程中 判断初始化方法的存在, 并调用 进行初始化。


func deepcoper(op Operator) Operator {
    rt := reflect.TypeOf(op)
    rtype := deRefType(rt)
    ropc := reflect.New(rtype)

    // 3.2 使用反射 call method 初始化
    method := ropc.MethodByName("SetDefaults")
    if method.IsValid() && !method.IsZero() {
        method.Call(nil)
    }

    opc := ropc.Interface()
    return opc.(Operator)
}

搞点看起来高级的: 接口初始化工厂

上述代码中都是直接对接口对象进行的操作。 搞一个 struct 创建并初始化接口, 可以携带和组织更多的信息。

func NewOperatorFactory(op Operator) *OperatorFactory {
    opfact := &OperatorFactory{}

    opfact.Type = deRefType(reflect.TypeOf(op))
    // opfact.Operator = op
    
    return opfact
}

type OperatorFactory struct {
    Type     reflect.Type
    Operator Operator
}

func (o *OperatorFactory) New() Operator {

    oc := reflect.New(o.Type).Interface().(Operator)

    return oc
}
相关文章
|
4月前
|
Go
Golang切片copy踩坑
Golang切片copy踩坑
27 0
|
4月前
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
34 1
|
7天前
|
Go
golang中置new()函数和make()函数的区别
golang中置new()函数和make()函数的区别
|
2月前
|
Go 数据安全/隐私保护
第九章 Golang中map
第九章 Golang中map
25 2
|
9月前
|
存储 Go C++
Golang map
Golang map
56 0
|
11月前
|
运维 Go
学习golang(5) 初探:go 数组/slice 的基本使用
学习golang(5) 初探:go 数组/slice 的基本使用
93 0
|
11月前
|
运维 小程序 Go
学习golang(2) 初探:go map 基本使用
学习golang(2) 初探:go map 基本使用
81 0
|
12月前
|
存储 缓存 安全
Golang中map基础
Golang中map基础
81 0
|
12月前
|
Go
Golang中函数的使用
Golang中函数的使用
59 0
Golang:deepcopy深拷贝工具库
Golang:deepcopy深拷贝工具库
443 0