Go八股(Ⅳ)***slice,string,defer***

news/2024/11/7 6:45:33/

***slice,string,defer***

1.slice和arry的区别

arry:

Go语言中arry即为数据的一种集合,需要在声明时指定容量和初值,且一旦声明就长度固定,访问时按照索引访问。通过内置函数len可以获取数组中的元素个数。

初始化

数组在初始化时必须指定大小和初值,不过Go语言为我们提供了其他灵活的方式。

例如:

func main() {var arr [5]int //声明了一个大小为5的数组,初始化值为{0,0,0,0,0}arr := [5]int{1}//声明并初始化一个大小为5的数组,初始值为{1,0,0,0,0}arr := [...]int{1, 2, 3}//通过“...”自动获取数组长度,初始化后值为{1,2,3}arr := [...]int{4:1}//指定序列号为4的元素数值为1,初始值为{0,0,0,0.1}
}

数组作参数传入

Go语言数组作为参数传入时,必须指定参数数组的大小,且传入的大小必须与指定的大小一致,数组为按值传递的,函数内对数组的值的改变不影响初始数组

例如:

package mainimport "fmt"func PrintArry(arr [5]int) {arr[0] = 5fmt.Println(arr)
}func main() {var arr [5]int = [5]int{1, 2, 3, 4, 5}PrintArry(arr)fmt.Println(arr)
}

运行结果确实这样的:

Slice

切片是Go语言中极为重要的一种数据类型,可以理解为动态长度的数组(虽然实际上Slice结构内包含了一个数组),访问时可以按照数组的方式访问,也可以通过切片操作访问。Slice有三个属性:指针、长度和容量。指针即Slice名,指向的为数组中第一个可以由Slice访问的元素;长度指当前slice中的元素个数,不能超过slice的容量;容量为slice能包含的最大元素数量,但实际上当容量不足时,会自动扩充为原来的两倍。通过内置函数lencap可以获取slice的长度和容量

初始化

Slice在初始化时需要初始化指针,长度和容量,容量未指定时将自动初始化为长度的大小。可以通过获取数组的引用,获取数组/Slice的切片构建或是make函数初始化数组。

例如

s:=[]int{1,2,3}//通过数组的引用初始化,值为{1,2,3},长度和容量为3arr:=[5]int{1,2,3,4,5}
s:=arr[0:3] //通过数组的切片初始化,值为{1,2,3},长度和容量为5s:=make([]int,4)//通过make初始化,值为{0,0,0,0},长度和容量wei4s:=make([]int,3,5)//通过make初始化值为{0,0,0},长度为3,容量为5

气质特别要注意的时通过切片方式初始化。若时通过对Slice的切片进行初始化,实际上初始化之后的结构如图所示:

此时x的值为[2,3,5,7,11],y的值为[3,5,7],且两个slice的指针指向的是同一个数组,也即x中的元素的值的改变将会导致y中的值也一起改变

这样的初始化方式可能会导致内存被过度占用,如只需要使用一个极大的数组中的几个元素,但是由于需要指向整个数组,所以整个数组在GC时都无法被释放,一直占用内存空间。故使用切片操作进行初始化时,最好使用append函数将切片出来的数据复制到一个新的slice中,从而避免内存占用陷阱。

Slice作为函数参数

Go语言中Slice作为函数参数传递时为按引用传递的,函数内对Slice内元素的修改将导致函数外的值也发生改变,不过由于传入函数的时一个指针的副本,所以对该指针的修改不会导致原来的指针的变化(例如append不会改变原来slice的值)。

例如

func PrintSlice(s []int) {s = append(s, 4)s[0] = -1fmt.Println(s)
}func main() {s := []int{1, 2, 3, 4, 5}s1 := s[0:3]fmt.Println("s:", s)fmt.Println("s1:", s1)PrintSlice(s1)fmt.Println("s:", s)fmt.Println("s1:", s1)
}

总的来说

  • 数组长度不能改变,初始化后长度就是固定的;切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
  • 结构不同,数组是一串固定数据,切片描述的是截取数组的一部分数据,从概念上说是一个结构体。
  • 初始化方式不同,如上。另外在声明时的时候:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。
  • unsafe.sizeof的取值不同,unsafe.sizeof(slice)返回的大小是切片的描述符,不管slice里的元素有多少,返回的数据都是24字节。unsafe.sizeof(arr)的值是在随着arr的元素的个数的增加而增加,是数组所存储的数据内存的大小。
  • 函数调用时的传递方式不同,数组按值传递,slice按引用传递。

Slice的扩容机制

1.18版本之前

Go1.18之前切片的扩容是以容量1024为临界点,当旧容量 < 1024个元素,扩容变成2倍;当旧容量 > 1024个元素,那么会进入一个循环,每次增加25%直到大于期望容量。

