值类型和引用类型

值类型有哪些?

基本数据类型都是值类型,包括:int系列、float系列、bool、字符串、数组、结构体struct。

引用类型有哪些?

切片slice、map、channel、func、接口interface
口决:切麦通方口

值类型和引用类型的区别?

值类型在内存中存储的是值本身,而引用类型在内存中存储的是值的内存地址。
值类型内存通常在栈中分配,引用类型内存通常在堆中分配。

垃圾回收

引用类型的内存在堆中分配,当没有任何变量引用堆中的内存地址时,该内存地址对应的数据存储空间就变成了垃圾,就会被GO语言的GC回收。

堆和栈

在Go中,栈的内存是由编译器自动进行分配和释放,栈区往往存储着函数参数、局部变量和调用函数帧,它们随着函数的创建而分配,函数的退出而销毁。
一个goroutine对应一个栈,栈是调用栈(call stack)的简称。一个栈通常又包含了许多栈帧(stack frame),它描述的是函数之间的调用关系,每一帧对应一次尚未返回的函数调用,它本身也是以栈形式存放数据。

与栈不同的是,应用程序在运行时只会存在一个堆。狭隘地说,内存管理只是针对堆内存而言的。程序在运行期间可以主动从堆上申请内存,这些内存通过Go的内存分配器分配,并由垃圾收集器回收。

切片

比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。

切片唯一合法的比较操作是和nil比较。

比较的详解

要检查切片是否为空,应该使用

len(s) == 0

来判断,而不应该使用

s == nil

来判断。

原因:
一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil。

我们通过下面的示例就很好理解了:

var s1 []int            //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}           //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0)    //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用 len(s) == 0 来判断,不应该使用 s == nil 来判断。

其根本原因在于后面两种初始化方式已经给切片分配了空间,所以就算切片为空,也不等于 nil。但是 len(s) == 0 成立,则切片一定为空。

注意:
govar 是声明关键字,不会开辟内存空间;使用 := 或者 make 关键字进行初始化时才会开辟内存空间。

切片如何扩容

在Go语言中,切片的容量是一种动态增长的机制。当切片的长度达到或超过容量时,Go语言会自动扩展其底层数组的容量,一般由append触发。切片容量增长(growslice)的具体
规则在不同版本的规则不同。

对于go1.18之前来说:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于1024的话,growslice时就会将容量翻倍;
  3. 如果当前切片的长度大于1024的话,growslice时会每次增加25%的容量,直到新容量大于期望容量。

对于go1.18之后来说,减小了倍增阈值,但是在后续25%的幅度增加的时候把阈值作为基准的一部分,来避免扩容次数过多的问题:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于阈值256的话,growslice时就会将容量翻倍;
  3. 如果当前切片的长度大于等于256,就会每次增加(newcap+3*threshold)/4的容量,直到新容量大于期望容量。
    newcap:当前容量
    thresold:倍增阈值 定值256

长度 len < 256:每次容量直接翻倍;
长度 len ≥ 256:用公式迭代扩容:
newcap = newcap + (newcap + 3threshold)/4
代入定值 threshold=256:
newcap = newcap + (newcap + 3
256)/4

此外,在扩容之后还会进行一步roundupsize,这一步主要是靠内存对齐的优化,来计算出最终的容量。

深拷贝和浅拷贝

操作对象

深拷贝和浅拷贝操作的对象都是Go语言中的引用类型

区别如下

引用类型的特点是在内存中存储的是其他值的内存地址,
而值类型在内存中存储的是真实的值。

我们在go语言中通过 := 赋值引用类型就是 浅拷贝,即拷贝的是内存地址,两个变量对应的是同一个内存地址对应的同一个值。

a := []string{1,2,3} 
b := a

如果我们通过copy()函数进行赋值,就是深拷贝,赋值的是真实的值,而非内存地址,会在内存中开启新的内存空间。

举例如下:

a := []string{1,2,3} 
b := make([]string,len(a),cap(a)) 
copy(b,a)

new与make的区别

**共同点:**给变量分配内存

不同点:

1)作用变量类型不同:new给string,int和数组等值类型分配内存,make给切片,map,channel分配内存;

2)返回类型不一样:new返回指向变量的指针,make返回变量本身;

3)new 分配的空间被清零(比如0,false)。make 分配空间后,会进行初始化,分配置内容空间;new 只是开内存、填语言默认零值,底层数据结构没构建,slice/map/chan 拿到后是 nil,无法使用;
make 专门给这三种引用类型:
分配底层真正存储数据的内存;
初始化语言运行时需要的底层结构体(切片长度容量、哈希桶、通道缓冲区、并发锁等);
变量脱离 nil 状态,具备正常读写、收发的能力;
可使用的元素区域自动填充对应零值。
极简口诀
new:给个空指针,只有外壳,内里空 nil,用不了;
make:外壳 + 底层存储全部装好,结构完整,开箱即用。

  1. 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上?这块我比较模糊,大家可以自己探究下,我搜索出来的答案是golang会弱化分配的位置的概念,因为编译的时候会自动内存逃逸处理,懂的大佬帮忙补充下:make、new内存分配是在堆上还是在栈上?

new和make都在堆上分配内存 new 函数分配内存,make 函数初始化 https://www.cnblogs.com/chenpingzhao/p/9918062.html

go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?

https://www.topgoer.cn/docs/golangxiuyang/golangxiuyang-1cmee0q64ij5p

作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;

避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。

多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中

defer,return,return value(函数返回值) 执行顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 参考:

【Golang】Go语言defer用法大总结(含return返回机制)__奶酪的博客-CSDN博客blog.csdn.net/Cassie_zkq/article/details/108567205

有名返回值

func b() (i int) { 	
    defer func() { 		
        i++ 		
        fmt.Println("defer2:", i) 	
    }() 	
    defer func() { 		
        i++ 		
        fmt.Println("defer1:", i) 	
    }() 	
    return i 
    //或者直接写成
    return 
} 
func main() { 	
    fmt.Println("return:", b()) 
} 

打印结果:

defer1: 1
defer2: 2
return: 2

函数返回指针

func c() *int { 	
    var i int 	
    defer func() { 		
        i++ 		
        fmt.Println("defer2:", i) 	
    }() 	
    defer func() { 		
        i++ 		
        fmt.Println("defer1:", i) 	
    }() 	
    return &i 
} 
func main() { 	
    fmt.Println("return:", *(c())) 
}

打印结果:

defer1: 1
defer2: 2
return: 2

Go的map如何实现排序

我们知道Go语言的map类型底层是由hash实现的,是无序的,不支持排序。

如果我们的数据使用map类型存储,如何实现排序呢?

解决思路

排序map的key,再根据排序后的key遍历输出map即可。

代码实现

package main

import (
   "fmt"
   "math/rand"
   "sort"
   "time"
)

func main() {
   rand.Seed(time.Now().UnixNano()) //初始化随机数种子

   var scoreMap = make(map[string]int, 30)

   for i := 0; i < 30; i++ {
      key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
      value := rand.Intn(30)           //生成0~50的随机整数
      scoreMap[key] = value
   }
   //取出map中的所有key存入切片keys
   var keys = make([]string, 0, 30)
   for key := range scoreMap {
      keys = append(keys, key)
   }
   //对切片进行排序
   sort.Strings(keys)
   //按照排序后的key遍历map
   for _, key := range keys {
      fmt.Println(key, scoreMap[key])
   }
}

输出结果

stu00 25
stu01 13
stu02 11
stu03 20
stu04 19
stu05 4
stu06 15
stu07 8
stu08 14
stu09 12
stu10 7
stu11 27
stu12 1
stu13 14
stu14 10
stu15 5
stu16 23
stu17 29
stu18 8
stu19 5
stu20 5
stu21 3
stu22 7
stu23 16
stu24 4
stu25 8
stu26 15
stu27 13
stu28 22
stu29 17

golang map底层实现

map的底层是一个散列表,它是由hmap和bmap组成,
hmap管理着桶,包括桶的扩容以及溢出桶。
bmap管理着键值对,数据类型上相当于链表,每个节点由 tophash8个键值对溢出桶地址组成
tophash 相当于 key 的索引,
8个键值对 的存储方式是键与键存储在一起,值与值存储在一起,
溢出桶 是用来解决hash冲突的
map的存储过程是:key用hash函数计算出的值分出了高位的tophash和低位,高位用来定位key的位置,低位用来定位桶。

map扩容机制是什么

map 分为增量扩容等量扩容
概念解释:
公式:桶总数 = (2^B)
B 不是普通数学对数,是以 2 为底桶总数的对数,数学写法:
(B = log_2(桶总数量))
简单说:B 是指数,桶的真实总数永远是 2 的 B 次方,桶个数只允许是 2、4、8、16、32…… 这种 2 的幂次。举例子:

  1. B=3 → 桶数 =(2^3=8)
  2. B=5 → 桶数 =(2^5=32)
  3. B=15 → 桶数 =(2^{15}=32768)

Go map 不会出现 9、10、17 这种非 2 倍数的桶数,全部靠 B 控制。

增加扩容:
键值对数量>8,并且键值对总数 > 桶总数 ×6.5
桶总数 =(2^B),扩容后 B=B+1,桶数量翻倍((2^{B+1})),重新搬迁所有 key。

等量扩容:
等量扩容是分段扩容的方式
等量扩容(元素不多,但 hash 冲突严重、溢出桶太多)总键值对不多,没必要加桶。桶总数(2^B)保持不变,只是把所有 key 打散重新分配到现有桶和溢出桶,优化查询效率。触发门槛由下面 B 的两套阈值决定。

情况 1:B < 16(桶总数 =(2^B),桶比较少)触发等量扩容条件:溢出桶数量 noverflow ≥ (2^B)
举例:B=4,桶总数 =(2^4=16)。只要溢出桶≥16,触发等量扩容。

情况 2:B ≥ 16(桶总数≥(2^{16}=65536),桶很多)不再用(2^B)做阈值,统一封顶阈值固定为(2^{15}=32768)。
不管 B 是 16、20、30,只要溢出桶≥32768,就触发等量扩容。

目的:B 很大时,(2^B)会是几十万、上亿,阈值太大永远触发不了扩容,所以强制上限。

通俗大白话总结B 是简写,用来少写一大串数字:
不用每次写 “桶总数是 2 的几次方”,直接记数字 B。
比如 B=10,不用记桶有 1024 个,只记 B=10 就行。
Go 底层全部用 B 来计算桶数量、溢出桶阈值。

数组和切片的区别

数组:
数据是值类型
数组固定长度,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型
赋值形式是值传递。
数组需要指定大小,不指定也会根据处初始化对的自动推算出大小,不可改变

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

Redis变慢解决办法梳理

详解文档:
https://blog.csdn.net/skye_fly/article/details/119979126
https://blog.csdn.net/weixin_68009402/article/details/130806249

三次握手

第一次握手:客户端将标志位SYN置为1,随机产生一个值序列号seq=x,并将该数据包发送给服务端,客户端进入syn_sent状态,等待服务端确认。
第二次握手:服务端收到数据包后由标志位SYN=1,知道客户端在请求建立连接,服务端将标志位SYN和 ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给客户端以确认连接请求,服务端进入syn_rcvd状态。
第三次握手:客户端收到确认后检查,如果正确则将标志位ACK为1,ack=y+1,并将该数据包发送给服务端,服务端进行检查如果正确则连接建立成功,客户端和服务端进入established状态,完成三次握手,随后客户端和服务端之间可以开始传输 数据了

四次挥手

第一次挥手:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入fin_wait_1状态。
第二次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1,服务端进入Close_wait状态。此时TCP连接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接收。
第三次挥手:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入Last_ack状态。
第四次挥手:客户端收到FIN后,客户端进入Time_wait状态,接着发送一个ACK给服务端,确认后,服务端进入Closed状态,完成四次挥手。

详解文档:
https://www.topgoer.cn/docs/interview/interview-1do5q31n4oj4i
https://blog.csdn.net/sq4521/article/details/90751241

参数文档:
作者:王中阳Go
链接:https://juejin.cn/post/7131717990558466062
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:海马  创建时间:2023-11-21 12:08
最后编辑:海马  更新时间:2026-08-02 08:06
上一篇:
下一篇: