标签 Golang 下的文章

Go语言中的深拷贝:概念、实现与局限

本文永久链接 – https://tonybai.com/2024/09/28/understand-deep-copy-in-go

前不久,在“Gopher部落”知识星球上回答了一个Gopher关于深拷贝(Deep Copy)的问题,让我感觉是时候探讨一下深拷贝技术了。

在日常开发工作中,深拷贝的使用频率相对较低,可能有80%的时间不需要使用深拷贝,只有在特定情况下才会遇到。这主要是因为大多数开发中处理的对象比较简单,通常只需使用浅拷贝(Shallow Copy)就能满足需求;此外,多数时候我们需要共享状态或数据,使用浅拷贝可以方便多个部分访问同一数据;最后,深拷贝通常比浅拷贝耗时更多,尤其是当对象嵌套较深时。因此,开发者倾向于选择更高效的浅拷贝。

说了这么多,那究竟什么是深拷贝以及浅拷贝呢?深拷贝又是在哪些场合下适用呢?在Go中如何实现深拷贝呢?带着这些问题,我们在本文中就来探讨一下Go语言中的深拷贝技术,希望能让大家对深拷贝技术的概念、实现以及局限有一个全面的了解。

1. 从细胞分裂看深拷贝

我们在初中生物课上都学过细胞分裂(Cell Division),有条件的学校的学生可以用显微镜观看到细胞分裂的全过程,大致就如下图所示:


细胞分裂过程(图片来自网络)

我们知道细胞分裂复制了整个细胞的所有成分,包括细胞核、细胞质等,生成了一个完全独立的新细胞。无论原始细胞如何变化,分裂出的新细胞不会受到影响。而深拷贝就像是真正的细胞分裂,完全复制了原对象及其内部所有嵌套对象的数据,使新对象和原对象相互完全独立,各自演进,互不影响。

下面,我将使用Go语言给出一个结构体类型的示例,并用示意图直观展示深拷贝和浅拷贝的区别:

// Address 结构体
type Address struct {
    City  string
    State string
}

// Person 结构体
type Person struct {
    Name    string
    Age     int
    Address *Address
}

这里定义了Address和Person两个结构体,其中Person包含一个指向Address的指针(这可以理解为Person结构体的嵌套对象)。我们先来创建一个原始对象:

// 创建原始 Person 实例
original := Person{
    Name: "Alice",
    Age:  30,
    Address: &Address{
        City:  "New York",
        State: "NY",
    },
}

基于这个原始对象,我们可以使用下面代码创建一个浅拷贝的对象:

shallowCopy := original

下面是浅拷贝完毕的对象关系示意图:

我们看到浅拷贝后,两个Person对象虽然有部分字段已经完全独立分开(Name和Age),但仍然存在关联,那就是Address字段指向了同一个Address对象。这样无论是原始对象修改了Address,还是浅拷贝后的对象修改了Address,都会对另一个对象产生影响。

我们再来看看深拷贝,这里为Person结构体增加了深拷贝的方法,然后通过该方法得到一个深拷贝后的对象:

// DeepCopy方法
func (p Person) DeepCopy() Person {
    newPerson := p
    if p.Address != nil {
        newAddress := *p.Address
        newPerson.Address = &newAddress
    }
    return newPerson
}

deepCopy := original.DeepCopy()

我们看到:DeepCopy方法实现了对Person的深拷贝,它不仅复制了Person结构体,还创建了一个新的Address结构体并复制了其内容。这样原始对象与深拷贝出的对象就完全分开了,下面是深拷贝后的对象关系示意图:

通过上面的示意图,我们可以将深拷贝与浅拷贝的对比整理如下:

  • 浅拷贝(Shallow Copy)

创建一个新对象,并复制原对象的字段值,但对于引用类型(如指针、切片、map等),仅复制引用,不复制引用的对象。通常通过简单的赋值操作就能实现浅拷贝。

  • 深拷贝(Deep Copy)

创建一个新对象,递归地复制原对象的所有字段值,对于引用类型,创建新的对象并复制其内容,而不是简单地复制引用。通常,深拷贝需要额外编写代码实现,简单的赋值操作对于复杂类型而言,无法实现深拷贝。

很显然就像在本文开始时所说的那样,我们日常使用最多的就是浅拷贝,浅拷贝的实现也是非常简单的,通过赋值语句就可以。那么我们为什么还需要深拷贝呢?或者说,在什么场景下需要使用到深拷贝呢?下面我就就来看看。

2. 为什么需要深拷贝?

根据上面提到的深拷贝的特点:独立与隔离,当数据的独立性和隔离性非常重要时,它能避免共享数据引发的副作用。据此,以下是需要使用深拷贝的常见场景,我们逐一简要说明一下。

2.1 防止意外修改共享数据

在Go语言中,切片、map和指针都是引用类型。如果多个对象引用同一个底层数据结构,修改其中一个对象的数据会影响所有引用该数据的对象。因此,在这些场合下,如果希望避免修改一个对象时影响其他对象,使用深拷贝是必需的。

下面这个Go例子中,shallowCopy和original共享同一个Data map,修改shallowCopy的数据会直接影响original。通过深拷贝Data map,deepCopy保持了数据的独立性:

package main

import "fmt"

type Config struct {
    Port int
    Data map[string]string
}

func main() {
    original := &Config{
        Port: 8080,
        Data: map[string]string{"key1": "value1"},
    }

    shallowCopy := original // 只是浅拷贝,共享Data引用

    // 深拷贝 Data
    deepCopy := &Config{
        Port: original.Port,
        Data: make(map[string]string),
    }
    for k, v := range original.Data {
        deepCopy.Data[k] = v
    }

    shallowCopy.Data["key1"] = "modified" // 修改会影响original
    fmt.Println(original.Data["key1"])    // 输出 "modified"

    deepCopy.Data["key1"] = "deepModified" // 修改不会影响original
    fmt.Println(original.Data["key1"])     // 输出 "modified"
}

2.2 并发编程中的数据隔离

Go语言利用goroutine进行并发编程。当多个goroutine操作相同的数据时,可能会导致竞争条件和数据一致性问题。如果每个goroutine都需要独立的数据副本,那么深拷贝是确保数据隔离的最佳方法。

下面这个示例就是在并发场景下,使用append深拷贝切片,确保每个goroutine操作的是独立的data副本,避免数据竞争:

package main

import "fmt"

func worker(data []int, ch chan []int) {
    // 深拷贝切片,避免影响其他 goroutine
    newData := append([]int(nil), data...)
    for i := range newData {
        newData[i] *= 2 // 修改数据
    }
    ch <- newData
}

func main() {
    data := []int{1, 2, 3}
    ch := make(chan []int)

    go worker(data, ch) // 启动goroutine
    go worker(data, ch) // 启动另一个goroutine

    result1 := <-ch
    result2 := <-ch

    fmt.Println(result1) // goroutine 1的独立数据副本 [2 4 6]
    fmt.Println(result2) // goroutine 2的独立数据副本 [2 4 6]
}

2.3 不可变对象需求

Go目前不直接支持不可变对象,但在某些场合(如函数式编程或安全性要求较高的应用),不可变性是很有用的。如果你希望传递给某个函数的数据不能被修改,那么需要在传递前对数据进行深拷贝。

下面示例通过深拷贝,保证original的数据在传递过程中不会被修改,保证了不可变性:

package main

import "fmt"

type ImmutableData struct {
    Values []int
}

// 修改函数
func modifyData(data ImmutableData) {
    data.Values[0] = 100 // 尝试修改
}

func main() {
    original := ImmutableData{
        Values: []int{1, 2, 3},
    }

    // 传递之前进行深拷贝
    copyData := ImmutableData{
        Values: append([]int(nil), original.Values...),
    }

    modifyData(copyData)
    fmt.Println(original.Values) // 输出 [1 2 3],original数据保持不变
}

2.4 回滚机制或撤销操作

在涉及事务处理或编辑器等场景中,Go开发者常需要在操作前保存对象的快照,以便在出现错误或用户撤销操作时恢复到原状态。这时候,深拷贝用于保存独立的状态副本。下面示例使用了更复杂的数据结构来展示深拷贝的作用,并体现了在实际应用中如何通过深拷贝实现状态的回滚机制:

package main

import (
    "encoding/json"
    "fmt"
)

// State 结构体包含嵌套结构体和引用类型
type State struct {
    Value    string
    Data     []int
    Metadata *Metadata
}

// Metadata 是嵌套的引用类型结构体
type Metadata struct {
    Version int
    Author  string
}

// 深拷贝函数,通过JSON序列化与反序列化实现
func deepCopy(original *State) *State {
    copy := &State{}
    bytes, _ := json.Marshal(original)
    _ = json.Unmarshal(bytes, copy)
    return copy
}

