前言 / 背景

公司12月推出了一个新的端到端OCR模型,所以Python实现的预处理和后处理步骤就直接被消灭了。不过模型给出的结果其实也是需要后处理才能正常使用的,只是旧模型的后处理还需要shapely、cv2等库,新的端到端模型只需要做一些什么语言都能实现的简单工作。

不过就算这样,要在Golang做深度学习的后处理也是有够难的,我封装和抽象了很多矩阵对象,才让代码不至于变得完全无法维护。其中调用最频繁的方法应该是 Copy() 了。后处理操作中难免遇到需要避免修改原始矩阵的地方,所以深拷贝几乎是家常便饭的事情。

正文

什么是深拷贝?

上代码:

    var a = []int{10, 9, 8}
    var b = a
    a[1] = 2
    fmt.Println(a[1], b[1])

在这里,我创建了一个长度为3的切片,并将其赋值给变量 a 。其内容被初始化为 [10, 9, 8] ,也就是这个对象里面包含着三个0。这个时候我再定义一个变量 b ,并将 a 赋值给它。

如果此时我更改 a 中的第二个元素为2,也就是 a[1] = 2 这行,是否会影响到 b 中的值呢?答案是会,执行结果是 2 2 。因为这里我只是简单地将 a 对应的数组赋值给 b ,这个赋值其实并没有重新创建一个切片,而是简单地将这两个变量都指向同一个切片。

那么,要怎么才能把 a 的值“复制”到 b 上呢?当然是像这样:

    var a = []int{10, 9, 8}
    var b = []int{a[0], a[1], a[2]}
    a[1] = 2
    fmt.Println(a[1], b[1])

此时,这段代码执行的结果将会是 2 9 。因为 b 在创建时的方法是从头声明一个切片,然后再拷贝 a 内的数值。这个就是“深拷贝”的最基础实现。

拓展

为什么直接设 var b = a 不行,而 var b = []int{a[0], a[1], a[2]} 却可以呢?值不都是从 a 中获取的吗?因为在大部分编程语言中,数组、切片、结构体、指针等数据结构对应的底层内存都是不会被赋值操作轻易更改的,而基础数据类型,例如整数、浮点、字符串等类型的变量的赋值操作才会实际产生内存层面的赋值。

怎么更优雅地实现深拷贝?

在C/C++中,我们可以使用 memcpy 来完成这个操作,不过显然在Golang中不行。由于Golang的内存安全特性,本身对指针、内存操作的限制相对严格,所以我们在Golang中很难用类似 memcpy 的思路来实现深拷贝。

如果要说“歪门邪道”的话,我觉得可能将结构体、指针、切片这些可以被反序列化的类型反序列化成json然后再序列化回来就好,但这样实在是太不优雅,我们这里在讨论 “怎么更优雅地实现深拷贝” ,所以这个想法并不在此列。

但是正如本文研究深拷贝的初衷,很多时候在业务中我们不可能没有需要一个性能与优雅兼具的深拷贝实现的时候。那在Golang中我们要怎么解决这个问题呢?我想 reflect 特性应该能很好地回答这个问题。

mohae/deepcopy,这个库虽然只有不到600个star,却被非常多项目引用,也包括我在业务网关中实现的深拷贝。这个项目的实际逻辑只有一个文件,125行。对多维数组、多层结构体、指针等可能具有嵌套结构的变量,它都实现了完善的支持。

mohae/deepcopy的实现:

// deepcopy makes deep copies of things. A standard copy will copy the
// pointers: deep copy copies the values pointed to.  Unexported field
// values are not copied.
//
// Copyright (c)2014-2016, Joel Scoble (github.com/mohae), all rights reserved.
// License: MIT, for more details check the included LICENSE file.
package deepcopy

import (
    "reflect"
    "time"
)

// Interface for delegating copy process to type
type Interface interface {
    DeepCopy() interface{}
}

// Iface is an alias to Copy; this exists for backwards compatibility reasons.
func Iface(iface interface{}) interface{} {
    return Copy(iface)
}

// Copy creates a deep copy of whatever is passed to it and returns the copy
// in an interface{}.  The returned value will need to be asserted to the
// correct type.
func Copy(src interface{}) interface{} {
    if src == nil {
        return nil
    }

    // Make the interface a reflect.Value
    original := reflect.ValueOf(src)

    // Make a copy of the same type as the original.
    cpy := reflect.New(original.Type()).Elem()

    // Recursively copy the original.
    copyRecursive(original, cpy)

    // Return the copy as an interface.
    return cpy.Interface()
}

// copyRecursive does the actual copying of the interface. It currently has
// limited support for what it can handle. Add as needed.
func copyRecursive(original, cpy reflect.Value) {
    // check for implement deepcopy.Interface
    if original.CanInterface() {
        if copier, ok := original.Interface().(Interface); ok {
            cpy.Set(reflect.ValueOf(copier.DeepCopy()))
            return
        }
    }

    // handle according to original's Kind
    switch original.Kind() {
    case reflect.Ptr:
        // Get the actual value being pointed to.
        originalValue := original.Elem()

        // if  it isn't valid, return.
        if !originalValue.IsValid() {
            return
        }
        cpy.Set(reflect.New(originalValue.Type()))
        copyRecursive(originalValue, cpy.Elem())

    case reflect.Interface:
        // If this is a nil, don't do anything
        if original.IsNil() {
            return
        }
        // Get the value for the interface, not the pointer.
        originalValue := original.Elem()

        // Get the value by calling Elem().
        copyValue := reflect.New(originalValue.Type()).Elem()
        copyRecursive(originalValue, copyValue)
        cpy.Set(copyValue)

    case reflect.Struct:
        t, ok := original.Interface().(time.Time)
        if ok {
            cpy.Set(reflect.ValueOf(t))
            return
        }
        // Go through each field of the struct and copy it.
        for i := 0; i < original.NumField(); i++ {
            // The Type's StructField for a given field is checked to see if StructField.PkgPath
            // is set to determine if the field is exported or not because CanSet() returns false
            // for settable fields.  I'm not sure why.  -mohae
            if original.Type().Field(i).PkgPath != "" {
                continue
            }
            copyRecursive(original.Field(i), cpy.Field(i))
        }

    case reflect.Slice:
        if original.IsNil() {
            return
        }
        // Make a new slice and copy each element.
        cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
        for i := 0; i < original.Len(); i++ {
            copyRecursive(original.Index(i), cpy.Index(i))
        }

    case reflect.Map:
        if original.IsNil() {
            return
        }
        cpy.Set(reflect.MakeMap(original.Type()))
        for _, key := range original.MapKeys() {
            originalValue := original.MapIndex(key)
            copyValue := reflect.New(originalValue.Type()).Elem()
            copyRecursive(originalValue, copyValue)
            copyKey := Copy(key.Interface())
            cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
        }

    default:
        cpy.Set(original)
    }
}

因为这个实现只有一百多行,所以我想我应该可以把它放在正文里。这是一个相当优雅的实现,通过枚举 reflect.Type 实现对指针、Map、结构体、切片类型的深拷贝支持。此外,还通过递归调用保证了对所有嵌套数据结构的支持。

所以我们就可以像这样封装一个方法来实现深拷贝:

type lineIndicator [][2][2]int

func (li lineIndicator) Copy() lineIndicator {
    return deepcopy.Copy(li).(lineIndicator)
}

由于这个实现暴露的方法 deepcopy.Copy() 的入参和返回值都是 interface{} ,所以针对需要深拷贝的类型额外封装一个方法来做断言和入参类型限制是有必要的。这么做在应用层面的实现就十分优雅了,代码量小、逻辑清晰简单、高性能,这就是我们研究这个问题的全部目的,也是这篇文章的讨论的问题的终极答案。

结语

reflect和序列化实现的缺陷

前面聊了序列化实现和reflect实现,但其实这两种实现都无法对没有导出的field进行深拷贝。核心原因是Golang关于参数导出的规则——变量以大写开头视为可导出,以小写开头视为不可导出。

无法拷贝私有field确实是这种方法的硬伤,要解决这个问题只能把实现搬到包内部,虽然事实上我们也这么做了,但很多时候为了一碟醋包一盘饺子确实没有太大必要。


Q.E.D.
0xC4A1

2024-12-27 21:09 提笔
2024-12-28 18:55 完稿

最后修改:2025 年 01 月 15 日
如果这对你有用,我乐意之至。