1、golang 中 make 和 new 的区别?(基本必问)
1.make和new都是golang用来分配内存的內建函
数,且在堆上分配内存,
2.make 即分配内存,也初始化内存。
3.new只是将内存清零,并没有初始化内存。
4.make返回的还是引用类型本身,make只能用来
分配及初始化类型为slice,map,channel的数据
5.new可以分配任意类型的数据
6.而new返回的是指向类型的指针。
2、数组和切片的区别 (基本必问)
1.内部结构
数组在内存中是一段连续的内存空间,元素的类型
和长度都是固定的。切片在内存中由一个指向底层
数组的指针、长度和容量组成,长度表示切片当前
包含的元素个数,容量表示切片可以扩展的最大元
素个数
2.长度
数组的长度在创建时指定,不能更改。切片的长度
可以动态扩展或收缩,可以根据需要自由调整。
3.使用方式
数组在使用时需要明确指定下标访问元素,不能动
态生成。切片可以使用 append 函数向其末尾添
加元素,可以使用 copy 函数复制切片,也可以
使用 make 函数创建指定长度和容量的切片
3、for range 的时候它的地址会发生变化么?for 循环遍历 slice 有什么问题?
1.地址没有发生变化
2.在使用 for range 语句遍历切片或数组时,
每次迭代都会返回该元素的副本,而不是该元素
的地址。这意味着,每次循环完成后,该元素所
对应的地址都是相同的(最后一个),并不会改变
3.for range循环的工作方式是为了优化性能和
降低内存分配。在for range中,迭代变量会被
重复使用,而不是在每次迭代中创建一个新的变量
这个设计选择有助于减少内存分配和提高性能。在
这种情况下,如果你尝试在循环中保存迭代变量的
地址,实际上保存的是同一个地址。因此,在循环
结束后,你可能会遇到保存的值都是最后一次迭代
的值的情况。这是Go语言中的一个设计决策,旨在
提高程序的性能。如果你需要在循环中保存每次迭
代的值,可以考虑在每次迭代中创建一个新的变量
来保存值,而不是直接使用迭代变量。这样可以确
保你在集合或数组中保存的是不同的地址而不是同
一个地址
package main
import "fmt"
type girl struct {
Name string
Age int
}
func main() {
gl := make(map[string]*girl)
studs := []girl{
{Name: "Lili", Age: 23},
{Name: "Lucy", Age: 24},
{Name: "Han Mei", Age: 21},
}
//错误的写法
for _, v := range studs {
gl[v.Name] = &v
}
//正确的写法
for _, v := range studs {
temp := v
gl[v.Name] = &temp
}
for mk, mv := range gl {
fmt.Println(mk, "=>", mv.Age)
}
}
4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?(for defer)defer recover 的问题?(主要是能不能捕获)
1.在Go语言中,defer关键字用于延迟执行一个
函数调用,通常用于确保在函数执行结束后释放
资源或执行一些清理操作。关于defer的几个问题:
多个 defer 的顺序: 多个defer语句按照后进
先出(Last In, First Out,LIFO)的顺序
执行,即最后一个defer语句会最先执行,倒数
第二个会在倒数第一个之后执行,以此类推。
2.defer 在何时修改返回值: defer语句中
的函数调用是在包含它的函数执行完毕之后才执
行的。如果包含defer的函数有命名的返回值,
并且在defer语句执行时修改了这个返回值,那
么最终的返回值将是defer语句中修改后的值。
func example() (result int) {
defer func() {
result += 10
}()
return 5
}
在这个例子中,尽管return 5语句执行时返回
的是5,但由于defer中修改了result,实际上
最终的返回值是15。
3.defer与recover通常一起使用,用于处理
函数中的错误。recover只能捕获在同一个
goroutine中发生的panic,而且必须在defer
中调用。如果recover在没有发生panic的情况
下调用,它会返回nil
4.defer和recover是Go语言中用于处理资源
释放和错误恢复的重要机制。
5、 uint 类型溢出
Golang的uint类型溢出问题通常会在大量运算
中发生,特别是在涉及到大量循环或者大数运算时
当uint类型的值超过其最大值时,它会发生溢出,
然后从该类型的最小值开始循环,解决方案:
1.使用更大的数据类型:例如,如果你正在使用
uint32,你可以尝试升级到uint64。这将提供
更大的值范围,从而减少溢出的可能性。
2.添加溢出检查:在每次运算之后,你可以检查
结果是否小于任一操作数(假设我们只在正数上
进行操作)。如果是这样,那么就发生了溢出。
3.使用 math/big 包:对于非常大的数值,你
也可以考虑使用 math/big 包中的 Int 类型
这个类型可以处理任意大小的数值,但是运算速
度会慢一些。
6、介绍 rune 类型
1.rune是int32 的别名,几乎在所有方面等同
于int32,它用来区分字符值和整数值。
2.golang中的字符串底层实现是通过byte数组
的中文字符在unicode下占2个字节,在utf-8编
码下占3个字节,而golang默认编码正好是utf-8
3.byte 等同于int8,常用来处理ascii字符
4.rune 等同于int32,常用来处理unicode或
utf-8字符
func main() {
str := "米虫 is cool"
fmt.Println("STR LEN - ", len([]rune(str)))
}
7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)
Go 中解析的 tag 是通过反射实现的,反射是指
计算机程序在运行时(Run time)可以访问、检
测和修改它本身状态或行为的一种能力或动态知道
给定数据对象的类型和结构,并有机会修改它。反
射将接口变量转换成反射对象 Type 和 Value;
反射可以通过反射对象 Value 还原成原先的接口
变量;反射可以用来修改一个变量的值,前提是这
个值可以被修改;tag是啥:结构体支持标记,
name string json:name-field 就是
json:name-field 这部分
8、调用函数传入结构体时,应该传值还是指针? (Golang 都是值传递)
1.结构体的大小:如果结构体非常大,使用指针传
递会更有效率,因为这样只会复制指针值(一般是
8字节),而不是复制整个结构体。如果结构体小
值传递和指针传递的性能差异可能可以忽略不计
2.是否需要修改原始结构体:如果你需要在函数中
修改原始结构体,你应该使用指针传递。如果你使
用值传递,函数会接收结构体的一个副本,你在函
数中对结构体的修改不会影响到原始的结构体。
9、Slice 介绍
type slice struct {
array unsafe.Pointer
len int
cap int
}
array 指针指向底层数组
len 表示切片长度
cap 表示切片容量
2.make 创建 slice 可以同时指定其长度和容
量,底层会分配一个数组,数组的长度即容量。
slice = make([]int,5,10): 表示改 slice
长度为5,容量为10。使用数组来创建 slice 时
slice 与原数组共用一部分内存。
3.扩容 在使用 append 向 slice 追加元素时
若 slice 空间不足则会发生扩容,扩容会重新分
配一块更大的内存,将原slice拷贝到新 slice
然后返回新 slice。扩容后再将数据追加进去。
扩容操作只只对容量,扩容后的slice长度不变
容量变化规则如下:若slice容量够用,则将新
元素追加进去,slice.len++,返回原 slice
若slice容量不够用,将slice先扩容,扩容得到
新 slice,将新元素追加进新 slice,
slice.len++,返回新 slice。
4.使用 copy()内置函数拷贝两个切片时,会将
源切片的数据逐个拷贝到目的的切片指向的数组中
拷贝数量取两个切片长度的最小值。例如将长度为
10的切片拷贝到长度为5的切片时,将会拷贝5个
元素。也就是说 copy 不会发生扩容。根据数组
或切片来生成新的切片一般使用
slice := array[start:end] 方式,这种新
生成的切片没有指定容量,新切片的容量是从
start开始到array的结束(注意并不是到 end)
另一种写法,生成新切片同时指定其容量:
slice[start:end:cap] ,其中的 cap 为新
切片的容量,容量不能超过原切片实际值。
5.创建切片时可根据实际需要预分配容量,尽量
避免追加过程中扩容操作,有利于提升性能切片
拷贝时需要判断实际拷贝的元素个数谨慎使用多
个切片操作同一个数组,以防读写冲突每个切片
都指向一个底层数组每个切片都保存了当前切片
的长度、底层数组可用容量使用len()、cap()
计算切片长度、容量时,时间复杂度均为O(1)
不需要遍历切片通过函数传递切片时,不会拷贝
整个切片,因为切片本身只是个结构体而矣使用
append() 向切片追加元素时有可能触发扩容
扩容后将会生成新的切片
10、go struct 能不能比较?
在Go语言中,结构体(Struct)是否可以比较取决
于其字段。如果结构体的所有字段都是可比较的
那么这个结构体也是可比较的。这意味着你可以
使用 == 或 != 运算符来比较两个结构体变量。
11、context 结构是什么样的?
type Context interface {
Done() <-chan struct{}
Err() error
Deadline() (deadline time.Time, ok bool)
Value(key interface{}) interface{}
}
Context是一个接口,定义了4个方法,它们都是
幂等的。也就是说连续多次调用同一个方法,
得到的结果都是相同的。
1.Done()返回一个channel,可以表示context
被取消的信号:当这个channel被关闭时,说明
context被取消了,注意,这是一个只读的channel
读一个关闭的 channel 会读出相应类型的零值。
并且源码里没有地方会向这个channel里面塞入值
换句话说,这是一个receive-only的channel因
此在子协程里读这个 channel,除非被关闭否则
读不出来任何东西。也正是利用了这一点子协程从
channel里读出了值(零值)后,就可以做一些收尾
工作,尽快退出。
2.Err()返回一个错误,表示channel被关闭
的原因例如是被取消,还是超时。
3.Deadline()返回context的截止时间,通过
此时间函数就可以决定是否进行接下来的操作,
如果时间太短就可以不往下做了,否则浪费系统
资源。当然,也可以用这个 deadline 来设置
一个 I/O 操作的超时时间。
4.Value()获取之前设置的key对应的value。
12、context 使用场景和用途?(基本必问)
context提供了一种在不同goroutine之间传递
取消信号、截止时间、截止日期等元数据的方法。
context主要用于在大规模的并发或分布式系统
中进行取消操作、截止时间管理、跟踪请求以及
传递其他请求范围的数据。以下是一些context
的常见使用场景和用途:
1.取消操作:
通过context可以在一个goroutine中发出取消
信号,而在其他goroutine中接收到该信号,以
便在需要时终止操作。这在处理超时、错误或用
户请求取消时非常有用。
2.截止时间管理:
context可以与截止时间(deadline)结合使用
以便在规定的时间内执行操作。当超过截止时间
时,相关的操作可以被取消或终止。
3.请求范围数据传递:
context可以用于在请求处理过程中传递请求相关
的数据,如用户身份验证信息、请求跟踪标识等。
这样,无需在每个函数参数中显式传递这些值,而
可以通过context传递
4.并发控制:
context可以用于在多个goroutine之间同步
操作,通过取消信号可以协调它们的行为。例如
可以使用context来管理goroutine池,当主
goroutine需要停止所有子goroutine时,
可以通过context向它们发送取消信号
5.超时控制:
context允许在指定的时间内完成某项操作,如果
超过了这个时间,可以通过context来取消相关
的操作。这对于避免在网络请求、IO操作等场景
下发生超时问题非常有用。
6.连接传递:
在RPC(远程过程调用)等场景中,context可以
用于在请求和响应之间传递连接信息,以确保
相关的信息在整个调用链中可用。
7.context是Go语言中实现请求范围的上下文
传递的标准方式,可以在大规模并发和分布式系
统中帮助管理和控制请求的行为
13、channel 是否线程安全?锁用在什么地方?
Channel本身是线程安全的。在Go语言的并发
模型中,Channel是用来在不同的Goroutine
(Go语言中的线程)之间进行数据通信的,当一个
Goroutine向Channel发送数据时,直到另一个
Goroutine接收到这个数据之前,该Goroutine
将会被阻塞。这种机制保证了Channel的数据在
Goroutine之间传递时的安全性。在对buf中的
数据进行入队和出队操作时,为当前chnnel使用
了互斥锁,防止多个线程并发修改数据
14、go channel 的底层实现原理 (数据结构)
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
closed uint32
elemtype *_type
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
1.qcount:代表循环队列chan中已经接收但还
没被取走的元素的个数。
2.datagsiz:循环队列chan的大小。选用了一个
循环队列来存放元素,类似于队列的生产者 - 消
费者场景
3.buf:存放元素的循环队列的 buffer。
4.elemtype 和 elemsize:循环队列 chan
中元素的类型和 size。chan 一旦声明,它的
元素类型是固定的,即普通类型或者指针类型,
元素大小自然也就固定了。
5.sendx:处理发送数据的指针在buf中的位置
一旦接收了新的数据,指针就会加上elemsize
移向下一个位置。buf 的总大小是elemsize
的整数倍,而且 buf 是一个循环列表。
6.recvx:处理接收请求时的指针在buf中的位置
一旦取出数据,指针会移动到下一个位置。
7.recvq:chan是多生产者多消费者的模式,如果
消费者因为没有数据可读而被阻塞了,就会被加入
到 recvq 队列中。
8.sendq:如果生产者因为 buf 满了而阻塞
会被加入到 sendq 队列中。
15、 nil、关闭的 channel 再进行读、写、关闭会怎么样
1.nil channel:未初始化channel,未经make
2.closed channel:执行了closed的channel
3.对nil channel的读写会永久block
4.向closed channel写入会发生panic
5.从closed channel读取仍然可以读取剩余
的数据,直到数据全部读取完成立即读出零值
6.向nil channel写数据,程序将会永久阻塞
16、 向 channel 发送数据和从 channel 读数据的流程是什么样的?
1.向通道(channel)发送数据和从通道读取
数据是 Go 语言中实现并发通信的重要机制。
2.向通道发送数据,创建通道: 使用 make
函数创建一个通道,指定通道的类型和容量
发送数据:使用<-操作符将数据发送到通道中。
3.创建通道:使用make函数创建一个通道,指定
通道的类型和容量读取数据:使用 <- 操作符
从通道中读取数据。
4.通道是阻塞的,如果没有接收者,发送操作
将被阻塞;如果没有发送者,接收操作也将
被阻塞。这种机制确保了 goroutine 之
间的同步和通信