func main() {
    // 初始化原始状态
    state := &State{
        Value: "initial",
        Data:  []int{1, 2, 3},
        Metadata: &Metadata{
            Version: 1,
            Author:  "Alice",
        },
    }

    // 保存当前状态的深拷贝
    backup := deepCopy(state)

    // 修改状态
    state.Value = "modified"
    state.Data[0] = 100
    state.Metadata.Version = 2

    // 输出修改后的状态
    fmt.Println("Current state:", state.Value)                       // 输出 "modified"
    fmt.Println("Current Data:", state.Data)                         // 输出 "[100 2 3]"
    fmt.Println("Current Metadata.Version:", state.Metadata.Version) // 输出 "2"

    // 恢复之前的状态
    state = backup

    // 输出恢复后的状态
    fmt.Println("Restored state:", state.Value)                       // 输出 "initial"
    fmt.Println("Restored Data:", state.Data)                         // 输出 "[1 2 3]"
    fmt.Println("Restored Metadata.Version:", state.Metadata.Version) // 输出 "1"
}

在这个场景中,backup是对state的深拷贝,确保可以在需要时恢复到原始状态。

在以上这些场景中,深拷贝虽然开销较大,但它确保了数据的独立性、隔离性以及安全性。当然,深拷贝适用的场景可能不止这些,这里也无法穷举所有场景。

知道了深拷贝的一些应用场景后,我们再来梳理一下如何在Go中实现深拷贝,其实在上面的示例中已经见过不少深拷贝的实现方法了。

3. Go语言中实现深拷贝的方法

在Go语言中,实现深拷贝有几种常见的方法,每种方法都有其优缺点和适用场景。让我们逐一探讨这些方法。

3.1 手动实现深拷贝

赋值操作通常无法实现复杂结构的深拷贝,因此最常见的深拷贝实现方法就是像上面示例中那样根据具体的类型手动实现深拷贝。手动实现深拷贝是最直接但也可能是最繁琐的方法,通常我们要为每种要深拷贝的类型单独编写深拷贝函数DeepCopy(Go没有像Java那样有object基类,因此也没有内置的clone方法去override)。

关于手动实现深拷贝DeepCopy方法的示例在前面我们已经见识过了,比如最开始的那个Person类型DeepCopy方法。

手动实现深拷贝的优点显而易见,那就是开发者可以完全控制拷贝的过程,并且性能通常较好,可以避免使用反射等有额外开销的机制来实现。

当然不足也很明显,那就是我们需要为每个要支持深拷贝的类型都维护一个单独的实现,并且对于带有复杂嵌套结构的类型,这个实现还会很冗长和复杂。

当是否可以有“万能”的深拷贝函数呢?我们继续往下看。

3.2 使用反射实现通用深拷贝

借助Go的reflect大法,我们可以实现一个通用的深拷贝函数,理论上,可以适用于各种类型。下面是一个示例实现(仅是示例,不要用在生产中):

package main

import (
    "fmt"
    "reflect"
)

// 深拷贝函数,使用 reflect 递归处理各种类型
func DeepCopy(src interface{}) interface{} {
    if src == nil {
        return nil
    }

    // 通过 reflect 获取值和类型
    value := reflect.ValueOf(src)
    typ := reflect.TypeOf(src)

    switch value.Kind() {
    case reflect.Ptr:
        // 对于指针,递归处理指针指向的值
        copyValue := reflect.New(value.Elem().Type())
        copyValue.Elem().Set(reflect.ValueOf(DeepCopy(value.Elem().Interface())))
        return copyValue.Interface()

    case reflect.Struct:
        // 对于结构体,递归处理每个字段
        copyValue := reflect.New(typ).Elem()
        for i := 0; i < value.NumField(); i++ {
            fieldValue := DeepCopy(value.Field(i).Interface())
            copyValue.Field(i).Set(reflect.ValueOf(fieldValue))
        }
        return copyValue.Interface()

    case reflect.Slice:
        // 对于切片,递归处理每个元素
        copyValue := reflect.MakeSlice(typ, value.Len(), value.Cap())
        for i := 0; i < value.Len(); i++ {
            copyValue.Index(i).Set(reflect.ValueOf(DeepCopy(value.Index(i).Interface())))
        }
        return copyValue.Interface()

    case reflect.Map:
        // 对于映射,递归处理每个键值对
        copyValue := reflect.MakeMap(typ)
        for _, key := range value.MapKeys() {
            copyValue.SetMapIndex(key, reflect.ValueOf(DeepCopy(value.MapIndex(key).Interface())))
        }
        return copyValue.Interface()

    default:
        // 其他类型(基本类型,数组等)直接返回原始值
        return src
    }
}

