与其他语言相比,使用 Go 有什么好处?
- 与其他作为学术实验开始的语言不同,Go 代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。
- Golang 针对并发进行了优化,并且在规模上运行良好。
- 由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。
- 自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。
Golang 使用什么数据类型?
Golang 使用以下类型:
- Method
- Bool
- String
- Array
- Slice
- Struct
- Pointer
- Function
- Interface
- Map
- Channel
Go 程序中的包是什么?
包 (pkg) 是 Go 工作区中包含 Go 源文件或其他包的目录。源文件中的每个函数、变量和类型都存储在链接包中。每个 Go 源文件都属于一个包,该包在文件顶部使用以下命令声明:
package <packagename>
您可以使用以下方法导入和导出包以重用导出的函数或类型:
import <packagename>
Golang 的标准包是 fmt,其中包含格式化和打印功能,如 Println().
Go 支持什么形式的类型转换?将整数转换为浮点数。
Go 支持显式类型转换以满足其严格的类型要求。
i := 55 //int
j := 67.8 //float64
sum := i + int(j) //j is converted to int
什么是 Goroutine?你如何停止它?
一个 Goroutine 是一个函数或方法执行同时旁边其他任何够程采用了特殊的Goroutine 线程。Goroutine 线程比标准线程更轻量级,大多数 Golang 程序同时使用数千个 Goroutine。
要创建 Goroutine,请 go 在函数声明之前添加关键字
go f(x, y, z)
您可以通过向 Goroutine 发送一个信号通道来停止它。Goroutines 只能在被告知检查时响应信号,因此您需要在逻辑位置(例如 for 循环顶部)包含检查。
package main
func main() {
quit := make(chan bool)
go func() {
for {
select {
case <-quit:
return
default:
// …
}
}
}()
// …
quit <- true
}
如何在运行时检查变量类型?
类型开关(Type Switch)是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case 用作条件语句,如果没有一个 case 为真,则执行 default。
Go 两个接口之间可以存在什么关系?
如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。如果接口 A 的方法列表是接口 B 的方法列表的自己,那么接口 B 可以赋值给接口A。接口查询是否成功,要在运行期才能够确定。
Go 当中同步锁有什么特点?作用是什么
当一个 Goroutine(协程)获得了 Mutex 后,其他 Goroutine(协程)就只能乖乖的等待,除非该 Goroutine 释放了该 Mutex。RWMutex 在读锁占用的情况下,会阻止写,但不阻止读 RWMutex。 在写锁占用情况下,会阻止任何其他Goroutine(无论读和写)进来,整个锁相当于由该 Goroutine 独占同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性。
Go 语言当中 Channel(通道)有什么特点,需要注意什么?
如果给一个 nil 的 channel 发送数据,会造成永远阻塞。
如果从一个 nil 的 channel 中接收数据,也会造成永久阻塞。
给一个已经关闭的 channel 发送数据, 会引起 panic从一个已经关闭的 channel 接收数据, 如果缓冲区中为空,则返回一个零值
Go 语言当中 Channel 缓冲有什么特点?
无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的。
Go 语言中 cap 函数可以作用于哪些内容?
可以作用于的类型有:
- array(数组)
- slice(切片)
- channel(通道)
Go Convey 是什么?一般用来做什么?
- go convey 是一个支持 Golang 的单元测试框架
- go convey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到 Web 界面
- go convey 提供了丰富的断言简化测试用例的编写
Go 语言当中 new 的作用是什么?
new 的作用是初始化一个内置类型的指针 new 函数是内建函数,函数定义:
func new(Type) *Type
- 使用 new 函数来分配空间
- 传递给 new 函数的是一个类型,而不是一个值
- 返回值是指向这个新分配的地址的指针
Go 语言中 make 的作用是什么?
make 的作用是为 slice, map or chan 的初始化 然后返回引用 make 函数是内建函数,函数定义:
func make(Type, size IntegerType) Type
make(T, args)函数的目的和 new(T)不同 仅仅用于创建 slice, map,channel 而且返回类型是实例
Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?
- 虽然这三个函数,都是格式化输出,但是输出的目标不一样
- Printf 是标准输出,一般是屏幕,也可以重定向。
- Sprintf()是把格式化字符串输出到指定的字符串中。
- Fprintf()是把格式化字符串输出到文件中。
Go 语言当中数组和切片的区别是什么?
数组:
数组固定长度。数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型数组需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变。数组是通过值传递的
切片:
切片可以改变长度。切片是轻量级的数据结构,三个属性,指针,长度,容量不需要指定大小切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容。
Go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明
- 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。
- 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。
Go 语言当中数组和切片在传递的时候的区别是什么?
- 数组是值传递
- 切片是引用传递
Go 语言是如何实现切片扩容的?
package main
import "fmt"
func main() {
arr := make([]int, 0)
for i := 0; i < 2000; i++ {
fmt.Println("len 为", len(arr), "cap 为", cap(arr))
arr = append(arr, i)
}
}
我们可以看下结果
依次是
0,1,2,4,8,16,32,64,128,256,512,1024
但到了 1024 之后,就变成了
1024,1280,1696,2304
每次都是扩容了四分之一左右
看下面代码的 defer 的执行顺序是什么? defer 的作用和特点是什么?
defer 的作用是:
你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。
defer 的常用场景:
- defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
- 通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
- 释放资源的 defer 应该直接跟在请求资源的语句后。
Golang Slice 的底层实现
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据。
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片对象非常小,是因为它是只有 3 个字段的数据结构:
- 指向底层数组的指针
- 切片的长度
- 切片的容量
Golang Slice 的扩容机制,有什么注意点?
Go 中切片扩容的策略是这样的:
- 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
- 否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍
- 否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等新申请的容量
- 如果最终容量计算值溢出,则最终容量就是新申请容量
扩容前后的 Slice 是否相同?
情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的 Slice
情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行append() 操作。这种情况丝毫不影响原数组
要复制一个 Slice,最好使用 Copy 函数。
Golang 的参数传递、引用类型
Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan 等这些),这样就可以修改原内容数据
Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针
Golang Map 底层实现
Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(aheader for a go map),一个叫 bmap(a bucket for a Go map,通常叫其bucket)。
Golang Map 如何扩容
双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。
等量扩容:重新排列,极端情况下,重新排列也解决不了,map 存储就会蜕变成链表,性能大大降低,此时哈希因子hash0 的设置,可以降低此类极端场景的发生。
Golang Map 查找
Go 语言中 map 采用的是哈希查找表,由一个 key 通过哈希函数得到哈希值,64位系统中就生成一个 64bit 的哈希值,由这个哈希值将 key 对应存到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突。
细节:key 经过 hash 后共 64 位,根据 hmap 中 B 的值,计算它到底要落在哪个桶时,桶的数量为 2^B,如 B=5,那么用 64 位最后 5 位表示第几号桶,在用hash 值的高 8 位确定在 bucket 中的存储位置,当前 bmap 中的 bucket 未找到,则查询对应的 overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。如果当前 map 处于数据搬移状态,则优先从oldbuckets 查找。
介绍一下 Channel
Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。
channel 收发遵循先进先出 FIFO 的原则。分为有缓冲区和无缓冲区,channel中包括 buffer、sendx 和 recvx 收发的位置(ring buffer 记录实现)、sendq、recv。当 channel 因为缓冲区不足而阻塞了队列,则使用双向链表存储。
Go 语言的 Channel 特性?
- 给一个 nil channel 发送数据,造成永远阻塞
- 从一个 nil channel 接收数据,造成永远阻塞
- 给一个已经关闭的 channel 发送数据,引起 panic
- 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
- 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
- 关闭一个 nil channel 将会发生 panic
Channel 的 ring buffer 实现
channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ringbuffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列
在 channel 中,ring buffer 的实现如下:
上图展示的是一个缓冲区为 8 的 channel buffer,recvx 指向最早被读取的数据,sendx 指向再次写入时插入的位置。