Golang中的深浅拷贝、结构体的拷贝、或可能的深拷贝失败原因

本文探讨了Go语言中深拷贝与浅拷贝的区别,指出值类型默认深拷贝,引用类型如slice默认浅拷贝。通过示例展示了浅拷贝如何共享内存,以及深拷贝的必要性和如何在结构体中实现真正深拷贝,包括无指针情况下的直接赋值和有指针情况下的反射应用。

前言

大一学习C++基础时候便接触过这些概念,转Golang之后便没有再专门学习。直到前些日子的这场面试遇到一个问题——

深浅拷贝 修改拷贝的值,是否影响另一个?
浅拷贝什么时候影响,什么时候不影响?(浅拷贝只是拷贝了第一级,所以根据他内容判断,是值类型还是引用类型)(此部分没找见直接文章,待我回头研究一下源码。。)

再结合自己使用经历来看,的确许多时候,对于一个函数的传参、返回值是否需要带*,还都是有些模糊的。因此专程学习一下相关概念,进行记录。

深浅拷贝

本部分参考于此

简介

深拷贝:创建一个一样的新对象,新分配一块内存。新旧对象的修改操作互不影响。

浅拷贝:只复制指向对象的指针,新旧对象依旧是同一块内存,修改时一起修改。

Go语言中的深浅拷贝

值类型的数据,默认全部都是深复制,Array、Int、String、Struct、Float,Bool。
引用类型的数据,默认全部都是浅复制,Slice,Map。

测试代码

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	// 浅拷贝
	p1 := Person{Name: "Alice", Age: 25}
	p2 := &p1

	fmt.Printf("浅拷贝 - 修改前:\np1地址:%p\np2地址:%p\n", &p1, &p2)
	fmt.Printf("浅拷贝 - 修改前:\np1内容:%+v\np2内容:%+v\n", p1, p2)

	p1.Name = "修改后"
	fmt.Printf("浅拷贝 - 修改后:\np1地址:%p\np2地址:%p\n", &p1, &p2)
	fmt.Printf("浅拷贝 - 修改后:\np1内容:%+v\np2内容:%+v\n", p1, p2)

	// 深拷贝
	p3 := Person{Name: "Bob", Age: 30}
	p4 := p3
	fmt.Printf("\n深拷贝 - 修改前:\np3地址:%p\np4地址:%p\n", &p3, &p4)
	fmt.Printf("深拷贝 - 修改前:\np3内容:%+v\np4内容:%+v\n", p3, p4)

	p3.Name = "修改后"
	fmt.Printf("深拷贝 - 修改后:\np3地址:%p\np4地址:%p\n", &p3, &p4)
	fmt.Printf("深拷贝 - 修改后:\np3内容:%+v\np4内容:%+v\n", p3, p4)

	// new的情况下,看似是深拷贝其实是浅拷贝
	p5 := new(Person)
	p5.Name = "climber"
	p5.Age = 18
	p6 := p5
	fmt.Printf("\n new的情况下,看似是深拷贝其实是浅拷贝 - 修改前:\np5地址:%p\np6地址:%p\n", &p5, &p6)
	fmt.Printf("深拷贝 - 修改前:\np5内容:%+v\np6内容:%+v\n", p5, p6)

	p3.Name = "修改后"
	fmt.Printf("深拷贝 - 修改后:\np5地址:%p\np6地址:%p\n", &p5, &p6)
	fmt.Printf("深拷贝 - 修改后:\np5内容:%+v\np6内容:%+v\n", p5, p6)

}

测试结果:
浅拷贝:
在这里插入图片描述

深拷贝:
在这里插入图片描述

特殊情况:使用new
在这里插入图片描述

特别注意——使用new时会导致看似深拷贝其实浅拷贝

使用new时候,这里p6 := p5,我们发现二者同时进行了修改,但是地址却不一样。

这是因为new创建时候返回的是指针,所以我们p5存的是一个指向X区域的指针,这个指针在A区。进行p6 := p5时候,相当于对这个A区域的指针进行了复制,复制得到一个在B区的指针(因此打印出来的地址不一样)。但是这个A指针和B指针,都是指向了X区域,所以修改时候会同时修改。

特别注意——结构体的拷贝

顺着上一层new的探索继续思考(其实主要还是那天面试官给的提示…)
所谓深拷贝,其实也只是对第一层的内容进行复制,因此可能导致,复制前后的结构体中,所存的指针依旧指向同一个地址。
见示例代码:

package main

import "fmt"

type Person struct {
	Name   string
	Age    int
	Scores []int
}

func main() {
	// 创建原始结构体
	original := Person{
		Name:   "Alice",
		Age:    30,
		Scores: []int{90, 85, 80},
	}

	// 对原始结构体进行拷贝
	copy := original

	// 修改拷贝的结构体的参数
	copy.Name = "Bob"
	copy.Age = 25
	copy.Scores[0] = 95

	// 打印原始结构体和拷贝的结构体
	fmt.Println("Original:", original)
	fmt.Println("Copy:", copy)
}

结果如下:

在这里插入图片描述

我们发现,切片中的值被一起修改了,符合预期。

结构体如何实现真正的深拷贝?

通过上述测试可以看出,结构体中的地指针都是发生了浅拷贝,其余参数发生的是深拷贝。那么如何才能正确的实现整个结构体的深拷贝呢?

当结构体中,没有指针时,直接赋值就是深拷贝。

当结构体中存在指针和值时,可以通过反射等方法实现,具体见此文

<think>好的,用户想了解如何在Golang中附加结构体并读取其数据。首先,我需要明确“附加结构体”具体指什么。在Golang中,常见的结构体操作包括组合、嵌套、嵌入以及使用指针值方法进行操作。可能用户指的是将结构体嵌入到另一个结构体中,者通过某种方式关联两个结构体,以便访问数据。 接下来,我需要考虑如何解释结构体的组合和嵌入。Golang中没有继承,但可以通过嵌入结构体来实现类似的效果。例如,一个结构体可以嵌入另一个结构体,从而获得被嵌入结构体的字段和方法。这可能就是用户所说的“附加”结构体。然后,关于读取数据,需要说明如何通过点操作符访问嵌入结构体的字段,者如果使用嵌套结构体可能需要指定中间字段名。此外,可能需要提到标签(tags)的使用,比如JSONXML标签,用于序列化和反序列化数据,但用户的问题可能更偏向于结构体之间的组合而非数据格式的转换。 还需要考虑用户可能的实际应用场景,比如是否在构建一个具有扩展性的系统,者处理某种数据存储问题。根据用户提供的引用,他们之前提到了MapReader结构体,其中包含缓存和头信息,可能用户想了解如何将不同的结构体组合在一起,比如将读取器与缓存结合,者处理错误信息的结构体附加到主结构体上。另外,用户提供的引用中有关于错误处理的例子,使用指针接收器实现错误接口。这可能暗示用户对结构体方法以及接口实现也有兴趣,需要将这部分知识结合起来。需要确保回答的结构清晰,分步骤说明如何嵌入结构体,如何访问数据,可能还需要提到结构体标签的使用,以及通过方法操作结构体数据。同时,要给出代码示例,这样用户能更直观地理解。例如,展示一个包含嵌入结构体的例子,并演示如何访问其字段。 此外,需要注意用户提到的“附加”可能是指动态附加,但Golang是静态类型语言,无法在运行时动态附加结构体,但可以通过组合、接口者使用map等方式来模拟。因此,需要明确说明Golang结构体组合是编译时确定的,但可以通过设计模式来灵活使用。 最后,生成相关问题,帮助用户进一步学习,比如结构体嵌入与继承的区别,如何实现嵌套结构体的JSON序列化,结构体方法的指针与值接收器的区别等。</think>在Go语言中实现结构体的关联和数据读取,主要通过结构体组合、字段标签和反射机制实现。以下是具体实现方法: ### 一、结构体组合(嵌套) 通过嵌套结构体实现数据关联: ```go type Address struct { City string `json:"city"` Street string `json:"street"` } type User struct { Name string `json:"name"` Age int `json:"age"` Address Address `json:"address"` // 嵌套结构体 } func main() { user := User{ Name: "张三", Age: 30, Address: Address{ City: "北京", Street: "朝阳路", }, } // 读取嵌套数据 fmt.Println(user.Address.City) // 输出:北京 } ``` 这种组合方式在编译时确定类型关系,是Go语言推荐的结构设计方式[^3]。 ### 二、动态附加结构体 对于需要运行时动态附加数据的场景,可以使用以下方法: 1. **使用匿名结构体**: ```go type Base struct { ID int } func main() { extended := struct { Base Name string }{ Base: Base{ID: 1}, Name: "测试", } fmt.Println(extended.ID) // 输出:1 } ``` 2. **通过接口实现动态附加**: ```go type Attachable interface { Attach(key string, value interface{}) Get(key string) interface{} } type Container struct { data sync.Map } func (c *Container) Attach(key string, value interface{}) { c.data.Store(key, value) } func (c *Container) Get(key string) interface{} { val, _ := c.data.Load(key) return val } ``` ### 三、反射读取结构数据 使用`reflect`包读取结构体字段信息: ```go func PrintStruct(s interface{}) { val := reflect.ValueOf(s) typ := val.Type() for i := 0; i < val.NumField(); i++ { field := typ.Field(i) value := val.Field(i) fmt.Printf("%s (%s): %v\n", field.Name, field.Type, value.Interface()) } } ``` ### 四、结构体标签解析 结合反射读取字段标签: ```go type Example struct { Field string `customTag:"important"` } func main() { e := Example{"value"} t := reflect.TypeOf(e) field, _ := t.FieldByName("Field") fmt.Println(field.Tag.Get("customTag")) // 输出:important } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值