type Address struct {
    Street string
    City   string
}

type Person struct {
    Name    string
    Age     int
    Address *Address
}

func main() {
    // 初始化原始对象
    original := &Person{
        Name: "Alice",
        Age:  30,
        Address: &Address{
            Street: "123 Go St",
            City:   "Golang City",
        },
    }

    // 使用 reflect 实现的通用深拷贝
    copy := DeepCopy(original).(*Person)

    // 修改拷贝对象的值
    copy.Address.City = "New City"
    copy.Age = 31

    // 输出结果
    fmt.Println("Original Addr:", original.Address) // 输出 &{123 Go St Golang City}
    fmt.Println("Copy Addr:", copy.Address)         // 输出 &{123 Go St New City}
}

我们看到,在示例中,reflect包可以在运行时检查和操作Go的值。通过reflect.ValueOf(src)获取到值后,根据值的类型(指针、结构体、切片、map等)再递归进行深拷贝。如果遇到指针类型,DeepCopy将递归地拷贝指向的值,新的值通过reflect.New创建;对于结构体类型,它通过NumField()遍历字段,并递归地深拷贝该字段;对切片进行深拷贝时,首先使用reflect.MakeSlice()创建新的切片,再递归处理每个元素; 对于map,它用reflect.MakeMap()创建新的map,并递归处理键值对。

使用reflect包实现深拷贝的优点十分明显,那就是通用性强,能够处理各种数据结构(如指针、结构体、切片、map等),无需为每个类型单独实现DeepCopy方法。但由于使用了reflect,其带来的额外开销也是不可忽视的,尤其是对于嵌套很深的复杂类型。

有些情况是reflect无法正确处理的,比如被拷贝的类型中带有非导出字段时(比如给Person结构体增加一个gender字段),上面的反射版DeepCopy实现就会抛出panic:

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

此外,实现一个生产级的DeepCopy并非易事,我们可以找一些“久经考验”的第三方库,比如下面的jinzhu/copier。

3.3 使用第三方库

有一些第三方库提供了深拷贝功能,例如github.com/jinzhu/copier,这类库通常结合了反射和一些优化技巧。在经过广泛的使用和反馈后,可以在生产中使用,并且可以覆盖大多数需求场景。

下面是使用copier实现对带有非导出字段的结构体类型的深拷贝:

package main

import (
    "fmt"

    "github.com/jinzhu/copier"
)

type Person struct {
    Name    string
    Age     int
    Address *Address
    gender  string
}

type Address struct {
    Street string
    City   string
}

func main() {
    addr := Address{
        Street: "Go 101 street",
        City:   "Mars Capital",
    }
    original := Person{
        Name:    "Alice",
        Age:     30,
        Address: &addr,
        gender:  "female",
    }

    fmt.Println(original) // 输出:{Alice 30 0xc0000b0000 female}

    var copied Person
    err := copier.CopyWithOption(&copied, &original, copier.Option{
        DeepCopy: true,
    })
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(copied) // 输出:{Alice 30 0xc0000b0020 female}
}

copier是怎么做到的呢?翻看copier源码,可以找到这样一个函数:

func copyUnexportedStructFields(to, from reflect.Value) {
    if from.Kind() != reflect.Struct || to.Kind() != reflect.Struct || !from.Type().AssignableTo(to.Type()) {
        return
    }

    // create a shallow copy of 'to' to get all fields
    tmp := indirect(reflect.New(to.Type()))
    tmp.Set(from)

    // revert exported fields
    for i := 0; i < to.NumField(); i++ {
        if tmp.Field(i).CanSet() {
            tmp.Field(i).Set(to.Field(i))
        }
    }
    to.Set(tmp)
}

我们看到copyUnexportedStructFields函数首先检查源值和目标值是否都是结构体,并且源类型是否可以赋值给目标类型。如果可以赋值,则创建一个目标类型的新实例tmp,并将源值完整地设置到这个新实例中。这一步可以复制所有字段,包括非导出字段。接下来,遍历目标结构体的所有字段。对于可以设置的字段(即导出字段),将原始目标值中的对应字段值设置回tmp。最后,将tmp设置回原始目标值。

