分类 技术志 下的文章

跟上Go演进步伐,你只需要关注这几件事儿

本文永久链接 – https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution

Go语言以其简洁、高效和强大的特性赢得了众多开发者的青睐。与许多主流编程语言有着明确的演进Roadmap或下一个版本spec不同,Go的演进过程更加独特、灵活与开放。这种看起来不那么正式和严肃的演进方式却也能让Go快速响应开发者的需求,同时保持语言的稳定性和一致性。

作为一名Go开发者,跟上Go的演进步伐,甚至是参与到这个激动人心的过程中来,不仅能让你更好地利用语言的新特性,还能帮助你更深入地理解Go的设计哲学。

但很多Go开发者只知道每年Go有两次的大版本Release,并通过大版本的Release Notes来了解Go的演进。这无可厚非,但对于那些想及时跟进Go演进的Gopher来说,光有一年两次的Release Notes还是不够的的,很难及时跟进Go的演进决策。

但如果直接到Go语言项目的issue中去翻阅,面对Go丰富的社区讨论和频繁的更新,你可能会感到无从下手。别担心,本文将为你指明方向,让你只需关注几个关键点,就能轻松跟上Go的演进步伐。

1. 开发计划早知道

Go的版本规划具有很高的灵活性。每个Go 1.x版本在开发前,Go语言项目相关人员都会在golang-dev讨论组上发布一个帖子,这个帖子通常的标题为”Planning Go 1.x”,例如”Planning Go 1.23″,如下图:

很多contributor,无论是Go团队的,还是外部贡献者的,会在该帖子下面留下自己的plan(注意:这些plan中的特性可不一定会在最终的版本中发布),然后等main tree开放后,就会将已经准备完毕的cl(changelist) merge到main tree中去,或开始提交cl,等待Go团队或社区的开发者进行评审。

当然对于Go 1.x这样的大版本,Go团队会在github建立专门的milestone跟踪,大家也可以在对应的milestone中看到该版本带来的新特性等,下图是目前正在积极开发的Go 1.24版本里程碑

通过查看这些Plan或定期查看Go 1.x里程碑,你可以提前了解Go的发展方向,为新版本的到来做好准备。

当然如果要了解那些更早的Go演进的决策,我们还得关注和跟踪下面的Proposal Project看板和三个关键的issue。

2. Proposal Project看板和三个关键的Issue

Go在早期并没有规范的proposal提案流程,更多是由Rob Pike、Robert Griesemer等三个Go语言之父,外加Ian Taylor和Russ Cox讨论确定,这一状态在Russ Cox建立明确的Go proposal提案流程后结束,提案流程是Go团队审查提案并决定接受或拒绝提案的过程。Russ Cox在提案流程中明确了Go项目的开发过程是设计驱动(design-driven)的,必须首先对语言、库或工具的重大更改进行讨论(包括Go语言项目主仓库和所有golang.org/x仓库中的API更改,以及对go command的命令行更改),并在实现这些设计之前进行正式记录。

Go团队目前使用Proposal Project看板和GitHub Issues来追踪语言的演进,下面我们来看看这个看板和值得关注的三个Issue。

2.1 Proposal Project看板

Proposal Project看板是Go团队跟踪proposal的全局视图,当然要理解该看板,我们需要先来简单看看Go的proposal流程以及每个提案的生命周期是怎样的。

Go Proposal流程并不复杂,可以概括为下面这个示意图:

该流程图展示了Go提案流程的几个主要步骤:

  • 任何人都可以作为提案作者,在Go项目上创建一个简短的issue来描述提案。
  • Go团队成员以及任何Go社区成员在issue上进行初步讨论,由一组人组成的Go提案审核委员会决定是接受提案、拒绝提案,还是需要进一步的设计文档。
  • 如果需要进一步的设计文档,提案作者会撰写一个详细的设计文档。
  • 在设计文档的评论减少/收敛后(意见趋于一致后),由Go提案审核委员会会进行最终讨论,决定接受或拒绝提案。

Go提案审查委员会使用GitHub项目看板来跟踪提案的状态并管理提案的生命周期(如下图所示):

该看板针对每个提案issue设置了几个生命周期状态:

  • Incoming:新提交的提案
  • Active:正在积极讨论的提案
  • Likely Accept / Likely Decline:可能被接受或拒绝的提案
  • Accepted / Declined:已被接受或拒绝的提案
  • Hold:需要设计修订或需要几周或更长时间才能获得附加信息的提案,这类提案一旦准备就绪,还会回到Active状态

