Go学习

slice:

slice 的底层数据其实是 数组,slice 是对数组的封装,它描述一个数组的片段。slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

type slice struct {
	array unsafe.Pointer*  //元素指针
	len int  //长度
	cap int  //容量
}

扩容

1.17及以前:

如果期望容量大于当前容量的两倍就会使用期望容量;

如果当前切片的长度小于 1024 就会将容量翻倍;

如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

Go1.18及以后,引入了新的扩容规则:

当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;

原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4;

数组和切片的区别?

数组:数组固定长度。数组长度是数组类型的一部分,数组需要指定大小,不指定也会根据初始化,自动推算出大小, 大小不可改变。数组是通过值传递的。

切片(slice):切片可以改变长度。切片是轻量级的数据结构,三个属性,指针,长度,容量,不需要指定大小。切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容。

从一个切片截取出另一个切片,修改新切片的值会影响原来的切片内容吗?

在截取完之后,如果新切片没有触发扩容,则修改切片元素会影响原切片,如果触发了扩容则不会。

package main
import "fmt"
func main() {
	slice := []int{0,1,2,3,4,5,6,7,8,9}   // len=10  cap=10
	s1 := slice[2:5]  // [2 3 4]   len=5-2=3,  cap=10-2=8
	s2 := s1[2:6:7]  // [4 5 6 7]   len=6-2=4, cap=7-2=5
	
	s2 = append(s2, 100)
	s2 = append(s2, 200)
	s1[2] = 20
	
	fmt.Println(s1)  // [2 3 20]
	fmt.Println(s2)  // [4 5 6 7 100 200]
	fmt.Println(slice)  // [0 1 2 3 20 5 6 7 100 9]
}

示例:
slice  |0|1|2|3|4|5|6|7|8|9|
s1         |2|3|4| | | | | |
s2             |4|5|6|7| |

第一次 s2 追加100时,s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,slice 和 s1 都可以看得到。

第二次 s2 追加200时,s2 的容量不够用,该扩容了。于是,s2 将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

最后,修改 s1 索引为2位置的元素,这次只会影响原始数组相应位置的元素。它影响不到 s2 了。

注意:打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

slice作为函数参数传递,会改变原slice吗?

当 slice 作为函数参数时,因为会拷贝一份新的 slice 作为实参,所以原来的 slice 结构并不会被函数中的操作改变,也就是说,slice 其实是一个结构体,包含了三个成员:len, cap, array 并不会变化。

但是需要注意的是,尽管slice结构不会变,但是其底层数组的 数据 如果有修改的话,则会发生变化。若传的是 slice 的指针,则原 slice 结构会变,底层数组的数据也会变。

package main
import "fmt"
func myAdd(s []int) {
	// i只是一个副本,不能改变s中元素的值
	/*for _, i := range s {
		i++
	}*/
	
	for i := range s {
		s[i] += 1  //会改变s中元素的值
	}
}

func myAppend(s []int) []int {
	// 这里 s 虽然改变了,但并不会影响外层函数的 s
	s = append(s, 100)
	return s
}

func myAppendPtr(s *[]int) {
	// 会改变外层 s 本身
	*s = append(*s, 100)
	return
}

func main() {
	s := []int{1,1,1}
	myAdd(s)
	newS := myAppend(s)
	fmt.Println(s)   // [2 2 2]
	fmt.Println(newS)  // [2 2 2 100]
	
	s = newS
	myAppendPtr(&s)
	fmt.Println(s)  // [2 2 2 100 100]
}

内存逃逸现象什么?

逃逸是指在函数内部创建的,在函数结束后仍被其他部分引用或持有。是编译器在程序编译时期根据逃逸分析策略,将原本应该分配到栈上的对象分配到堆上的一个过程。

识别不必要的场景,规避不必要的内存逃逸:闭包函数是对内存逃逸合理的利用。

考察內存管理,堆栈的区别:

栈上的内存分配和释放由编译器自动管理,速度快但空间有限。

堆上的内存分配需要运行时的系统的参与,相对较慢但空间较大,由GC回收

影响:

因为堆对象需要 垃圾回收机制 来 释放内存,栈对象会跟随函数结束被编译器回收,所以大量的内存逃逸会给 GC 带来压力,回收的开销增加;指针引用和内存安全;内存泄露风险

导致内存逃逸的原因:

栈空间不足时,无法容纳大变量,会导致内存逃逸;

作用域发生变化,使得函数内部的变量,被外部函数或变量所引用,导致函数执行完后,变量还存在;

编译时无法确定类型或大小。

主要逃逸场景:

返回局部变量指针:函数返回内部变量的地址,变量必须逃逸到堆上

interface{}类型:传递给interface{}参数的具体类型会逃逸,因为需要运行时类型信息

闭包引用外部变量:被闭包捕获的变量会逃逸到堆上

切片/map动态扩容:当容量超出编译期确定范围时会逃逸

大对象:超过栈大小限制的对象直接分配到堆上

func f1() (*int, []int, map[int]int) {
    i := 0
	list := []int {1,2,3,4}
	mp := map[int]int{1:1, 2:2}
	return &i, list, mp  //返回指针、slice和map,会发生逃逸
}

func f2() {
    i := 2
	ch := make(chan *int, 2)
	ch <- &i  //向chan中发送数据的指针或包含指针的值(chan是作为协程兼通讯的通道)
	<-ch  //函数结束后,ch会被销毁,但ch里面的值可能会被其他协程或方法引用
}

func f3() func() {  //返回一个函数
    i := 1
	return func() {
	    fmt.Println(i)  //非直接的函数调用,比如在闭包中引用包外的值,因为闭包执行的生命周期可能会超过函数周期
	}
}

func f4() {
    i := 1
	list := make([]*int, 10)
	list[0] = &i  //在slice或map中存储指针或包含指针的值,slice或map都需要动态分配内存来保存数据
}

func f5() {
    var a animal = dog{}  //没问题
	a.run()
	
	var a1 animal  //由于接口类型可以持有 任意实现了该接口的类型,编译器在编译时无法确定具体的动态类型
	a1 = dog{}
	a.run()
}

type animal interface {
    run()
}
type dog struct{}
func (a dog) run() {}

运行时注意:moved to heap、escapes to heap

如何知道一个对象是分配在栈上还是堆上?

Go和C++不同,Go局部变量会进行 逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢?

go build -gcflags '-m -m -l' xxx.go.

关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。

golang分配的两个基本原则

指向栈上对象的指针不能被存储到堆中。

指向栈上对象的指针不能超过该栈对象的生命周期。

make 和 new 的区别?

make 和 new 都是用于内存分配的内建函数,但它们的使用场景和功能有所不同。

make:用于初始化并分配内存,只能用于创建 slice、map 和 channel 三种类型;返回的是初始化后的数据结构,而不是指针。

new:用于分配内存,但不初始化,返回的是指向该内存的指针(指向未初始化零值的内存地址);可以用于任何类型的内存分配。

使用for range 的时候,它的地址会发生变化吗?

for index, value := range collection {
    //...
}

在Go 1.22之前:对于 for range 循环中的迭代变量,其内存地址是不会发生变化的。

这里的 value 是一个 副本。在每次迭代中,collection 中的当前元素值会被 复制 到 value 这个变量中。Go 编译器通常会为 value 分配一块 固定的内存地址,然后在每次迭代时,将当前元素的值覆盖到这块内存中。所以,当打印 &value 时,会发现它的内存地址在整个循环过程中都是保持不变的。

Go 1.23以后:对于 for range 循环中的迭代变量,其地址是临时的,是变化的,不一样的,不再是共享内存了。

使用 for range 遍历一个集合时,迭代变量的地址会发生变化。这是因为 for range 每次迭代时都会 重新生成 迭代变量(如 value),这些变量在内存中是不同的地址。

Go 语言函数传参是值类型还是引用类型?

在 Go 语言中只存在 值传递,要么是值的副本,要么是指针的副本。无论是值类型的变量还是引用类型的变量亦或是指针类型的变量作为参数传递都会发生 值拷贝,开辟新的内存空间。

另外值传递、引用传递和值类型、引用类型是两个不同的概念,不要混淆了。引用类型作为变量传递可以影响到函数外部是因为发生 值拷贝后 新旧变量 指向了 相同的内存地址。

多返回值是如何实现的?

Go 语言的多返回值 是通过在 函数调用栈帧 上预留空间并进行 值复制 来实现的。在函数调用发生时,Go 编译器会计算出函数 所有返回值 的总大小。在为该函数创建 栈帧 时,就会在调用方(caller)的栈帧上,为这些返回值预留出 连续的内存空间。

当函数执行到 return 语句时,它会将其要返回的各个值 复制 到这些预留好的栈空间中。函数执行完毕后,控制权返回给调用方。此时,调用方可以直接从它自己的栈帧上(即之前为返回值预留的空间)获取这些返回的值。

普通指针和unsafe.Pointer有什么区别?

普通指针比如 *int、 *string,它们有明确的类型信息,编译器会进行 类型检查 和 垃圾回收跟踪。不同类型的指针之间 不能直接转换,这是Go类型安全的体现。

unsafe.Pointer 是Go的通用指针类型,可以理解为C语言中的 void*,它绕过了Go的类型系统。unsafe.Pointer 可以与任意类型的指针相互转换,也可以与 uintptr 进行转换来做指针运算。

另外,通指针受GC管理和类型约束,unsafe.Pointer 不受类型约束但仍受GC跟踪。

unsafe.Pointer与uintptr有什么区别和联系

unsafe.Pointer 和 uintptr 可以相互转换,这是Go提供的唯一合法的指针运算方式。典型用法是先将 unsafe.Pointer 转为 uintptr 做算术运算,然后再转回 unsafe.Pointer 使用。

最关键的区别在于 GC跟踪。unsafe.Pointer 会被 垃圾回收器 跟踪,它指向的内存不会被错误回收;而 uintptr 只是一个 普通整数,GC完全不知道它指向什么,如果没有其他引用,对应内存可能随时被回收。

所以记住:unsafe.Pointer 有GC保护,uintptr 没有,这是它们最本质的区别。

全部评论

相关推荐

第一章&nbsp;深夜BUG凌晨两点十七分,林栖迟第十七次运行了那个程序。屏幕上弹出一行红色的报错信息,和之前十六次一模一样。她盯着那行字看了五秒钟,然后以一种近乎虔诚的姿态,把脸埋进了键盘里。“你到底要我怎样?”键盘发出了抗议的嗒嗒声,像是在说“关我什么事”。林栖迟抬起头,把额前的碎发撩到耳后,露出一张因为熬夜而显得有些苍白的脸。她今年二十四岁,在一家不算大也不算小的互联网公司做后端开发,入职两年,加班时长累计可以绕地球一圈——当然这是夸张,但她的黑眼圈不是夸张。今天的BUG尤其离谱。一个本该在测试环境跑得好好的接口,上了预发布环境就罢工,没有任何日志,没有任何报错,就像被人掐住了喉咙,无声无息地死掉了。她从晚上九点排查到现在,改了十七个版本,没有一个能跑通。“林栖迟,你是废物吗?”她对自己说。“你不是废物。”一个声音从身后传来。林栖迟猛地回头,差点从椅子上摔下去。公司深夜的开放式办公区空旷而安静,只有她头顶的一盏灯还亮着,在周围投下一个惨白的光圈。而在这个光圈的边缘,站着一个男人。他穿着一件深色的连帽卫衣,帽子没有拉起来,露出一头略长的黑发,有些凌乱地垂在额前。他的五官很深,眉骨高而窄,鼻梁挺拔,嘴唇的线条干净利落,整个人像是一幅用炭笔勾勒的素描,轮廓分明但色调偏冷。他的眼睛是深棕色的,在昏暗的灯光下几乎看不出颜色,只有瞳孔深处有一点微弱的光,像是冬天夜晚最后熄灭的那颗星。林栖迟不认识他。“你是谁?”她问,手已经摸到了桌上的一本《代码大全》,准备随时当板砖用。“我在十七楼上班。”男人说,“产品部。”“产品部?”林栖迟的警惕心下降了一点,但困惑上升了十倍,“产品部的人来技术部干嘛?你们不是应该在楼上喝咖啡想需求吗?”男人的嘴角微微动了一下——不是笑,更像是一种“果然如此”的表情。“我来找你。”“找我?为什么?”“因为你的代码。”林栖迟下意识地看了一眼自己的屏幕,上面还挂着那行红色的报错信息。她赶紧把屏幕关了,转过身来。“你看到了?”“看到了。”男人走过来,在她旁边的工位坐下,“你卡在预发布环境的问题,我帮你看看。”林栖迟瞪大了眼睛:“你是产品经理,你懂代码?”“我写代码的时候你可能还在高考。”“……你多大?”“二十八。”“那你二十二就写代码了?”林栖迟算了算,“你是哪个学校毕业的?”男人没有回答这个问题。他伸手按亮了她的屏幕,扫了一眼报错信息,然后说了一句让她血压飙升的话。“你第十七版的代码逻辑是对的,但有个配置文件的路径写错了。”“不可能,我检查了所有路径——”男人修长的手指在键盘上敲了几下,打开了那个配置文件,光标停在第三行。林栖迟凑过去一看,瞳孔骤然收缩。她写的是:config.load(“./config/production.yaml”)而他改成了:config.load(“./config/preproduction.yaml”)“预发布环境和生产环境用的配置文件不一样,你的代码里写死了路径,环境变量没生效。”男人的声音很平淡,像是在解释为什么一加一等于二。林栖迟盯着那个改过的配置文件,沉默了整整十秒钟。“所以,我花了五个小时,十七个版本,就是因为这个?”“因为一个字母。”男人纠正。“preproduction比production多了三个字母!”“但你错的是路径,不是字母数。”林栖迟深吸一口气,缓缓吐出。她有一种强烈的冲动,想把面前的显示器吃掉,这样她就不用面对这个残酷的事实了。“谢谢。”她咬着牙说,“你叫什么名字?”“沈砚辞。”“沈砚辞。”林栖迟重复了一遍,总觉得这个名字在哪里听过,但脑子因为熬夜已经变成了一锅粥,怎么也想不起来,“你是哪个产品线的?”“不重要。”沈砚辞站起来,拉了拉卫衣的帽子,“代码能跑了就行。”他转身走了两步,忽然停下来,回头看了她一眼。“林栖迟。”“嗯?”“你写代码的习惯不太好。变量命名太随意,注释太少,异常处理太粗糙。”他的语气依然是那种不带感情的陈述,“但你解决问题的思路是对的,只是在细节上容易钻牛角尖。多休息,脑子清醒了再写。”林栖迟张了张嘴,想说“你谁啊你凭什么评价我的代码”,但话到嘴边变成了:“你怎么知道我容易钻牛角尖?”沈砚辞看了她一眼。“因为你改了十七个版本,每一版都换了一种思路,但没有一版回到最初的问题去检查最基本的配置。这是典型的钻牛角尖——你觉得问题一定很复杂,所以一直在往复杂的方向找,反而忽略了最简单的可能性。”林栖迟沉默了。这个人说的每一个字都对,这让她很不爽。沈砚辞没有等她反驳,拉上帽子,走进了电梯。电梯门关上的瞬间,林栖迟看到他的侧脸在电梯的灯光下显得格外冷峻,像一座被雪覆盖的山。她转过头,看着屏幕上那个终于跑通的程序,心里的感觉很奇怪——不是如释重负,不是感激,而是一种被看穿的、无处可逃的窘迫。她打开搜索引擎,输入“沈砚辞&nbsp;产品部”,结果让她倒吸了一口凉气。沈砚辞,江城大学计算机系博士毕业,曾在硅谷某顶级科技公司担任技术总监,三年前回国,加入现在的公司担任首席产品官。业内人称“疯子”——不是贬义,是因为他的产品嗅觉极其敏锐,总能提前半年到一年押中风口,做出的产品一个比一个离谱,但一个比一个成功。最重要的是,他是公司创始人之外最大的个人股东,身家至少十几个亿。林栖迟关掉了搜索页面,把脸埋进了手心里。“林栖迟,你刚才对着一个身家十几亿的产品大神说‘产品部的人来技术部干嘛’。”“林栖迟,你完了。”
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务