这个过程巧妙地利用了Go语言的反射机制。通过创建一个新的结构体实例并直接设置整个源值,它可以绕过Go语言对非导出字段的访问限制。然后,通过只恢复导出字段的原始值,保持了目标结构体中导出字段的完整性,同时保留了源结构体中非导出字段的值。

然而,这种方法也有一些潜在的限制,比如对于包含指针或引用类型的非导出字段,这种方法就无法真正实现深拷贝,我们改造一下上面的示例:

type Person struct {
    Name    string
    Age     int
    Address *Address
    gender  *string
}

type Address struct {
    Street string
    City   string
}

func (p *Person) SetGender(gender string) {
    p.gender = &gender
}
func (p *Person) Gender() *string {
    return p.gender
}

func main() {
    addr := Address{
        Street: "Go 101 street",
        City:   "Mars Capital",
    }
    original := Person{
        Name:    "Alice",
        Age:     30,
        Address: &addr,
    }
    original.SetGender("female")

    fmt.Println(original) // 输出:{Alice 30 0xc00006a020 0xc000014070}
    fmt.Println(original.Gender()) // 输出:0xc000014070

    var copied Person
    err := copier.CopyWithOption(&copied, &original, copier.Option{
        DeepCopy: true,
    })
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(copied) // 输出:{Alice 30 0xc00006a040 0xc000014070}
    fmt.Println(copied.Gender()) // 输出:0xc000014070
}

这里我们在Person类型中增加了一个字符串指针类型的非导出字段gender,我们看到通过copier进行拷贝的结果并不符合深拷贝的要求,copied和original使用了同一个gender了。因此,像jinzhu/copier这样的第三方库,虽然能处理大多数常见情况,但我们仍要明确它的局限。

不过即便有了上述三类实现深拷贝的方法,有些时候要在Go中实现完美的深拷贝也是很难的,甚至是不可能的,下面我们来看看Go语言中深拷贝的局限性。

4. Go语言中深拷贝的局限性

我们先从已经遇到过的非导出字段说起。

4.1 无法访问的非导出字段

就像上面示例中那样,如果原类型中带有非导出字段,那么有些时候即便使用jinzhu/copier这样的第三方通用拷贝库也很难实现真正的深拷贝。如果原类型在你的控制下,最好的方法是为原类型手动添加一个DeepCopy方法供外部使用

不过,即便如此,某些情况下,手工实现一个DeepCopy方法也是很难的,甚至是不可能的,我们看下面两种局限的情况。

4.2 循环引用问题

当原类型中存在循环引用时,简单的递归深拷贝可能会导致无限循环。例如:

type Node struct {
    Value int
    Next  *Node
    Prev  *Node
}

func main() {
    node1 := &Node{Value: 1}
    node2 := &Node{Value: 2}
    node1.Next = node2
    node2.Prev = node1

    // 这里的深拷贝可能会导致无限递归
}

针对这样的带有循环引用的类型,我们通常会手工实现其DeepCopy方法,并通过使用类似哈希表的方式记录已经复制过的对象,下面是一个Node结构体的DeepCopy的示例实现:

package main

import (
    "fmt"
)

// Node表示双向链表的节点
type Node struct {
    Value int
    Next  *Node
    Prev  *Node
}

// DeepCopy方法:对Node进行深拷贝
func (n *Node) DeepCopy() *Node {
    // 初始化visited map用于记录已访问的节点,防止无限递归
    visited := make(map[*Node]*Node)
    return n.deepCopyRecursive(visited)
}

// deepCopyRecursive私有递归方法,内部处理深拷贝逻辑
func (n *Node) deepCopyRecursive(visited map[*Node]*Node) *Node {
    // 如果节点为空,返回nil
    if n == nil {
        return nil
    }

    // 如果节点已经被拷贝过,直接返回拷贝的引用
    if copyNode, found := visited[n]; found {
        return copyNode
    }

    // 创建当前节点的拷贝,并将其加入已访问map
    copyNode := &Node{Value: n.Value}
    visited[n] = copyNode

    // 递归拷贝下一个和前一个节点
    copyNode.Next = n.Next.deepCopyRecursive(visited)
    copyNode.Prev = n.Prev.deepCopyRecursive(visited)

    return copyNode
}

