Go——切片困惑

2023-05-16

1、数组

Go的数组是有固定个相同类型元素的数据结构,底层采用连续的内存空间存放,数组一旦声明后大小就不可改变了。
注意:Go中的数组是一种基本类型,数组的类型不仅包括其元素类型,也包括其大小,[2]int和[5]int是两个完全不同的数组类型。

创建数组

  1. 声明时通过字面量进行初始化
  2. 直接声明,不显示地进行初始化
a := [3]int{1, 2, 3}
b := [...]int{1, 2, 3}
c := [3]int{1:1, 2:3}
var d [3]int 

数组名无论作为函数实参,还是作为struct嵌入字段,或者数组之间的直接赋值,都是值拷贝,不像C语言数组名因场景不同,可能是值拷贝,也可能是指针传递:C语言数组名作为函数实参传递时,直接退化为指针,int a[10]、int a[]、int *a在C语言中都是一个意思,就是一个指向int类型的指针;但是,当数组内嵌到C的struct里面时,又表现的是值拷贝的语义。

Go语言的数组不存在这种歧义,数组的一切传递都是值拷贝,体现在以下三个方面:

  1. 数组间的直接赋值。
  2. 数组作为函数参数。
  3. 数组内嵌到struct中。

下面以一个示例来证明这三条:

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	//直接赋值是浅拷贝
	b := a

	//修改a元素值并不影响b
	a[2] = 4

	fmt.Printf("%p,%v\n", &a, a) //0xc420016120,[1 2 4]
	fmt.Printf("%p,%v\n", &b, b) //0xc420016140,[1 2 3]

	//数组作为函数参数仍然是值拷贝
	f(a) //0xc4200161c0,[1 2 0]

	c := struct {
		s [3]int
	}{s: a}

	//结构是值拷贝,内部的数组也是值拷贝
	d := c

	//修改c中的数组元素值并不影响a
	c.s[2] = 20

	//修改d中的数组元素值并不影响d
	d.s[2] = 20

	fmt.Printf("%p,%v\n", &a, a) //0xc420016120,[1 2 4]
	fmt.Printf("%p,%v\n", &b, b) //0xc420016140,[1 2 3]
	fmt.Printf("%p,%v\n", &c, c) //0xc420016200,{[1 2 20]}
}

func f(a [3]int) {
	a[2] = 0
	fmt.Printf("%p,%v\n", &a, a)
}

2、切片

切片的创建

  1. 通过数组创建。
    array[b:e]创建一个包括e-b个元素的切片,第一个元素是array[b],最后一个元素是array[e-l]。
  2. make。
    通过内置的make函数创建,make([]T,len,cap)中的T是切片元素类型,len是长度,cap是底层数组的容量,cap是可选参数。
  3. 直接声明。
    可以直接声明一个切片,也可以在声明切片的过程中使用字面量进行初始化,直接声明但不进行初始化的切片其值为nil。例如:
var a []int //a is nil
var a []int []int(1,2,3,4}

切片数据结构

通常我们说切片是一种类似的引用类型,原因是其存放数据的数组是通过指针间接引用的。所以切片名作为函数参数和指针传递是一样的效果。切片的底层数据结构如下:
在这里插入图片描述
在这里插入图片描述

可以看到切片的数据结构有三个成员,分别是指向底层数组的指针、切片的当前大小和底层数组的大小。当len增长超过cap时,会申请一个更大容量的底层数组,并将数据从老数组复制到新申请的数组中。

nil切片和空切片

make([]int,0)var a []int创建的切片是有区别的。前者的切片指针有分配,后者的内部指针为0。示例如下:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	var a []int
	b := make([]int, 0)

	if a == nil {
		fmt.Println("a is nil") //a is nil
	} else {
		fmt.Println("a is not nil")
	}

	//虽然b的底层数组大小为0,但切片并不是nil
	if b == nil {
		fmt.Println("b is nil")
	} else {
		fmt.Println("b is not nil") //b is not nil
	}

	//使用反射中的SliceHeader来获取切片运行时的数据结构
	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	fmt.Printf("len=%d,cap=%d,type=%d\n", len(a), cap(a), as.Data) //len=0,cap=0,type=0
	fmt.Printf("len=%d,cap=%d,type=%d\n", len(b), cap(b), bs.Data) //len=0,cap=0,type=18212960
}

可以看到var a[]int 创建的切片是一个nil切片(底层数据没有分配,指针指向nil),数据结构如下:
在这里插入图片描述
可以看到make([]int, 0)创建的是一个空切片(底层数组指针费控,但底层数据是空的),数据结构如下:
在这里插入图片描述
看一下makeslice底层实现代码,就是到为什么make([]int, 0)创建的是一个空切片:

func makeslice(et *_type, len, cap int) slice {
	maxElements := maxSliceCap(et.size)
	if len < 0 || uintptr(len) > maxElements {
		panic(errorString("makeslice: len out of range"))
	}

	if cap < len || uintptr(cap) > maxElements {
		panic(errorString("makeslice: cap out of range"))
	}
	//调用mallocgc分配空间
	p := mallocgc(et.size*uintptr(cap), et, true)
	return slice{p, len, cap}
}

接下来看一下len和cap是0的情况下,mallocgc的代码片段

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	if gcphase == _GCmarktermination {
		throw("mallocgc called with gcphase == _GCmarktermination")
	}

	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
...

}

多个切片引用同一个底层数组引发的混乱

切片可以由数组创建,一个底层数组可以创建多个切片,这些切片共享底层数组,使用append扩展切片过程中可能修改底层数组的元素,间接地影响其他切片的值,也可能发生数组复制重建,共用底层数组的切片,由于其行为不明朗,不推荐使用。接下来看一个示例:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	a := []int{0, 1, 2, 3, 4, 5, 6}
	b := a[0:4]

	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	//a、b共享底层数组
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", a, len(a), cap(a), as.Data)
	fmt.Printf("b=%v,len=%d,cap=%d,type=%d\n", b, len(b), cap(b), bs.Data)

	b = append(b, 10, 11, 12)
	//a、b继续共享底层数组,修改b会影响共享的底层数组,间接影响
	fmt.Printf("a=%v,len=%d,cap=%d\n", a, len(a), cap(a))
	fmt.Printf("b=%v,len=%d,cap=%d\n", b, len(b), cap(b))

	//1en(b)=7,底层数组容量是7,此时需要重新分配数组,并将原来数组值复制到新数组
	b = append(b, 13, 14)
	as = (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs = (*reflect.SliceHeader)(unsafe.Pointer(&b))

	//可以看到a和b指向底层数组的指针已经不同了
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", a, len(a), cap(a), as.Data)
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n", b, len(b), cap(b), bs.Data)
}

//结果
a=[0 1 2 3 4 5 6],len=7,cap=7,type=842350559488
b=[0 1 2 3],len=4,cap=7,type=842350559488
a=[0 1 2 3 10 11 12],len=7,cap=7
b=[0 1 2 3 10 11 12],len=7,cap=7
a=[0 1 2 3 10 11 12],len=7,cap=7,type=842350559488
a=[0 1 2 3 10 11 12 13 14],len=9,cap=14,type=842350837872