了解了上述Go提案与审核流程,再看下面的几个关键Issue就容易多了。

2.2 proposal: review meeting minutes(33502)

该issue于2019年8月创建,其创建者为前Go团队技术负责人Russ Cox。这是目前Go语言项目最核心的追踪Issue,它记录了Go提案审查会议的纪要,通常每周更新一次(如下图所示):

我们看到内容包括:

  • 发布当周已经决策为Accepted和Declined的proposal列表
  • 后续Likely Accept和Likely Decline的proposal列表
  • 正处于Active讨论的proposal列表
  • 当前处于Hold状态的proposal列表

和Go提案看板不同,该issue是对提案Issue的状态变更的记录,Gopher可以第一时间看到每周Go提案的状态更新。

由于Russ Cox已经辞去了Go团队技术负责人的头衔,从2024年9月下旬开始,Go团队新的技术负责人Austin Clements将继续主持提案审核会议,并更新该Issue。

除了Review meeting minutes这个重要的issue外,还有两个issue值得我们关注,通过它们,我们可以及时了解到Go编译器和运行时的演进以及Go语法特性的演进。

2.3 Go compiler and runtime meeting notes(43930)

Go编译器和运行时团队定期(大约每周)召开会议,讨论Go编译器和运行时的后续开发和演进事宜,该会议是Google Go团队的内部会议,但Go团队觉得Go社区有必要了解这个会议上的一些讨论议题、过程与会议结论,从而知道Go编译器和运行时团队正在以及将要做什么。

于是前Go团队成员Jeremy Faller于2021年1月创建了该Issue,向Go社区发布Go编译器和运行时的最新演进动向。

之前Go编译器和运行时团队的负责人是Austin Clements,如今是CherryMui

2.4 spec: language change review meeting minutes(33892)

编译器和运行时之外,Gopher最关心的就是Go语法的演进以及Go语言规范的变更,这个事儿是由Go语言之父之一的Robert Griesemer亲自抓的。在2019年8月,Robert Griesemer就建立了跟踪Go语法变化的issue,当然最初是要跟踪Go2的演进,后来Go泛型落地后,Go2彻底融入了Go1,该issue也就变成了跟踪Go语法演进的Issue。Robert Griesemer主持的Go语言变更审查会议每月举行一次,并将会议讨论的记录发布到该Issue上。

3. Discussion与Russ Cox博客

关于Go语言演进的动向,还有两个渠道可以关注,一个是Go团队在github repo上发起的discussionRuss Cox在2021年7月启用了discussion,旨在寻找一个地方来扩大许多人可能想要参与的讨论。当前,该discussion仅针对非常有限的事项添加讨论,并且只有少数Go核心团队的人才有发起discussion的权限。一些在前几个版本的重要语言特性变化以及标准库的变化,都在这里进行了充分的讨论,比如loopvar语义修正自定义iterator开启标准库major版本更新的math/rand/v2以及gonew工具等。

另外一个则是Russ Cox的博客,作为Go项目团队前技术负责人,作为Rob Pike的接班人,Russ Cox很好地完成了承上启下的作用,并为Go的演进和发展确立了演进框架、方法以及方向。Russ Cox经常在自己的博客上先“憋大招,做铺垫”!最典型的就是vgo,也就是go module的前身,在短短几周内Russ Cox在博客上发表了7篇关于vgo的设计思路文章,为后来Go module的落地奠定了基础,至此基本上不再有Gopher抱怨Go依赖管理了。Russ Cox现已辞去Go技术负责人的头衔,后续是否还能在他的博客上看到Go相关的新特性的设计,让我们拭目以待!

4. 小结

在快速发展的技术环境中,Go语言以其独特的演进方式和灵活的开发计划,吸引了越来越多的开发者。本文介绍了如何及时有效地跟踪Go的演进的方法,包括关注大版本开发计划、Proposal Project看板和关键的issue,帮助Gopher及时了解语言的新特性与设计决策。通过参与讨论和关注Go团队的动态,开发者不仅能掌握最新的语言更新,还能深入理解Go的设计哲学和发展方向。希望每位Gopher都能抓住这些资源,与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/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语言第一课 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