参考链接: 1.1 Go 中的 = 和 := 有什么区别? — Go语言面试宝典 1.0.0 documentation (iswbm.com)
本文是对上诉该链接的一些总结复习,属于查缺补漏把。下面所列举的一些代码均来自上述网站,本文对此进行借鉴。
1. go中的= 和:=的区别
= 只是用于赋值,在此之前需要进行声明
:= 可以用于声明并赋值,但是必须在函数内
2. go中指针的意义是什么,指的是&var而不是unsafe.Pointer
(1)省内存,golang都是值传递,如果参数传递的是一个数组,那么会每次都复制一份占用大量的内存
(2)易编码,如果不使用指针类型,有时候return的时候需要再重新new一个对象
3. go多返回值的作用
多变量返回易于编程,省去了中间变量的声明,比如 a, b = swap(b, a)
还有一个,经常用于判断是不是有err,所以除了返回值外需要多定义一个error
4. go有异常类型吗
go没有异常类型(panic),只有错误类型(error),go的一场和错误是可以相互转化的。
(1)错误转异常:比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
(2)异常转错误:比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。
5. rune和byte有什么区别
一个byte是8bit,是uint8的别名类型,但是byte只能表示ASCII的一个字符,一个rune是32bit,是uint32的类型,包括4个byte,可以表示更多的编码。
6. 什么是go的浅拷贝和深拷贝
浅拷贝拷贝的是数据地址,只复制指向对象的指针,引用类型默认都是浅拷贝,深拷贝会重新开辟内存地址,初始化一样的值。
比如 a := []int{1,2,3}
b := a b的地址和a一样
copy(c, a) c是开辟一个新的地址
7. 什么是字面量和组合字面量
字面量就是未命名常量,比如"abc",0xF 0i17 0b111
字面量和变量的区别在于,字面量没法取地址,变量可以取地址
func foo() string{
return "hello"
}
func main(){
fmt.Println(&foo()) //失败,返回的hello是字面量,无法取地址
a := foo()
fmt.Println(&a) //成功,此时是一个变量
}
组合字面量是为结构体、数组、切片和map构造值,并且每次都会创建新值。它们由字面量的类型后紧跟大括号及元素列表。每个元素前面可以选择性的带一个相关key。所谓的组合字面量其实就是把对象的定义和初始化放在一起了。
type Profile struct {
Name string
Age int
Gender string
}
func main() {
// 声明 + 属性赋值
xm := Profile{
Name: "iswbm",
Age: 18,
Gender: "male",
}
}
在初始化Profile类型的时候就把值定义了,并且初始化了。
8. 对象选择器自动解引用
.这个操作符叫做选择器,自己自动定位指针或者对象变量,访问到对象的子对象。定义方法的时候,需要定义一个接收器。
type Profile struct {
Name string
}
func(p *Profile) say)(){
fmt.Pritnln(p.Name)
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm 正常写法
fmt.Println(p1.Name) // output: iswbm 自动识别指针对象
}
9. map不可寻址,如何修改值的属性
package main
type Person struct {
Age int
}
func (p *Person) GrowUp() {
p.Age++
}
func main() {
m := map[string]Person{
"iswbm": Person{Age: 20},
}
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
}
因为map的值是不可寻址的,所以m["iswbm"]返回的是原来的值的一个拷贝,而不是同样地址的一个引用,所以不能直接这么修改。
可以用一个变量承接,比如 p := m["iswbm"],再修改,或者 "iswbm": &Person{Age:20}用指针的方式来处理。
10. 有类型常量和无类型常量的区别
当你把有无类型的常量,赋值给一个变量的时候,无类型的常量会被隐式的转化成对应的类型,可要是有类型常量,不就会进行转换,在赋值的时候,类型检查就不会通过,从而直接报错
// 无类型常量
const RELEASE = 3
const RELEASE2 int = 3
func main() {
var x int16 = RELEASE
var y int32 = RELEASE
fmt.Printf("type: %T \n", x) //type: int16
fmt.Printf("type: %T \n", y) //type: int32
var x int16 = RELEASE2 //报错,无法显式转换
}
11. 为什么传参使用切片而不使用数组?
因为数组是值类型,切片是引用类型,如果传递的参数用的是数组,每次都会复制,就会造成很多内存浪费,如果每次传递的是切片,实际上传递的是一个指向堆数组的指针,能节省很多空间。当然可以传指针数组。
12. Go 语言中 hot path 有什么用呢?
hot path ,热点路径,顾名思义,是你的程序中那些会频繁执行到的代码。目的就是针对频繁访问的一些代码做优化,能够带来明显的效果。
在机器码中,这个偏移量是传递指令的附加值,这会使指令变得更长。对性能的影响是,CPU必须对结构指针添加偏移量以获取想要访问的字段的地址。
因此访问struct的第一个字段的机器码更快,更加紧凑。
13 引用类型和指针,有什么不同
切片是一个引用类型,作为参数传递的时候,改变能够反映到实际参数上的,但是如果在函数内进行了扩容,则不会反映到实参,因为扩容之后指向的是一块新开辟的内存地址,而原始数据函数指向的还是一个旧的地址,此时就需要再返回一下指针。
14. go是指传递还是引用传递、指针传递
go语言都是值传递,不是引用传递也不是指针传递。只是引用类型传递的是地址的值。
15.go中哪些是可寻址,哪些是不可寻址的
可以用&操作符获取地址的对象就是可寻址的,不能的就是不行。
可寻址: 变量,指针, 数组元素,切片, 切片元素, 组合字面量(struct)主要指的是能够对下面的字段寻址。
不可寻址: 常量,字符串,函数和方法,基本类型字面量, map中的元素, 数组字面量
1. Slice扩容后内存如何计算
(1)首先我们初始化一个大小为2的切片,然后添加3个元素,那么此时应该是为5个。扩容,如果大小小于1024时,需要扩容就是扩张为两倍,如果大于1024时,就是1.25倍。但是如果依旧小于扩容后的长度,那么就以扩容后的长度为准。
(2)扩容的时候需要考虑到内存对其的问题,内存分配一直是分配为2^n个,比如我们需要分配40个字节,处于32-48之间,我们只能分配48(偶数*8)
2. goroutine存在的意义是什么
线程其实分两种:
因此,goroutine 的存在必然是为了换个方式解决操作系统线程的一些弊端 – 太重 。
3. 说说go闭包的底层原理
一个函数内引用了外部的局部变量,这种现象,就称之为闭包。下面的代码中defer的func就引用了局部变量i
闭包中引用的外部局部变量并不会随着 adder 函数的返回而被从栈上销毁
import "fmt"
func func1() (i int) { //i在外部还要使用,所以变量是在堆上申请的
i = 10
defer func() {
i += 1
}()
return 5
}
func func2() (int) {
i := 10
defer func() {
i += 1
}()
return i //在返回值写了变量名,返回值是在上级的栈内存申请的,直接赋值该变量,所以func2的i是在外面的栈上
}
func main() {
closure := func1()
fmt.Println(closure)
}
(1)闭包函数里引用的外部变量,是在堆还是栈内存申请的,取决于,你这个闭包函数在函数 Return 后是否还会在其他地方使用,若会, 就会在堆上申请,若不会,就在栈上申请。
(2)闭包函数里,引用的外部变量,存储的并不是对值的拷贝,存的是值的指针。
(3)函数的返回值里若写了变量名,则该变量是在上级的栈内存里申请的,return 的值,会直接赋值给该变量。
4. defer的变量快照什么时候会失效
func func1() {
age := 0
defer fmt.Println(age) // output: 0
/*
defer func() {
fmt.Println(age)
}()
*/
age = 18
fmt.Println(age) // output: 18
}
func main() {
func1()
}
若 defer 后接的是单行表达式,(没注释的内容)那defer 中的 age 只是拷贝了 func1 函数栈中 defer 之前的 age 的值;
若 defer 后接的是闭包函数,(注释了的内容)那defer 中的 age 只是存储的是 func1 函数栈中 age 的指针。
5. go的抢占式调度的了解
go在1.1版本中,只有一个协程主动让出CPU资源时才能触发调度,进行下一个协程
在1.2版本中,sysmon监控线程发现协程执行太长了(gc或者stw时)就会设置抢占标记,当这个协程在call函数服用到morestack的逻辑时会检查是否有抢占标记,如果有就会切换调度主线程。
在1.14之后,基于信号的抢占式调度,只要协程超过一定时间(10s)就会强行夺取cpu运行权
https://github.com/golang/gofrontend/blob/20e74f9ef8206fb02fd28ce3d6e0f01f6fb95dc9/libgo/go/runtime/proc.go#L4938
6. go栈空间的扩容/缩容过程
go的协程初始栈为2k,随着调用层级的增加会触发栈空间的扩容。
扩容触发:
在调用函数时会检查runtime.morestack,检查goroutine内存是否充足,如果不充足会调用runtime.newstack创建新的栈,新栈的大小是旧栈的两倍,最大栈空间不能超过maxstacksize, 1G
缩容触发:
函数返回后栈空间会回收,如果返回的多,内存利用率太低,在垃圾回收的时候会检查栈空间内存利用率,如果低于25%的时候就进行缩容,缩成原来的50%,但是最低至少为2K。
不过是扩容还是缩容都是调用runtime.copystack进行开辟新的空间,然后把旧栈的数据拷贝的新栈,再调整指针的指向。
7. 说说GMP的原理
2.7 说一下 GMP 模型的原理 — Go语言面试宝典 1.0.0 documentation (iswbm.com)
G是Goroutine,go提供的轻量级线程,每个goroutine大概需要4K内存
M是Thread (Machine缩写),对应的是操作系统的内核线程,做多不超过1W个
P是processor,处理器,用于协调goroutine和thread,通过gomaxprocs进行设置
两个队列,全局队列和本地队列(对于每个p各自有一个队列)
每个P绑定一个M,自带一个队列用于存放G,当队列空了的时候会自动从全局队列获取G进行执行,如果全局队列为空,会从其他人的队列中获取一个G
策略:
复用线程可以避免线程的频繁创建销毁,而是对线程的复用
work stealing机制可以偷取G,而不是销毁线程
hand off机制为当G因为系统调用而导致阻塞时,可以把P转移给其他控线线程执行。
gomaxprocs是CPU核数/2
一个goroutine最多占用10ms避免其他的goroutine被饿死,这是抢占式调度,
8. GMP模型为什么要有P
在v1.1的时候是没有P的,M直接从全局队列获取G来执行,加了P之后,可以大幅度减轻对全局队列的依赖,可以实现work stealing和hand off
9. 不分配内存的指针类型能用吗
不能
package main
import (
"fmt"
)
func main() {
var i *int
i = new(int) //如果没有分配空间,就会出错
*i=10
fmt.Println(*i)
}
10. 如何在强制转换类型时不发生内存拷贝
[]byte()和string的切换,string底层是一个[]byte指针,直接转换的话就是把直接地址赋予过去
11.go的gc演变
go v1.3之前是标记清除,先stw,然后遍历所有的对象,不可达的标志为白色,然后清理,耗时太久。
go 1.5开始采用三色标记法。
[典藏版]Golang三色标记、混合写屏障GC模式图文全分析 - 个人文章 - SegmentFault 思否
黑色:有被引用并且遍历完所有对象和属性
白色:还没有检测
灰色:检测有被引用但是还有属性没遍历完
为了避免标记时对象被引用或者取消引用,可以使用插入屏障和删除屏障。
插入屏障:当A要引用B时,B被标记为灰色。
删除屏障:如果资深为灰或白时,就标记为灰
12. go哪些动作会触发runtime调度
(1)系统调用,比如sleep,读磁盘,网络请求之类的
(2)等待锁和通道时,因为阻塞了,也会触发调度
(3) 调用 runtime.Gosched会调用
1. 局部变量分配在栈上还是堆上?
堆内存:由内存分配器和垃圾收集器负责回收
栈内存:由编译器自动进行分配和释放
一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
而堆内存呢?由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要 GC (垃圾回收)的介入参与,如果有大量的 GC 操作,将会使程序性能下降得历害。
一般来说,局部变量的作用域仅在该函数中,当函数返回后,所有局部变量所占用的内存空间都将被收回,对于这类变量,都是从栈上分配内存空间
但是如果局部变量在函数外还会继续使用,那么是在堆上分配的,go编译器会自动优化。
2. 为什么常量、字符串和字典不可寻址?
不可寻址是指的不能通过&获取内存地址。
如果常量可以寻址,就可以通过指针修改常量的值,不符合要求
如果map可寻址,在遇到不存在的元素和map扩张的时候,地址就会变化了,寻址没有意义。
字符串每次修改之后都指向一个新的地址,因此寻址也没有意义。
3. 为什么 slice 元素是可寻址的?
slice存放元素的是一个匿名数组,数组的元素是可以寻址的,因此切片的元素也应该是可以寻址的
4. Go 的默认栈大小是多少?最大值多少?
// rumtime.stack.go
// The minimum size of stack used by Go code
_StackMin = 2048
var maxstacksize uintptr = 1 << 20 // enough until runtime.main sets it for real
5. Go 中的分段栈和连续栈的区别?
早期的go版本使用的栈是分段栈,随着goroutine的层级深入会调用runtime.morestack和runtime.newstack创建新的栈空间,以双向链表的方式串联起来。但是如果在循环中调用函数,会一直分配释放造成巨大的额外开销,称为热分类问题。
现在的go版本使用了连续栈,初始化一片比旧栈大两倍的新栈并且将所有的值都迁移进新栈中,会有以下几个步骤
(1)调用用runtime.newstack在内存空间中分配更大的栈内存空间;
(2)使用runtime.copystack将旧栈中的所有内容复制到新的栈中;
(3)将指向旧栈对应变量的指针重新指向新栈;
(4)调用runtime.stackfree销毁并回收旧栈的内存空间;
6. 内存对齐、内存布局是怎么回事?
字长指的是CPU一次可以访问数据的最大长度,对于32位的CPU,字长是4byte,对于64位的CPU,字长是8byte
如果内存布局按顺序的话,对于以下的对象,占用的内存大小是13(4+8+1),为什么第二个是8而不是3呢,因为指针的内存对齐系数是8,但是此时访问Bar.y的时候,需要访问内存两次
但是如果内存布局按字长的话,cpu只需要访问内存一次,第一个int32,占据4个byte,但是由于运行在64位的机器上,所以实际上占用了8byte,而最后一个bool也需要占用8byte,如果先 int32 bool *Foo,那么就能够只使用16byte的空间
type Foo struct {
A int8 // 1
B int8 // 1
C int8 // 1
}
type Bar struct {
x int32 // 4
y *Foo // 8
z bool // 1
}
7. Go 里是怎么比较相等与否?
如果要比较一个Interface{},那么会比较它所拥有的的字段type和data。
(1) 当类型和数值都相等的时候,两个interface{}相等
(2) 当type和data都处于unset状态时,那么interface就是nil,此时也是相等的
(3) 当interface{}和具体类型变量进行比较的时候,会将非interface{}转化为interface进行比较,比较类型和数值
(4) 当和nil本身进行比较的时候,是不相等的,比如
package main
import (
"fmt"
"reflect"
)
func main() {
var a *string = nil
var b interface{} = a
fmt.Println(b==nil) // false
}
a转化为interface后是(type=nil, data=nil),但是b实际上是(type=*string, data=nil)这是不一样的
8. 所有的 T 类型都有 *T 类型吗?
所谓的*T类型,就是指向T类型的指针,在前文说过了,对于不可寻址的内容,是无法构建指向它们的指针的。比如常量、map的值,string。如下会报错:
package main
import "fmt"
type T string
func (T *T) say() {
fmt.Println("hello")
}
func main() {
const NAME T = "iswbm"
NAME.say()
}
9. 数组对比切片有哪些优势?
数组由于是定长,在编译时就编译器就可以检查是否越界
长度是类型的一部分,反射检查的时候就可以直接验证是不是合法的数组
reflect.TypeOf(array1) == reflect.TypeOf(array2)
类型相同的数组可以比较,从而,固定长度的数组可以作为map的key使用
10. GMP 偷取 G 为什么不需要加锁?
GMP从本地丢列取G的操作是一个GAS操作,具有原子性,由硬件直接支持,没有并发竞争。加锁是操作系统层面的实现的。
但是CAS操作虽然简单但是需要付出一定的代价: