[golang系列] 一口气复习golang总结

参考链接: 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 ,热点路径,顾名思义,是你的程序中那些会频繁执行到的代码。目的就是针对频繁访问的一些代码做优化,能够带来明显的效果。

  • 当需要访问struct的第一个字段时,我们可以直接对指针解引用来访问第一个字段。
  • 要访问其他字段时,除了结构指针之外, 还需要提供与第一个字段的偏移量

在机器码中,这个偏移量是传递指令的附加值,这会使指令变得更长。对性能的影响是,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存在的意义是什么

线程其实分两种:

  • 一种是传统意义的操作系统线程(创建和切换都要进入内核,开销打)
  • 一种是编程语言实现的用户态线程,也称为协程,在 Go 中就是 goroutine(一般的线程为了避免栈溢出一开始分配比较大的栈内存,而goroutine默认只有2K并且还能够自动收缩扩张。)

因此,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 的默认栈大小是多少?最大值多少?

  • v1.0 ~ v1.1 — 最小栈内存空间为 4KB;
  • v1.2 — 将最小栈内存提升到了 8KB;
  • v1.3 — 使用连续栈替换之前版本的分段栈;
  • v1.4 — 将最小栈内存降低到了 2KB;
  • 关于这个初始值和最大值,在 Go 的源码 runtime/stack.go 里其实都可以找到
  • 默认栈大小是2K,最大值大概1G
  • // 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操作虽然简单但是需要付出一定的代价:

    1. 为了保证CAS执行成功,需要for循环不断尝试知道成功才返回,这样会阻塞其他硬件对CPU的访问,开销较大。
    2. 每次使用CAS原子操作只能对一个共享变量操作

猜你喜欢

转载自blog.csdn.net/u013379032/article/details/132417420
今日推荐