1.18版本之后

当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

有什么好处呢,首先是双倍容量扩容的最大阈值从1024降为了256,只要超过了256,就开始进行缓慢的增长。其次是增长比例的调整,之前超过了阈值之后,基本为恒定的1.25倍增长,而现在超过了阈值之后,增长比例是会动态调整的,随着切片容量的变大,增长比例逐渐向着1.25进行靠近

内存对齐

以下是内存对齐得源码;

switch {// 当数组元素的类型大小为1时,不需要乘除计算就能够得到所需要的值  case et.size == 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)//前面两个语句只是对老长度和预期cap的类型转换,关键是下一个语句决定了newcap的长度// 内存对齐capmem = roundupsize(uintptr(newcap))// 判断是否溢出overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)// 当类型大小是8个字节时  case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)// 当类型大小是2的幂次方时  case isPowerOfTwo(et.size):var shift uintptrif sys.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.Ctz64(uint64(et.size))) & 63} else {shift = uintptr(sys.Ctz32(uint32(et.size))) & 31}lenmem = uintptr(old.len) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)// 当大小不是上面任何一种时  default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}

之所以进行内存对齐,是因为更加合理得分配内存,如果分配得太多就会出现内存得浪费,如果分配得太少就会出现性能过低情况。

_MaxSmallSize: 其值为32768,即32kb大小。在Go中,当对象大小超过32kb时,内存分配策略和小于等于32kB时是有区别的。(对于内存大于32KB的称为大对象,会单独处理,对于内存小于等于32KB的对象,会在跨度类数组中找到合适的数组大小,其实这一步也就进行了内存对齐操作,找到了最小的对齐内存,所以往往newcap大小会比之前的稍有不同,一般都是向上取了一些值)
smallSizeMax: 其值为1024字节。
smallSizeDiv: 其值为8字节。
largeSizeDiv: 其值为128字节。
_PageSize: 8192字节,即8kb大小。Go按页来管理内存,而每一页的大小就为8kb。
class_to_size:Go中的内存分配会按照不同跨度(也可理解为内存大小,有点类似于段),其中跨度是指,go每一页的大小是8kb,对datablock划分成不同大小的内存块,
除了最小的8b,其余的大小都是8*2n,即8,16,32,48,…32768,具体规则间隔为8,16,32,64,128…,对应class_to_size的数组(1.18之后好像多了一个24元素)
将内存分割成不同内存块链表。当需要分配内存时,按照对象大小去匹配最合适的跨度找到空闲的内存块儿。Go中总共分为67个跨度,class_to_size是一个长度为68的数组,分别记录0和这67个跨度的值。
size_to_class8: 这是一个长度为129的数组,代表的内存大小区间为0~1024字节。以索引i为例,此位置的对象大小m为i
smallSizeDiv,size_to_class8[i]的值为class_to_size数组中跨度最接近m的下标。
size_to_class128:这是一个长度为249的数组,代表的内存大小区间为1024~32768字节。以索引i为例,此位置的对象大小m为smallSizeMax
i*largeSizeDiv, size_to_class128[i]的值为class_to_size数组中跨度最接近m的下标。
divRoundUp: 此函数返回a/b向上舍入最接近的整数。
alignUp: alignUp(size, _PageSize) = _PageSize * divRoundUp(size,
_PageSize)。

 上面得一大块内容,简而言之就是Go语言未来更好得分配内存,将每次扩容得量划分为67个区间

例如:

s3 := []int{1, 2}
s3 = append(s3, 3, 4, 5)
fmt.Println(cap(s3))

根据前文知,所需容量为5,又因所需容量大于2倍当前容量,故新容量也为5。

又因为int类型大小为8(等于64位平台上的指针大小),所以实际需要的内存大小为5 * 8 = 40字节。而67个跨度中最接近40字节的跨度为48字节,所以实际分配的内存容量为48字节。

最终计算真实的容量为48 / 8 = 6,和实际运行输出一致。

零切片,空切片,nil切片的区别

零切片

简单来说就是切片中的值都为0,切片已经分配空间,并且值也不为空

// 创建零切片
slice4 := make([]int,2,5)
fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 输出:[0 0] {824634474496 2 5}

空切片

空切片就是已经初始化过空间的切片,但是切片中并没有内容

通常用make或者字面量进行初始化

s1 := []int{} // s1 是一个空切片,通过字面量创建
s2 := make([]int, 0) // s2 也是一个空切片,通过 make 创建

nil切片

通常使用var 来定义,既没有分配空间,更不用说切片的长度

var slice []int
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {0 0 0}

 string类型

string标准概念

在go的标准包中定义如下:

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
  • string是8bit字节的集合,通常是但并不一定非得是UTF-8编码的文本。
  • string可以为空(长度为0),但不会是nil。
  • string对象不可以修改。
type stringStruct struct {str unsafe.Pointer		//字符串首地址,指向底层字节数组的指针len int					//字符串长度
}

对于字符串Hello,实际底层结构如下:

3.string类型的操作

3.1  声明

var str string
str = "Hello"

具体的字符串构建过程,是先根据字符串构建stringStruct,再转换成string:

func gostringnocopy(str *byte) string {//根据字符串地址构建stringss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} //先构造stringStructs := *(*string)(unsafe.Pointer(&ss))//再将stringStruct转换成stringreturn s
}

3.2.1  []byte转string

[]byte切片转换成string很简单(语法上):

func GetStringBySlice(s []byte) string {return string(s)
}

下面是转化时的内存图:

转换过程如下几步:

  1. 根据切片的长度申请内存空间,假设内存地址为p,切片长度为len(s)
  2. 构建string(sting.str =p; string.len=len)
  3. 拷贝数据(切片中数据拷贝到新申请的内存空间)

 3.2.2 string类型转[]byte

 下面是转化的代码,语法上很简单

func GetSliceByString(str string) []byte {return []byte(str)
}

同样string类型转化成[]byte类型也需要一次内存的拷贝。

1.申请切片内存空间

2.将string拷贝到切片

3.3 字符串的拼接

在Go语言中,字符串是不可变得,拼接字符串事实上是创建了一个新的字符串,如果代码中存在大量的字符串拼接,对性能会产生影响。

下面是go语言中关于拼接字符串的源码:

func concatstrings(buf *tmpBuf, a []string) string {idx := 0l := 0 //拼接后的字符串总长度count := 0for i, x := range a {n := len(x)if n == 0 {continue}if l+n < l {throw("string concatenation too long")}l += ncount++idx = i}if count == 0 {return ""}// If there is just one string and either it is not on the stack// or our result does not escape the calling frame (buf != nil),// then we can return that string directly.if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {return a[idx]}s, b := rawstringtmp(buf, l)//生成指定大小的字符串,返回一个string和切片,二者共享内存for _, x := range a {copy(b, x)b = b[len(x):]//string无法修改,只能通过切片修改}return s
}// 生成一个新的string,返回的string和切片共享相同的空间
func rawstring(size int) (s string, b []byte) {p := mallocgc(uintptr(size), nil, false)stringStructOf(&s).str = pstringStructOf(&s).len = size*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}return
}

3.3.1常见的拼接方式

使用“+”

s1+s2+s3

使用fmt.Sprintf

fmt.Sprintf("%s%s",s1,s2)

使用strings.Builder

func BuilderConcat(n int, str string) string {var builder strings.Builderfor i := 0; i < n; i++ {builder.WriteString(str)}return builder.String()
}

使用bytes.Buffer

func bufferConcat(n int, s string) string {buf := new(bytes.Buffer)for i := 0; i < n; i++ {buf.WriteString(s)}return buf.String()
}

 使用[]byte

func byteConcat(n int, str string) string {buf := make([]byte, 0)for i := 0; i < n; i++ {buf = append(buf, str...)}return string(buf)
}

3.4 字符串的截取

1.截取普通英语字符串

str := "HelloWorld"
content := str[1 : len(str)-1] 

2.截取带中文的字符串

一个中文字符确定不止一个字节,需要先将其转为[]rune,再截取后,再转为string

strRune := []rune(str)
fmt.Println("string(strRune[:4]) = ",string(strRune[:4]))

4.为什么字符串不允许修改(只读属性)

在go实现中,string不包含内存空间,只有一个内存的地址,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。

string通常指向字符串字面量,而字符串字面量存储存储位置是只读段,而不是堆或栈上,所以string不可修改。

修改字符串时,可以将字符串转换为 []byte 进行修改。

var str string = "hello"
strBytes := []byte(str)
strBytes[0] = 'H'
str = string(strBytes)
fmt.Println(str)

defer

defer

一个函数中多个defer的执行顺序

defer 的作用就是把defer关键字之后的函数执行压入一个栈中延迟执行,多个defer的执行顺序是后进先出LIFO,也就是先执行最后一个defer,最后执行第一个defer

func main() {defer fmt.Println(1)defer fmt.Println(2)defer fmt.Println(3)
}

return返回值的运行机制

1.返回值赋值

2.RET指令

而defer执行在赋值之后,RET之前。

defer,return,返回值三者执行的顺序是:return最先执行,先将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带返回值退出

 不带命名返回值

如果函数的返回值是无名的(不带命名的返回值),则go语言会在执行return的时候执行一个类似创建一个临时变量作为保存return值得动作。

func main() {fmt.Println("return i:", test())
}func test() int {i := 0defer func() {i++fmt.Println("defer1 ---i:", i)}()defer func() {i++fmt.Println("defer2 ---i:", i)}()return i
}

运行结果如下图所示:

 

如图所示,函数执行时先返回值然后再执行defer之后得函数。

上面得例子实际上进行了三步操作:

(1)赋值,因为返回值没有命名,所以return默认指定了一个返回值(假设为s),首先将i赋值为s,i初始值是0,所以s也是0

(2)后续的defer操作因为是针对i进行的,所以不会影响s,此后s不会更新,所以s还是0

(3)返回值,return s,也就是return 0

var i int

s:=i

return s

带命名的返回值

有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个给返回值(虽然defer实在return之后执行的),由于使用函数定义的变量,所以执行defer操作后会对该变量的修改会影响的return 的值

func main() {fmt.Println("return i:", test())
}func test() (i int) {defer func() {i++fmt.Println("defer1 ---i:", i)}()defer func() {i++fmt.Println("defer2 ---i:", i)}()return i
}

运行结果如下;

这种情况其实就相当于一直在操作一个内存地址中的数。


http://www.ppmy.cn/news/1544984.html

相关文章

VisionPro —— 颜色匹配工具详解

颜色提取工具CogColorExtractorTool 从彩色图像中抽取像素来创建灰度图像 CogColorExtractorTool简介 使用Color Extractor工具从彩色图像中提取像素&#xff0c;以便根据指定的参考颜色创建亮像素和暗像素的灰度图像。该工具还生成一个彩色图像&#xff0c;该图像可用作诊断…

华为eNSP:mux-vlan

一、什么是mux-vlan&#xff1f; Mux-vlan 是一种多路复用的虚拟局域网&#xff08;Virtual Local Area Network&#xff09;技术。它将多个不同的VLAN流量转发到同一个物理端口&#xff0c;从而实现VLAN间的互通。 在传统的以太网环境中&#xff0c;每个VLAN通常都有一个独立…

基于卷积神经网络(CNN)的时间序列预测,15个输入1个输出,可以更改数据集,MATLAB代码

1. 数据收集与预处理 数据清洗&#xff1a;处理缺失值、异常值等。特征工程&#xff1a;提取有助于预测的特征。数据标准化&#xff1a;将时间序列数据标准化&#xff0c;使其具有零均值和单位方差&#xff0c;有助于模型训练。滑动窗口划分&#xff1a;将时间序列数据划分为多…

linux资源优化

1.思考步骤 查看资源使用的详细情况根据使用情况决定分配策略修改资源分配 2.如何查看运行中的使用的详细情况&#xff1f; 进程跟踪&#xff1a;top查看打开的文件&#xff1a;lsof lsof的输出、包含以下字段。  COMMAND&#xff1a;拥有文件描述符的的进程对应的命令…

速盾:高防cdn遭受攻击会瘫痪吗?

在互联网时代&#xff0c;网络安全问题日益突出&#xff0c;各种攻击手段层出不穷。在这个背景下&#xff0c;高防CDN&#xff08;Content Delivery Network&#xff09;作为一种常见的安全防护技术&#xff0c;受到了广泛应用。但是&#xff0c;很多人对高防CDN遭受攻击后的表…

每日OJ题_牛客_小红的口罩_堆+贪心_C++_Java

目录 牛客_小红的口罩_堆贪心 题目解析 C代码 Java代码 牛客_小红的口罩_堆贪心 小红的口罩 描述&#xff1a; 疫情来了&#xff0c;小红网购了 n个口罩。众所周知&#xff0c;戴口罩是很不舒服的。小红每个口罩戴一天的初始不舒适度为 ai​。 小红有时候…

SQL中的IN语句和EXISTS语句

大家好&#xff0c;使用SQL时经常需要根据其他表的值过滤数据&#xff0c;常见方法是使用IN和EXISTS子句。这两者都用于检查子查询中值的存在&#xff0c;但它们的工作方式略有不同&#xff0c;并可能对性能产生不同影响。本文将探讨IN和EXISTS的定义、工作原理及其使用场景&am…

Spring 中的各种 Editor,其实都是java.beans包中PropertyEditor的实现类

java Bean规范中有这样一个接口,PropertyEditor&#xff0c;从这个接口的名字来看&#xff0c;是用来进行编辑属性的&#xff0c;那自然是编辑对象的属性。 1.为什么需要属性编辑器呢 我们通常会在类型定义各种类型的属性&#xff0c;通常我们自己通过new创建对象&#xff0c;并…