问题总结:多个切片共享一个底层数组,其中一个切片的append操作可能引发如下两种情况。

  1. append追加的元素没有超过底层数组的容量,此种append操作会直接操作共享的底层数组,如果其他切片有引用数组被覆盖的元素,则会导致其他切片的值也隐式地发生变化。
  2. append追加的元素加上原来的元素如果超出底层数组的容量,则此种append操作会重新申请新数组,并将原来数组值复制到新数组。由于有这种二义性,所以在使用切片的过程中应该尽量避免多个切面共享底层数组,可以使用copy进行显式的复制。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go——切片困惑 的相关文章

  • requests.post处理Content-Type: multipart/form-data的请求

    前几天遇到一个需求 xff0c 要调用一个接口发送请求 xff0c 抓包之后得到的数据是这样的 上网看了一些资料得知 xff0c 原来这个接口的数据是通过multipart form data格式传过去的 xff0c multipart f
  • 上一步,下一步(撤销和恢复)

    var data 61 data count 61 0 data list 61 function regain function handleSaveCss 获取workspace body里面的内容 var c 61 34 worksp
  • Ubuntu下dpkg安装软件遇到包依赖问题的处理方法

    造冰箱的大熊猫 64 cnblogs 2019 9 10 向灵魂工程师致敬 xff01 在Ubuntu环境下通过dpkg命令安装deb包时 xff0c 如果遇到包依赖问题 xff0c 如 sudo dpkg i xxx deb Readin
  • Ubuntu18优化桌面版的运行速度

    一 刚开始使用Ubuntu18后 xff0c 感觉开机和运行速度都不理想 xff0c 通过改变一些配置可以提高下用户体验感 二 改变一些配置 a 使用Preload预加载 sudo apt install preload y b 禁用不必要
  • Debian安装mplayer,解决没有声音及声卡独占问题

    通过软件中心可以安装Gnome mplayer 本来以为这样这个播放器已经是 万能 的了 xff0c 但是最近下载了几个 mkv的电影 却发现Gnome mplayer没有办法打开 感觉很失望 在网上找了一番后 说只要下载源代码自己安装就行
  • CentOS7中安装MySQL5.7

    安装必要的组件 yum install y autoconf automake imake libxml2 devel expat devel cmake gcc gcc c 43 43 libaio libaio devel bzr bi
  • 20190708新的开始

    题目描述 发展采矿业当然首先得有矿井 xff0c 小 FF 花了上次探险获得的千分之一的财富请人在岛上挖了 n 口矿井 xff0c 但他似乎忘记考虑的矿井供电问题 为了保证电力的供应 xff0c 小 FF 想到了两种办法 xff1a 在这一
  • Debian安装JDK

    sudo tar zxvf jdk 8u60 linux x64 tar gz C usr local vi bashrc export JAVA HOME 61 usr local jdk1 8 0 60 export JRE HOME
  • Go——多值赋值和短变量声明

    1 多值赋值 可以一次性声明多个变量 xff0c 并可以在声明时赋值 xff0c 而且可以省略类型 xff0c 但必须遵守一定的规则要求 xff0c 具体看下面的示例 如下都是合法的 span class token comment 相同类
  • 「一本通 1.2 练习 2」扩散(loj10015)

    题目描述 一个点每过一个单位时间就会向 4 个方向扩散一个距离 xff0c 如图所示 xff1a 两个点 a b 连通 xff0c 记作 e a b xff0c 当且仅当 a b 的扩散区域有公共部分 连通块的定义是块内的任意两个点 u v
  • .db文件打开方式

    有时在工作中 xff0c 数据库格式db后缀的格式 xff0c 直接是打不开的 xff0c 所以我这里使用了数据库管理工具 xff0c 步骤如下 1 在电脑安装 Navicat Premium xff0c 安装后在桌面生成图标 xff0c
  • MathType的配置问题;将word中的公式转换为mathtype格式失败,缺少OMML2MML.XSL

    安装MathType后打开word报错 打开会出现以下问题 xff1a 首先 xff0c 把startup添加到word的信任中心 xff1a 要确保路径被office信任 依次打开word gt 文件 gt 选项 gt 信任中心 gt 信
  • XMPP系列(四)---发送和接收文字消息,获取历史消息功能

    今天开始做到最主要的功能发送和接收消息 获取本地历史数据 先上到目前为止的效果图 xff1a 首先是要在XMPPFramework h中引入数据存储模块 xff1a 聊天记录模块的导入 import 34 XMPPMessageArchiv
  • linux新增磁盘后,用fdisk等命令查询不到

    ls sys class scsi host xff08 会看到有host0 host1 hostN xff0c 对每个host进行如下操作 xff09 echo 34 34 gt sys class scsi host host0 sca
  • ubuntu上源码编译安装mysql5.7.27

    一 查看操作系统环境和目录结构 xff0c 并创建mysql用户和组 xff0c 以及规划安装mysql所需要的目录 cat etc issue 查看发行版本信息 xff1a cat proc version 查看正在运行的内核版本信息 u
  • (转-收集)MSSQL手工注入语句集合

    and exists select from sysobjects 判断是否是MSSQL and exists select from tableName 判断某表是否存在 tableName为表名 and 1 61 select 64 6
  • 滚动视图 UIScrollView

    UIScrollView xff1a 提供可以显 示 大于应 用窗 口的内容功能的控件 用户可以通过 手势使内容滚动和缩放 从 而查 看全部内容 初始化一个UIScrollView的对象 1 UIScrollView scroll 61 U
  • 基于steam的游戏销量预测 — PART 1 — 爬取steam游戏相关数据的爬虫

    语言 xff1a python 环境 xff1a ubuntu 爬取内容 xff1a steam游戏标签 xff0c 评论 xff0c 以及在 steamspy 爬取对应游戏的销量 使用相关 xff1a urllib xff0c lxml
  • WechatHelper

    using System using System Collections Generic using System Configuration using System IO using System Linq using System
  • Go——range复用临时变量

    range复用临时变量 span class token keyword package span main span class token keyword import span span class token string 34 s

随机推荐