Go新手别再被切片复制坑了

news2024/12/23 14:07:55

概述

Go 语言中切片的复制是非常重要也比较容易让新手困惑的问题。本文将通过大量示例代码,全面介绍切片复制的相关知识,包括:

  1. 切片的结构

  2. copy()函数的用法

  3. 切片复制的本质

  4. 浅复制和深复制的区别

  5. 如何实现切片深复制

  6. copy()函数的常见用途

  7. 切片复制需要注意的几点

1. 切片的结构

在讲解切片复制之前,我们先快速回顾下切片的结构。

切片是对数组的抽象和封装,所以切片实际上是一个包含三个字段的结构体:

type slice struct {  array *[ ]Type   len   int    cap   int}
  • array 指向底层数组

  • len 记录可用元素数量

  • cap 记录总容量

举个例子:

arr := [5]int{1, 2, 3, 4, 5}s := arr[1:3] // s引用arr的部分数据

这个切片 s 的结构大致如下:

s.array = &arrs.len = 2s.cap = 4

数组是值类型,但切片作为引用类型,所以切片之间赋值或传参时,只会复制引用,底层数组同一块内存被共享。

2. copy()函数用法

Go 语言内置的 copy()函数可以用于切片之间元素的复制,函数签名如下:

func copy(dst, src []Type) int

copy()会将 src 切片中的元素复制到 dst 中,复制长度以 len(src)和 len(dst)的最小值为准。它返回复制的元素个数。

使用 copy()复制切片:

s1 := []int{1, 2, 3}s2 := make([]int, 10) 
n := copy(s2, s1)
fmt.Println(s1, s2, n) // [1 2 3] [1 2 3 0 0 0 0 0 0 0] 3

这里我们把 s1 切片中的 3 个元素复制到 s2 中,n 返回复制的元素个数 3。

需要注意的是,copy()会先计算 dst 的长度 l=len(dst),再计算复制长度 n=min(len(src), l)。

再看一个例子:

s1 := []int{1, 2, 3}s2 := make([]int, 2)
n := copy(s2, s1) 
fmt.Println(s1, s2, n) // [1 2 3] [1 2] 2

s2 的长度只有 2,所以只复制了 s1 的前 2 个元素,n 返回 2。

3. 切片复制的本质

Go 语言中切片之间复制实际上是“引用的复制”,而不是值的复制。

也就是说,复制的是底层数组的引用,底层数组本身并没有复制。复制前后,src 和 dst 切片引用的是同一底层数组。

s1 := []int{1, 2, 3}  s2 := make([]int, 3)
copy(s2, s1) 
fmt.Println(&s1[0], &s2[0]) // 0xc0000180a8 0xc0000180a8

可以看到,s1 和 s2 的底层数组地址是一样的。修改 s2 会影响 s1:

s2[0] = 100fmt.Println(s1) // [100 2 3]

这种复制属于浅复制(shallow copy),类似于 C 语言中的 memcpy,只复制数组指针和相关属性。 

4. 浅复制和深复制

根据复制的层次,可以将切片复制分为浅复制和深复制

  • 浅复制:只复制切片的基本数据,底层数组共享

  • 深复制:复制切片及底层数组,break 引用关系

上面 copy()函数实现的是浅复制,如果需要深复制,需自己实现。

4.1 浅复制

浅复制只复制切片本身,底层数组共享,修改一个切片会影响另一个:

func main() {  s1 := []int{1, 2, 3}  s2 := shallowCopy(s1) // 浅复制
  s2[0] = 100  fmt.Println(s1) // [100 2 3]}
func shallowCopy(src []int) []int {  dst := make([]int, len(src))  copy(dst, src)  return dst}

浅复制对元素包含指针的切片也是问题:

type User struct {  id    int  name  *string}
func main() {  u1 := User{1, &name}
  u2 := shallowCopy([]User{u1})
  *u2[0].name = "newName" // 修改了u1.name} 

4.2 深复制

深复制需要自己实现,完全 break 底层数组引用关系:

func deepCopy(src []int) []int {  dst := make([]int, len(src))  for i := range src {    dst[i] = src[i]   }
  return dst}

这样修改 dst 不会影响到 src。

对于包含指针的切片,需要额外处理指针指向的内容。

5. 切片深复制实现

下面介绍几种实现切片深复制的方法。

5.1 手动循环赋值

可以通过手动循环一个个元素进行深复制:

func copyDeep(dst, src []int) {  for i := range src {    dst[i] = src[i]   }}

类似 for 循环的方式也可以用于自定义类型:

type User struct {  id   int  name string}
func copyUserDeep(dst, src []User) {  for i := range src {    dst[i].id = src[i].id    dst[i].name = src[i].name  }}

手动循环虽然稍微繁琐,但是性能和可控性较好。

   

5.2 利用反射

Go 语言反射可以自动深复制任意类型,但是需要注意反射带来的性能损耗:

import "reflect"
func copyDeep(dst, src interface{}) {  dv := reflect.ValueOf(dst).Elem()  sv := reflect.ValueOf(src).Elem()
  for i := 0; i < sv.NumField(); i++ {    fd := dv.Field(i)    if fd.CanSet() {      fd.Set(sv.Field(i))    }  }}

使用时:

var s1 []int = []int{1, 2, 3}s2 := make([]int, 3)
copyDeep(&s2, &s1)

反射的威力在于可以处理任意类型,但是需要注意反射带来的额外性能损耗。

5.3 利用 encoding/gob

gob 是一个二进制数据序列化的格式,可以用于深度 Copy:

import (  "bytes"  "encoding/gob")
func copyDeep(src, dst interface{}) error {  buff := new(bytes.Buffer)  enc := gob.NewEncoder(buff)  dec := gob.NewDecoder(buff)  if err := enc.Encode(src); err != nil {    return err  }
  if err := dec.Decode(dst); err != nil {    return err  }
  return nil}

使用 encoding/gob 进行深拷贝也有一定的性能损耗。

5.4 利用第三方库

如果需要频繁深拷贝,可以考虑使用一些第三方库,如:

  • github.com/jinzhu/copier

  • github.com/ulule/deepcopier

这些库利用反射实现泛型深拷贝,并进行了性能优化,会更高效。

   

6. copy()函数的常见用途

copy()作为切片浅复制的主要函数,使用场景还是很多的,主要有:

  • 切片扩容时复制老数据

  • 从一个切片截取部分元素到新切片

  • 切片重组,两个切片交换元素

  • 将字节流复制到字节切片缓冲

  • 文件拷贝等

6.1 切片扩容

Go 语言中切片扩容时,常用 copy()来复制老数据:

func appendSlice(slice []int) []int {  newSlice := make([]int, len(slice)+1)   copy(newSlice, slice)  return newSlice}

6.2 截取切片

从一个大切片截取需要的部分到新切片:

bytes := []byte("Hello World")
hello := make([]byte, 5)copy(hello, bytes[:5]) 
world := make([]byte, 5)copy(world, bytes[6:])

6.3 切片重组

两个切片可以通过 copy 相互交换元素:

s1 := []int{1, 2, 3}s2 := []int{4, 5}
copy(s1, s2) copy(s2, s1)

交换后 s1=[4,5,3],s2=[1,2]。

6.4 字节流复制

IO 操作读取字节流时,常用 copy()写入字节切片缓冲:

buf := make([]byte, 1024)
for {  n, err := r.Read(buf)  // 使用buf前N字节}

6.5 文件复制

利用 copy()可以实现高效的文件拷贝:​​​​​​​

func CopyFile(dst, src string) error {  r, w := os.Open(src), os.Create(dst)  defer r.Close(); defer w.Close()    buf := make([]byte, 1024*1024)  for {    n, err := r.Read(buf)    if err != nil {      if err == io.EOF {        break      }      return err      }
    if n == 0 {      break    }
    w.Write(buf[:n])  }  return nil}

7. 注意事项

最后需要注意几点:

  • copy()要求 dst 必须提前分配内存,否则会 panic

  • 指针或包含指针的切片只会复制指针,不会深复制目标对象

  • 多次复制切片会造成 GC 负担,尽量复用内存减少不必要的复制

8​​​​​​​. 思考题

  • 描述下切片的结构包含哪些字段

  • copy()函数签名是什么

  • 切片复制的本质是什么

  • 如何实现切片的深复制

  • copy()函数有哪些常见的使用场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1368879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GB28181视频汇聚平台EasyCVR级联后,部分通道视频无法播放是什么原因?

GB28181协议智慧安防平台EasyCVR是基于各种IP流媒体协议传输的视频汇聚和融合管理平台。视频流媒体服务器EasyCVR采用了开放式的网络结构&#xff0c;支持高清视频的接入和传输、分发&#xff0c;平台提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制…

如何计算JMeter性能和稳定性测试中的TPS?

1、普通计算公式 TPS 总请求数 / 总时间 按照需求得到基础数据&#xff0c;比如在去年第xxx周&#xff0c;某平台有5万的浏览量那么总请求数我们可以估算为5万&#xff08;1次浏览都至少对应1个请求&#xff09; 总请求数 50000请求数 总时间&#xff1a;由于不知道每个请…

CAN FD和传统CAN的组网技术

在汽车领域&#xff0c;特别是新能源汽车技术的发展&#xff0c;对汽车内部的数据传输带宽提出了越来越高的要求&#xff0c;因此新的总线协议CAN FD应运而生&#xff0c;其最大传输速率可达8Mbps。然而由于历史原因以及成本因素&#xff0c;在相当长的一段时间内&#xff0c;传…

python 工作目录 与 脚本所在目录不一致

工作目录&#xff1a;执行脚本的地方 我以为工作目录会是当前执行脚本的目录位置&#xff0c;但其实不是&#xff0c;例如&#xff1a; 图中红色文件为我执行的脚本文件&#xff0c;但是实际的工作目录是PYTHON LEARNING 可以用如下代码查询当前工作目录&#xff1a; import os…

游戏引擎支持脚本编程有啥好处

很多游戏引擎都支持脚本编程。Unity、Unreal Engine、CryEngine等大型游戏引擎都支持使用脚本编写游戏逻辑和功能。脚本编程通常使用C#、Lua或Python等编程语言&#xff0c;并且可以与游戏引擎的API进行交互来控制游戏对象、设置变量、执行行为等。使用脚本编程&#xff0c;游戏…

【Proteus仿真】【Arduino单片机】路灯控制系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器&#xff0c;使用LCD1602显示模块、人体红外传感器、光线检测模块、路灯继电器控制等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示时间、工作…

TCP三次握手过程?

TCP三次握手过程&#xff1f; 分享 回答 1 浏览 3662 一颗小胡椒 2 CISM-WSE CISP-PTS 三次握手是 TCP 连接的建立过程。在握手之前&#xff0c;主动打开连接的客户端结束 CLOSE 阶段&#xff0c;被动打开的服务器也结束 CLOSE 阶段&#xff0c;并进入 LISTEN 阶段。随后进入…

LeetCode(209)长度最小的子数组⭐⭐

给定一个含有 n 个正整数的数组和一个正整数 s &#xff0c;找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组&#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0。 示例&#xff1a; 输入&#xff1a;s 7, nums [2,3,1,2,4,3]输出&#xff1a;2…

Linux———head,tail命令详解(狠狠爱住)

目录 head 命令&#xff1a; head 命令基本语法&#xff1a; 常用选项 示例 显示文件的前 10 行&#xff1a; 显示文件的前 5 行&#xff1a; 显示文件的前 100 个字节&#xff1a; 不显示文件名的标题信息&#xff1a; 显示文件名的标题信息&#xff1a; tail 命令&…

网络原理OSI

目录 一、应用层 1、功能 2、协议的分类 二、UDP原理&#xff08;传输层&#xff09; 1、协议端格式 2、格式解释 三、TCP原理&#xff08;传输层&#xff09; 1、协议端格式 2、长度 3、可靠传输 &#xff08;1&#xff09;确认应答 &#xff08;2&#xff09;超时…

RBAC权限管理概念

基于RBAC模型的权限设计&#xff1a;如何设计系统权限体系&#xff1f; | 人人都是产品经理 一&#xff0c;什么是RBAC RBAC(基于角色的权限控制)模型的核心是在用户和权限之间引入了角色的概念。取消了用户和权限的直接关联&#xff0c;改为通过用户关联角色、角色关联权限的…

软件测试|MySQL主键自增详解:实现高效标识与数据管理

简介 在MySQL数据库中&#xff0c;主键自增是一种常见的技术&#xff0c;用于自动为表中的主键字段生成唯一的递增值。本文将深入讨论MySQL主键自增的原理、用途、使用方法&#xff0c;以及在实践中的注意事项和最佳实践。 主键自增 主键自增的原理 主键自增是通过使用AUTO…

蓝桥杯 python 第二题 数列排序

这里给出一种解法 """ # 错的 n int(input()) dp[int(i) for i in input().split(" ")] dp.sort() print(" ".join(str(i) for i in dp)) """#这个是对的 num int(input())l list(map(int, input().split()))l.sort()pr…

CLIP论文总结

文章目录 NLP的积淀Method1. 预训练的方法&#xff1a;放宽约束&#xff1a;对比学习2. 模型训练训练时间 ExperimentsMotivationPrompt&#xff1a;提示&#xff1a;也就是文本的引导作用Prompt enginneringPrompt ensembling 对比实验 NLP的积淀 取之不尽用之不竭的自监督信…

常孝元宇宙《神由都城》发布会成功召开

2024年1月9日,2024常孝元宇宙《神由都城》发布会在北京市中国科技会堂举办,由中国移动通信联合会元宇宙产业工作委员会主办,常州神由之星数字信息产业发展有限公司、常州孝道文化产业股份有限公司共同承办。 本次发布会以“创新引领、协同发展”为主题,邀请第十二届全国政协副主…

海淘注意事项科普

关税和税费&#xff1a; 在海淘过程中&#xff0c;您可能需要支付关税和其他税费。了解目标国家的相关规定&#xff0c;预先了解可能的费用&#xff0c;并确保考虑到这些额外成本。 货币汇率&#xff1a; 注意货币汇率的波动&#xff0c;以避免因兑换率变化导致支付更多费用。…

Unity | Shader基础知识(第九集:shader常用单词基础知识速成)

目录 一、顶点&#xff08;Vertex&#xff09;和法线(Normal) 二、UV信息 三、 基础数据种类 1 基础数据种类 2 基础数据数组 3 基础数据数组的赋值 4 对数据数组的调用 四、 基础矩阵 1 基础矩阵种类 2 对矩阵数组的调用 2.1对一个数据的调用 2.2对多个数据的调用 2…

【华为】IPsec VPN 实验配置(动态地址接入)

【华为】IPsec VPN 实验配置&#xff08;动态地址接入&#xff09; 注意实验需求配置思路配置命令拓扑R1基础配置配置第一阶段 IKE SA配置第二阶段 IPsec SA ISP_R2基础配置 R3基础配置配置第一阶段 IKE SA配置第二阶段 IPsec SA PCPC1PC2 检查建立成功查看命令清除IKE / IPsec…

视频号小店和抖音小店相比,新手做哪个比较好?

我是电商珠珠 抖音小店在19年被抖音所发展&#xff0c;在这过程中&#xff0c;抖音小店通过自身的不断完善&#xff0c;从兴趣电商到全域兴趣电商模式&#xff0c;从直播电商到商城的出现&#xff0c;凭借着门槛低流量高的优势&#xff0c;让很多商家尝到了红利。 尤其是在20…