func main() {
    // 创建包含循环引用的双向链表
    node1 := &Node{Value: 1}
    node2 := &Node{Value: 2}
    node1.Next = node2
    node2.Prev = node1

    // 进行深拷贝
    copyNode1 := node1.DeepCopy()

    // 修改拷贝对象,确保原始对象不受影响
    copyNode1.Next.Value = 3

    // 输出原始链表和拷贝链表的指针地址,验证深拷贝是否成功
    fmt.Println("Original node1 address:", node1)
    fmt.Println("Original node1.Next address:", node1.Next)
    fmt.Println("Original node2.Prev address:", node2.Prev)

    fmt.Println("Copied node1 address:", copyNode1)
    fmt.Println("Copied node1.Next address:", copyNode1.Next)
    fmt.Println("Copied node2.Prev address:", copyNode1.Next.Prev)
}

运行这段示例程序会得到下面结果:

Original node1 address: &{1 0xc00011c018 <nil>}
Original node1.Next address: &{2 <nil> 0xc00011c000}
Original node2.Prev address: &{1 0xc00011c018 <nil>}
Copied node1 address: &{1 0xc00011c048 <nil>}
Copied node1.Next address: &{3 <nil> 0xc00011c030}
Copied node2.Prev address: &{1 0xc00011c048 <nil>}

下面再说一种极端情况,导致我们即便手工实现也无法实现深拷贝。

4.3 某些类型不支持拷贝

Go语言的某些内置类型或标准库中的类型,比如sync.Mutex、time.Timer等不应该被复制,复制这些类型可能会导致未定义的行为。

type Resource struct {
    Data  string
    mutex sync.Mutex
}

// 错误的深拷贝方式
func (r *Resource) DeepCopy() *Resource {
    return &Resource{
        Data:  r.Data,
        mutex: r.mutex, // 不应该复制 mutex
    }
}

对于这样的包含不支持拷贝的类型,我们在不改变源类型组成的情况下,无法实现深拷贝。

除了上面三种情况外,有些时候性能也是使用深拷贝时需要考量的点,尤其是当你使用反射实现的通用深拷贝技术时,可能会带来显著的性能开销。尤其是在关键路径上处理大型数据结构或频繁操作时,这可能成为一个问题。

如果在使用深拷贝时遇到性能问题,可以考虑通过手动编写深拷贝逻辑替代反射、使用对象池或预分配的方式缓存并优化内存分配,减少深拷贝的次数,甚至是针对复杂类型或数据结构的并发拷贝来优化,这些需要视具体场景来确定优化策略,这里就不展开了。

5. 深拷贝(Deep Copy)vs. 克隆(Clone)

最后再来说一下深拷贝(Deep Copy)和克隆(Clone)。它们都是复制对象的概念,但它们在概念和实现细节上存在一些差异。

通过上面说明,我们知道深拷贝是一种递归的复制过程,不仅复制对象本身,还会复制该对象所有引用的其他对象。这意味着所有的对象层级都会被独立地复制,最终形成一个完全独立的新对象,原对象和拷贝之间不存在任何共享的内存。

而克隆是指复制一个对象。其行为依赖于具体语言的实现方式。对于某些语言,克隆可能指的是浅拷贝(Shallow Copy),即只复制对象的基础数据字段,引用类型字段仍然指向原始对象。也有些语言将克隆定义为深拷贝,取决于上下文。比如在Java中,Object类提供了clone()方法,默认是浅拷贝,用户可以通过实现Cloneable接口来自定义克隆的行为,比如实现为深拷贝的逻辑。

因此,当目标对象在结构上与原对象一致的情况下,可以将深拷贝理解为一种特定类型的克隆。但在一些场景下(比如RPC),深拷贝不仅仅是简单的在内存中深度复制自身,而是需要考虑源对象和目的对象之间的结构差异和数据转换逻辑,本文并未覆盖这类场景,大家可以自行脑补。

5. 小结

在本文中,我们深入探讨了Go语言中的深拷贝概念、实现方法以及局限性。深拷贝在需要对象之间完全独立的场景中尤为重要,尤其是在防止意外修改共享数据、并发编程、不可变对象需求、回滚机制等情况下。我们介绍了手动实现深拷贝、利用反射的通用深拷贝方法以及使用第三方库的不同实现方式,并分析了每种方法的优缺点。

尽管深拷贝提供了数据的独立性和安全性,但在实现过程中也面临一些挑战,包括无法访问非导出字段、循环引用的问题,以及某些类型不支持拷贝的限制。性能问题也是一个需要考虑的因素,特别是在处理复杂数据结构时。

通过对深拷贝的理解,我希望大家能够在实际开发中更有效地使用这一技术,并根据具体需求选择合适的实现方式,从而优化代码质量和程序性能。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

“类型名称”在Go语言规范中的演变

本文永久链接 – https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec

Go语言规范(The Go Programming Language Specification)是Go语言的核心文档,定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准,它确保了语言的稳定性和一致性,特别是在类型系统设计中,Go团队通过规范推动了语言的简洁性、稳定性与可维护性。对于Go开发者而言,Go语言规范也是语法特性使用的参考手册(虽然语言规范读起来比较抽象和晦涩)。

Go语言规范由Google的Go核心开发团队维护和演进,这与ISO标准的C/C++语言规范有所不同。C和C++语言的ISO标准更新较慢,需经过复杂的全球共识和审核流程,而相比之下,Go语言的管理方式就显得更加灵活,也能够迅速适应新需求。

然而,这种灵活性也带来了潜在的弊端。随着新语法特性的引入和演进,一些已有的概念的含义可能会发生变化,导致前后的不一致性,从而让开发者感到困惑。例如,Go中的Type Name(类型名称)就经历了从最初的Named Type,到Defined Type和Alias Type,最终又回归到Named Type的过程。

近期Go语言之父之一的Robert Griesemer在Go官博发表了一篇名为”What’s in an (Alias) Name?“的文章,其中就对Go spec中Type Name的历史演进做了回顾,这里我们就基于这段回顾对“类型名称(Type Name)”在Go语言规范中的演变做一下简要梳理,希望能帮助大家更好的理解Go。

1. Go规范中的Type Name(类型名称)

在Go语言规范中,Type Name是指给定类型的标识符,它为一个类型提供了唯一的名称。Type Name用于识别和引用各种类型,这包括Go内置(也叫预声明Predeclared Type)的基础类型(比如int、string)和用户自定义的类型,比如:

var x int       // int是基础类型的Type Name
type MyInt int  // MyInt是用户定义类型的Type Name

你可能会问,Go还有没有类型名称的类型吗?当然有了,有一些特殊的类型没有直接的类型名称。通常,这些类型是匿名类型(Anonymous Type),即它们并没有通过命名来标识,主要的匿名类型包括:

  • 字面量定义的复合类型(Composite Literals)

Go支持在代码中使用复合字面量来定义结构体、数组、切片、map等类型,而不为这些类型显式地定义名称。这些类型是在使用时定义的,并没有为其单独声明一个类型名称。

var data = struct { Name string; Age int }{"Alice", 30}  // 匿名结构体类型
var arr = [5]int{1,2,3,4,5} // 匿名数组类型
var arr = []int{1, 2, 3}  // 匿名切片类型
var m = map[string]int{"foo": 1, "bar": 2}  // 匿名map类型
  • 匿名函数类型

Go支持函数作为一等公民,函数本身可以作为类型,当定义匿名函数(即未命名函数)时,这些函数没有类型名称。

var f = func() int { //匿名函数类型func() int
    return 42
}

Type Name是一个广泛的概念,在Go spec中,Go设计者们将其做了细分,比如Named Type、Defined Type等。那么随着Go版本的变化,Go中的Type Name的分类有哪些重要的演进和变化呢,下面我们就重点说明一下Go spec中Type Name分类的三次重要变化。

2. 初始阶段:简单而明确的Named Type (2009-2017)

Go 1.0是Go语言的首个正式发布版本,其中确立了类型名称的基础概念。在这一阶段,Go的类型系统已经具备了高度的简洁性和一致性,这也是该语言设计的核心原则之一。

在Go语言的早期阶段(2009-2017),Go规范就确定了简单明确的Named Type的概念,它指的是通过下面语法定义的类型T:

type T existingType

这些通过类型声明定义的T被称为Named Type。而这里的existingType可以Predeclared的预声明类型(比如int、string),可以是已存在的Named Type,也可以是前面提到的匿名类型。

通过给现有类型赋予新名称来定义新的类型,与匿名类型等未命名类型形成鲜明对比。这种简单的分类满足了早期Go程序员的需求,为代码组织和类型系统提供了清晰的基础,提升了代码的可读性和模块化。

我们可以用示意图来展示这个阶段的Go类型名称分类:

而Named Type的定义方式也可以用下图表示:

我们看到,可以基于Predeclare Type、匿名类型以及已存在的Named Type来定义一个新的Named Type。并且,Named Type具有一些专有特性,比如可拥有自己的方法、只与自身类型赋值兼容,不与其底层类型直接兼容(除非进行显式类型转换)等。

3. 变革之始:别名类型的引入 (Go 1.9, 2017)

然而,随着Go 1.9在2017年引入别名类型(Alias Type),情况开始变得复杂:

type T = Q // T为Q类型的别名类型

别名类型的引入是为了支持大规模代码库的重构,但它也模糊了Named Type的界限,因为别名也是一个类型名称。

为了应对这一变化,Go团队引入了”Defined Type”的概念以代替界限模糊的Named Type,用以特指通过类型定义(type T Q)创建的新类型。

这样改动后,整个Go类型系统的类型名称分类就变成如下示意图中的状态了:

Defined Type定义和Alias Type的定义分别如下:

两者看起来差别不大,但只有Defined Type才拥有专有属性,比如可拥有自己的方法、只与自身类型赋值兼容等。我们也可以为Alias Type定义方法,但那个方法属于原类型。

4. 泛型时代的到来:概念的重塑 (Go 1.18, 2022)

2022年,Go 1.18的发布标志着Go语言进入了泛型时代,这一重大特性的引入再次挑战了现有的类型分类方式。

比如类型参数也是类型,它们有名称,与Defined Type一样,两个不同命名的类型参数表示不同的类型。换句话说,类型参数是Named Type,而且它们的行为在某些方面与Go原始的Named Type类似。更重要的是,Go的Predeclare Type(如int、string等)只能通过它们的名称来访问,并且像Defined Type和类型参数一样,如果它们的名称不同,它们也会不同,这样预声明的类型也变成了Named Type。

为了适应泛型,Go规范重新引入了Named Type,并将其范围扩大到包括预声明类型、Defined Type、类型参数以及部分情况下的别名类型。

重新引入Named Type后,Defined Type依然得以保留,整个Go系统类型的最新类型名称分类状态如下图所示:

5. 当前的权衡

在”What’s in an (Alias) Name?“的文章中,Robert还提到了学院派类型系统理论中的Nominal type(名义类型)和Structural type(结构类型)两个概念,虽然Go spec目前完全没有使用这两个概念。

Nominal type,也叫名义类型。这种类型的身份(identity)明确地与其名称相关联。两个类型即使结构完全相同,如果名称不同,也被视为不同的类型。像Go 1.18以后spec中的预声明类型(如int、string等)、Defined types(通过type关键字定义的类型)和类型参数都属于这种类型,这大体与Named Type是重叠的。

Structural type(结构类型)的类型的身份仅取决于其结构或组成,而不依赖于名称。如果两个类型的结构相同,它们就被视为相同的类型,即使它们可能有不同的名称,像Go中的接口类型(在某种意义上)、通过类型字面量创建的类型(如匿名结构体、函数类型等)等都可以归属与这种类型。值得注意的是,指向类型字面量的别名类型(如type AliasName = struct{ … })也可看作是structural type。

不过Robert也提到了,后续Go还会继续沿用Named Type、Defined Type等术语,而不会用这些学院派的类型术语来更新Go spec,这主要有几方面考虑:

  • 历史一致性:Go语言从早期就使用了named type、defined type等术语。突然改变可能会导致现有文档、教程和代码库的混乱。
  • 概念特殊性:Go的类型系统有其特殊性,不完全符合传统的nominal/structural二分法。例如,Go的接口类型结合了nominal和structural的特性。这么做,也可以避免引起其他语言中该术语用法的混淆。
  • 实用性考虑:”named type”、”defined type”等术语在Go的上下文中有明确的含义,直接对应于语言的特定特性和语法结构。这使得它们在讨论Go特定概念时更加实用。

6. 小结

本文基于Robert的文章讲述了Go语言类型系统中的类型名称的演变历程。我们回顾了Type Name在Go语言规范中的重要变化,从最初的简单Named Type到后来的Defined Type和Alias Type,再到引入泛型时代后的重新定义Named Type。每一次变化不仅反映了Go语言的不断发展,也展示了Go团队在应对复杂性和保持语言简洁性之间的平衡。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats