1、= 和 := 的区别?
:= 声明+赋值 = 仅赋值
2、指针的作用?
指针用来保存变量的地址。
* 运算符,也称为解引用运算符,用于访问地址中的值。
&运算符,也称为地址运算符,用于返回变量的地址。
3、Go 允许多个返回值吗?
允许
4、 Go 有异常类型吗?
Go 没有异常类型,只有错误类型(Error),通常使用返回值来表示异常状态。
5、什么是协程(Goroutine)
Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。
6、如何高效地拼接字符串
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder,最小化内存拷贝次数。
7、什么是 rune 类型
ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。
Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。
8、如何判断 map 中是否包含某个 key ?
dict["foo"] 有 2 个返回值,val 和 ok,如果 ok 等于 true,则说明 dict 包含 key "foo",val 将被赋予 "foo" 对应的值。
9、Go 支持默认参数或可选参数吗?
Go 语言不支持可选参数(python 支持),也不支持方法重载(java支持)。
10、defer 的执行顺序
多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。
defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值。
11、如何交换 2 个变量的值?
a, b = b, a
12、Go 语言 tag 的用处?
tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。
13、如何判断 2 个字符串切片(slice) 是相等的?
go 语言中可以使用反射 reflect.DeepEqual(a, b) 判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
通常采用的方式如下,遍历比较切片中的每一个元素(注意处理越界的情况)。
14、字符串打印时,%v 和 %+v 的区别
%v 和 %+v 都可以用来打印 struct 的值,区别在于 %v 仅打印各个字段的值,%+v 还会打印各个字段的名称。
但如果结构体定义了 String() 方法,%v 和 %+v 都会调用 String() 覆盖默认值。
15、Go 语言中如何表示枚举值(enums)
通常使用常量(const) 来表示枚举值。(iota)
16、空 struct{} 的用途;
使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。
再比如,声明只包含方法的结构体。
17、init() 函数是什么时候执行的?
init() 函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。同一个包,甚至是同一个源文件可以有多个 init() 函数。init() 函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init() 函数的执行顺序不作保证。
一句话总结: import –> const –> var –> init() –> main()
18、Go 语言的局部变量分配在栈上还是堆上?
由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。
19、2 个 interface 可以比较吗?
Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 == 或 != 比较。2 个 interface 相等有以下 2 种情况
两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
类型 T 相同,且对应的值 V 相等。
20、两个 nil 可能不相等吗?
可能。
接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。
两个接口值比较时,会先比较 T,再比较 V。
接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
21、简述 Go 语言GC(垃圾回收)的工作原理
最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。
标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:
标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。
标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。
三色标记算法将程序中的对象分成白色、黑色和灰色三类。
白色:不确定对象。
灰色:存活对象,子对象待处理。
黑色:存活对象。
标记开始时,所有对象加入白色集合(这一步需 STW )。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止,标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。
三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。
三色标记法并发执行仍存在一个问题,即在 GC 过程中,对象指针发生了改变。比如下面的例子:
A (黑) -> B (灰) -> C (白) -> D (白)
正常情况下,D 对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了 C 对 D 的引用,而 A 获得了 D 的引用。标记继续进行,D 就没有机会被标记为黑色了(A 已经处理过,这一轮不会再被处理)。
A (黑) -> B (灰) -> C (白)
↓
D (白)
为了解决这个问题,Go 使用了内存屏障技术,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。
一次完整的 GC 分为四个阶段:
1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
2)使用三色标记法标记(Marking, 并发)
3)标记结束(Mark Termination,需 STW),关闭写屏障。
4)清理(Sweeping, 并发)
22、函数返回局部变量的指针是否安全?
这在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。
23、 非接口的任意类型 T() 都能够调用 *T 的方法吗?反过来呢?
一个T类型的值可以调用为*T类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下。编译器在调用指针属主方法前,会自动取此T值的地址。因为不是任何T值都是可寻址的,所以并非任何T值都能够调用为类型*T声明的方法。
反过来,一个*T类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型 T 声明的方法,编译器都会为类型*T自动隐式声明一个同名和同签名的方法。
哪些值是不可寻址的呢?
字符串中的字节;
map 对象中的元素(slice 对象中的元素是可寻址的,slice的底层是数组);
常量;
包级别的函数等。
24、无缓冲的 channel 和 有缓冲的 channel 的区别?
对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。
对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。
无缓冲的channel由于没有缓冲发送和接收需要同步.
有缓冲channel不要求发送和接收操作同步.
25、什么是协程泄露(Goroutine Leak)?
协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。
常见的导致协程泄露的场景有以下几种:
缺少接收器,导致发送阻塞
缺少发送器,导致接收阻塞
死锁(dead lock)
两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。
无限循环(infinite loops)
这个例子中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。
26、Go 可以限制运行时操作系统线程的数量吗?
可以使用环境变量 GOMAXPROCS 或 runtime.GOMAXPROCS(num int) 设置
27、defer 作用域
defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。
defer 语句执行时,会将需要延迟调用的函数和参数保存起来。
28、知道golang的内存逃逸吗?什么情况下会发生内存逃逸?
golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。
能引起变量逃逸到堆上的典型情况:
在方法内把局部变量指针返回:局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
发送指针或带有指针的值到 channel 中: 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
在一个切片上存储指针或带指针的值: 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap ): slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
在 interface 类型上调用方法: 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
29、字符串转成byte数组,会发生内存拷贝吗?
字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。
30、拷贝大切片一定比小切片代价大吗?
并不是,所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。
31、能说说uintptr和unsafe.Pointer的区别吗?
unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
unsafe.Pointer 可以和 普通指针 进行相互转换;
unsafe.Pointer 可以和 uintptr 进行相互转换。
32、for循环select时,如果通道已经关闭会怎么样?如果select中的case只有一个,又会怎么样?
for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。
如果select里边只有一个case,而这个case被关闭了,则会出现死循环。
33、对已经关闭的的chan进行读写,会怎么样?为什么?
读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
写已经关闭的 chan 会 panic
34、对未初始化的的chan进行读写,会怎么样?为什么?
读写未初始化的 chan 都会阻塞。
35、json包里使用的时候,结构体里的变量不加tag能不能正常转成json里的字段?
如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息。
如果变量首字母大写,则为public。不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致。
加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用。
36、json包里使用的时候,会结构体里的字段边上加tag,有没有什么办法可以获取到这个tag的内容呢?
tag信息可以通过反射(reflect包)内的方法获取。
37、Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量?
Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行.
38、Go的GPM如何调度?
新创建的Goroutine会先存放在Global全局队列中,等待Go调度器进行调度,随后Goroutine被分配给其中的一个逻辑处理器P,并放到这个逻辑处理器对应的Local本地运行队列中,最终等待被逻辑处理器P执行即可。
在M与P绑定后,M会不断从P的Local队列中无锁地取出G,并切换到G的堆栈执行,当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡。
39、Go的Struct能不能比较
相同struct类型的可以比较
不同struct类型的不可以比较,编译都不过,类型不匹配
40、Go的Slice如何扩容
slice是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。但是slice本身并不是动态数据或者数组指针。
slice常见的操作有 reslice、append、copy。
41、Go中的map如何实现顺序读取。
Go中map如果要实现顺序读取的话,可以先把map中的key,通过sort包排序.
通过sort中的排序包进行对map中的key进行排序.
42、New和Make的区别
在go语言中,make和new都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。make返回的是引用类型本身;而new返回的是指向类型的指针。
43、Go值接收者和指针接收者的区别
值类型调用者
值接收者:方法会使用调用者的一个副本,类似于“传值”。
指针接收者:使用值的引用来调用方法。
指针类型调用者
值接收者:指针被引用为值
指针接收者:实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针