Bootstrap

面试总结:Golang常见面试题汇总

1. golang协程为什么比线程轻量?

  1. go协程切换比线程效率高

简单回答:
线程切换需要切换内核栈和硬件上下文的,而协程切换只发生在用户态,没有时钟中断、系统调用等机制,效率更高。

参考:线程与进程:问题6

详细解释:
线程是内核对外提供的服务,应用程序可以通过系统调用让内核启动线程,由内核来负责线程调度和切换。线程在等待IO操作时线程变为unrunnable状态会触发上下文切换。现代操作系统一般都采用抢占式调度,上下文切换一般发生在时钟中断和系统调用返回前,调度器计算当前线程的时间片,如果需要切换就从运行队列中选出一个目标线程,保存当前线程的环境,并且恢复目标线程的运行环境,最典型的就是切换ESP指向目标线程内核堆栈,将EIP指向目标线程上次被调度出时的指令地址。

go协程是不依赖操作系统和其提供的线程,golang自己实现的CSP并发模型实现(MGP),同时go协程也叫用户态线程,协程之间的切换发生在用户态,很轻量。在用户态没有时钟中断,系统调用等机制, 因此效率比较高。

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

  1. go协程占用内存少

执行go协程只需要极少的栈内存(大概是4~5KB),默认情况下,而线程栈的大小为1MB。goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行。

2. Golang中数组与切片比较?

数组:

  1. 数组是具有固定长度的,并且拥有0个或者多个相同元素的数据类型,数组在使用前必须确定数组长度。
  2. 数组的长度是数组类型的一部分,比如[3]int 和 [4]int是两种不同类型的数组,可以通过内置函数len(array)来获取数组的长度。
  3. 数组是值传递,像Java一样传的是内存地址,如果内存地址改变,之后的修改在代码块结束后不生效。

切片:

  1. 切片表示一个拥有相同类型的可变长度的序列。它对底层的数组(内部是通过数组保存数据的)进行了抽象,并提供相关的操作方法。切片在255容量内是2倍扩容,超过这个容量是1.25倍扩容。
  2. 相比数组,一般切片效率更高。
  3. 切片是地址传递。

len()返回切片中的元素个数。
cap()返回切片的容量即切片最长可以容纳的元素数量。

 type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

3. make和new的区别? go语言中的引用类型包含哪些?

  1. 从入参看,new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针,make接受参数是两个。
  2. make只能创建内建类型(slice、map、chan),因为对于引用类型的变量,不光要声明它,还要为它分配内容空间,同时返回这三个引用类型本身,并且初始化。new则是可以对所有类型进行内存分配,返回的是指向类型的指针。
  3. new返回指针, make 返回引用

指针变量存储的是另一个变量的地址。
引用变量指向另外一个变量。

引用类型:
数组切片、字典(map)、通道(channel)、接口(interface)

4. uint,int?

int包括有符号整型或无符号整型。

uint和uint8等都属于无符号int类型。

go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统就是4个字节,如果是64位就是8个字节。

5. 说说go语言的channel特性?

  1. 给一个 nil channel 发送数据,造成永远阻塞
  2. 从一个 nil channel 接收数据,造成永远阻塞
  3. 给一个已经关闭的 channel 发送数据,引起 panic
  4. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  5. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

6. 线程模型有哪些?为什么 Go Scheduler 需要实现 M:N 的方案?Go Scheduler 由哪些元素构成呢?

线程模型

在细说 Go 的调度模型之前,先来说说一般意义的线程模型。线程模型一般分三种,由用户级线程和 OS 线程的不同对应关系决定的。

  • N:1,即全部用户线程都映射到一个OS线程上,上下文切换成本最低,但无法利用多核资源;
  • 1:1 , 一个用户线程对应到一个 OS线程上, 能利用到多核资源,但是上下文切换成本较高,这也是 Java Hotspot VM 的默认实现;
  • M:N,权衡上面两者方案,既能利用多核资源也能尽可能减少上下文切换成本,但是调度算法的实现成本偏高。

为什么 Go Scheduler 需要实现 M:N 的方案? (为什么在内核的线程调度器之外Go还需要一个自己的调度器?)

  1. 线程创建开销大。对于 OS 线程而言,其很多特性均是操作系统给予的,但对于 Go 程序而言,其中很多特性可能非必要的。这样一来,如果是 1:1 的方案,那么每次 go func(){…} 都需要创建一个 OS 线程,而在创建线程过程中,OS 线程里某些 Go 用不上的特性会转化为不必要的性能开销,不经济

  2. 减少 Go 垃圾回收的复杂度。依据1:1方案,Go 产生所用用户级线程均交由 OS 直接调度。 Go 的垃圾回收器要求在运行时需要停止所有线程,才能使得内存达到稳定一致的状态,而 OS 不可能清楚这些,垃圾回收器也不能控制 OS 去阻塞线程。


Go Scheduler 由哪些元素构成呢?

  • G: Goroutine,Go 的用户级线程,常说的协程,真正携带代码执行逻辑的部分,由 go func(){…} 直接生成
  • P: Processor, 调度器的核心处理器,通常表示执行上下文,用于匹配 M 和 G 。P 的数量不能超过 GOMAXPROCS 配置数量,这个参数的默认值为CPU核心数;通常一个 P 可以与多个 M 对应,但同一时刻,这个 P 只能和其中一个 M 发生绑定关系;M 被创建之后需要自行在 P 的 free list 中找到 P 进行绑定,没有绑定 P 的 M,会进入阻塞态
  • M: Machine,就是 OS 线程本身,数量可配置

注:GOMAXPROCS 参数很重要,其决定了 P 的最大数量,也决定了自旋 M 的最大数量。线程自旋是相对于线程阻塞而言的,如果 G 迟迟不来,CPU 会白白浪费在这无意义的计算上。但好处也很明显,降低了 M 的上下文切换成本,提高了性能。Go 的设计者倾向于高性能的并发表现,为了避免过多浪费 CPU 资源,自旋的线程数不会超过 GOMAXPROCS。

在这里插入图片描述
本地队列(local queue): 本地是相对 P 而言的本地,每个 P 维护一个本地队列;与 P 绑定的 M 中如若生成新的 G,一般情况下会放到 P 的本地队列;当本地队列满了的时候,才会截取本地队列中 “一半” 的元素放入全局队列中;

全局队列(global queue):承载本地队列“溢出”的 G。为了保证调度公平性,schedule 过程中有 1/61 的几率优先检查全局队列,否则本地队列一直满载的情况下,全局队列中的 G 将永远无法被调度到;

窃取(stealing): 这似乎和 Java Fork-Join 中的 work-stealing 模型很相似,其目的也是一样,就是为了使得空闲(idle)的 M 有活干,不空等,提高计算资源的利用率。窃取也是有章法的,规则是随机从其他 P 的本地队列里窃取 “一半” 的 G。

一言蔽之,调度的本质就是 P 将 G 合理的分配给某个 M 的过程。

7. context包的用途?

context包的用途Context通常被译作上下文,它是一个比较抽象的概念,其本质,是存在上下层的传递,上层会把内容传递给下层。在Go语言中,程序单元也就指的是Goroutine。

8. 简述一下你对Go垃圾回收机制的理解?

三色标记法

  1. 起初所有对象都是白色。
  2. 从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
  3. 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
  4. 重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

在这里插入图片描述

我们来看普通的标记清楚算法是如何做的:
第一步,暂停程序业务逻辑, 找出不可达的对象,然后做上标记。第二步,回收标记好的对象。
对实时性要求比较高的系统来说,Mark-Sweep两个阶段,在Mark阶段会STW,这种需要长时间挂起的标记清除算法是不可接受的,而三色标记算法就很好的解决了这个问题。

Golang通过三色标记法,使得mark阶段和用户程序是并行的。先来看看如果mark阶段是与用户程序并发的,会产生如下问题:
在这里插入图片描述
可以知道,在mark阶段可能会有新的对象分配,这时候会出现对象的误标记为白色。为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响,如何能在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?这个时候就需要通过混合写屏障(hybrid write barrier)记录下来

9. 混合写屏障?

插入写屏障:

  • 强三色不变式:不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色。
    当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况,所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束。

删除写屏障:

  • 弱三色不变式:保护灰色对象到白色对象的路径不会断。
    这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

插入写屏障和删除写屏障的短板:

  • 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
  • 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

混合写屏障:

  1. GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
  2. GC期间,任何在栈上创建的新对象,均为黑色。
  3. 被删除的对象标记为灰色。
  4. 被添加的对象标记为灰色。

参考:Golang垃圾回收

下面代码有什么问题(或者输出是什么)?原因是什么?


  1. 写出下面代码输出内容。
package main

import (
    "fmt"
)

func main() {
    defer_call()
}

func defer_call() {
    defer func() { fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()

    panic("触发异常")
}

考点:
defer执行顺序
解答:
defer 是后进先出。
panic 需要等defer 结束后才会向上传递。 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。
输出:
打印后
打印中
打印前
panic: 触发异常


  1. 以下代码有什么问题,说明原因。
package main

import (
	"fmt"
)

type student struct {
    Name string
    Age  int
}

func pase_student() map[string]*student{
    m := make(map[string]*student)
    stus := []student{
        {Name: "zhou", Age: 24},
        {Name: "li", Age: 23},
        {Name: "wang", Age: 22},
    }
    for _, stu := range stus {
        m[stu.Name] = &stu
    }
	return m
}

func main() {
	m:=pase_student()
	fmt.Println(m)
}

解答:
与Java的foreach一样,都是使用副本的方式。所以m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝
输出:
map[li:0xc00011c018 wang:0xc00011c018 zhou:0xc00011c018]


  1. 下面的代码会输出什么,并说明原因
func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

考点:
go执行的随机性和闭包。
解答:
谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是A:均为输出10,B:从0~9输出(顺序不定)。 第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。 故go func执行时,i的值始终是10
第二个go func中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。


  1. 下面代码会输出什么?
type People struct{}

func (p *People) ShowA() {
    fmt.Println("showA")
    p.ShowB()
}
func (p *People) ShowB() {
    fmt.Println("showB")
}

type Teacher struct {
    People
}

func (t *Teacher) ShowB() {
    fmt.Println("teacher showB")
}

func main() {
    t := Teacher{}
    t.ShowA()
}

考点:
go的组合继承
解答:
这是Golang的组合模式,可以实现OOP的继承。 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能
输出:
showA
showB


  1. 下面代码会触发异常吗?请详细说明
func main() {
    runtime.GOMAXPROCS(1)
    int_chan := make(chan int, 1)
    string_chan := make(chan string, 1)
    int_chan <- 1
    string_chan <- "hello"
    select {
    case value := <-int_chan:
        fmt.Println(value)
    case value := <-string_chan:
        panic(value)
    }
}

考点:
select随机性
解答:
select会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则:

  • select 中只要有一个case能return,则立刻执行。
  • 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
  • 如果没有一个case能return则可以执行”default”块。

  1. 下面代码输出什么?
func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}

考点:
defer执行顺序
解答:
这道题类似第1题 需要注意到defer执行顺序和值传递
index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用calc(“10”,1,2)
执行index:2时,与之前一样,需要先调用calc(“20”,0,2)
执行到b=1时候开始调用,calc(“2”,0,2)
最后执行calc(“1”,1,3)。
输出:
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4


  1. 请写出以下输入内容
func main() {
    s := make([]int, 5)
    s = append(s, 1, 2, 3)
    fmt.Println(s)
}

考点:
make默认值和append
解答:
make初始化是由默认值的哦,此处默认值为0
输出:
[0 0 0 0 0 1 2 3]


  1. 下面的代码有什么问题?
type UserAges struct {
	ages map[string]int
	sync.Mutex
}

func (ua *UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}

func (ua *UserAges) Get(name string) int {
	if age, ok := ua.ages[name]; ok {
		return age
	}
	return -1
}

考点:
map线程安全
解答:
可能会出现fatal error: concurrent map read and map write. 读操作加锁即可。


  1. 下面的代码会有什么问题?
package main

import (
	"fmt"
)

type People interface {
	Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}


func main() {
	var peo People = Stduent{}
	think := "bitch"
	fmt.Println(peo.Speak(think))
}

考点:
golang的方法集
解答:
编译不通过。
Go本身不具有多态的特性,不能够像Java、C++那样编写多态类、多态方法。但是,使用Go可以编写具有多态功能的类绑定的方法。

比如下面的代码是可行的:

package main

import (
	"fmt"
)

type People interface {
	Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}

func speak(p People)  {
	fmt.Println(p.Speak("bitch"));
}

func main() {
	speak(&Stduent{})
	// var peo People = Stduent{}
	// think := "bitch"
	// fmt.Println(peo.Speak(think))
}

  1. 以下代码打印出来什么内容,说出为什么。
package main

import (
	"fmt"
)

type People interface {
	Show()
}

type Student struct{}

func (stu *Student) Show() {

}

func live() People {
	var stu *Student
	return stu
}

func main() {
	if live() == nil {
		fmt.Println("AAAAAAA")
	} else {
		fmt.Println("BBBBBBB")
	}
}

考点:
interface内部结构
解答:
很经典的题! 这个考点是很多人忽略的interface内部结构。
输出:
BBBBBBB

go中的接口分为两种。

  1. 一种是空的接口类似这样:
var in interface{}
  1. 另一种如题目:
type People interface {
    Show()
}

这两种接口的底层结构如下:

type eface struct {      //空接口
    _type *_type         //类型信息
    data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct {      //带有方法的接口
    tab  *itab           //存储type信息还有结构实现方法的集合
    data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
    size       uintptr  //类型大小
    ptrdata    uintptr  //前缀持有所有指针的内存大小
    hash       uint32   //数据hash值
    tflag      tflag
    align      uint8    //对齐
    fieldalign uint8    //嵌入结构体时的对齐
    kind       uint8    //kind 有些枚举值kind等于0是无效的
    alg        *typeAlg //函数指针数组,类型实现的所有方法
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}
type itab struct {
    inter  *interfacetype  //接口类型
    _type  *_type          //结构类型
    link   *itab
    bad    int32
    inhash int32
    fun    [1]uintptr      //可变大小 方法集合
}

可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口。

同理,下面也是非空的:

func Foo(x interface{}) {
	if x == nil {
		fmt.Println("empty interface")
		return
	}
	fmt.Println("non-empty interface")
}
func main() {
	var x *int = nil
	Foo(x)
}

  1. 是否可以编译通过?如果通过,输出什么?
func main() {
	i := GetValue()

	switch i.(type) {
	case int:
		println("int")
	case string:
		println("string")
	case interface{}:
		println("interface")
	default:
		println("unknown")
	}

}

func GetValue() int {
	return 1
}

考点:
type
解答:
编译失败,因为type只能使用在interface


  1. 下面函数有什么问题?
func funcMui(x,y int)(sum int,error){
    return x+y,nil
}

考点:
函数返回值命名
解答:
在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。


  1. 是否可以编译通过?如果通过,输出什么?
package main

func main() {
	println(DeferFunc1(1))
	println(DeferFunc2(1))
}

func DeferFunc1(i int) (t int) {
	t = i
	defer func() {
		t += 3
	}()
	return t
}

func DeferFunc2(i int) int {
	t := i
	defer func() {
		t += 3
	}()
	return t
}

考点:
defer和函数返回值
解答:
需要明确一点是defer需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数
DeferFunc1有函数返回值t作用域为整个函数,在return之前defer会被执行,所以t会被修改,返回4;
DeferFunc2函数中t的作用域为函数,返回1。


  1. 是否可以编译通过?如果通过,输出什么?
package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3}
	s2 := []int{4, 5}
	s1 = append(s1, s2)
	fmt.Println(s1)
}

考点:
append
解答:
append切片时候别漏了’…’


  1. 是否可以编译通过?如果通过,输出什么?
func GetValue(m map[int]string, id int) (string, bool) {
	if _, exist := m[id]; exist {
		return "存在数据", true
	}
	return nil, false
}
func main()  {
	intmap:=map[int]string{
		1:"a",
		2:"bb",
		3:"ccc",
	}

	v,err:=GetValue(intmap,3)
	fmt.Println(v,err)
}

考点:
函数返回值类型
解析:
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错:cannot use nil as type string in return argument.


  1. 是否可以编译通过?如果通过,输出什么?
const (
	x = iota
	y
	z = "zz"
	k
	p = iota
)

func main()  {
	fmt.Println(x,y,z,k,p)
}

考点:
iota
输出:
0 1 zz zz 4


  1. 下面函数有什么问题?
package main

const cl  = 100

var bl = 123

func main()  {
    println(&bl,bl)
    println(&cl,cl)
}

考点:
常量
解析:
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。会报错:cannot take the address of cl


  1. 编译执行下面代码会出现什么?
package main
import "fmt"

func main()  {
    type MyInt1 int
    type MyInt2 = int
    var i int =9
    var i1 MyInt1 = i
    var i2 MyInt2 = i
    fmt.Println(i1,i2)
}

考点:
Go 1.9 新特性 Type Alias
解析:
基于一个类型创建一个新类型,称之为defintion;基于一个类型创建一个别名,称之为alias。 MyInt1为称之为defintion,虽然底层类型为int类型,但是不能直接赋值,需要强转; MyInt2称之为alias,可以直接赋值。输出为:cannot use i (type int) as type MyInt1 in assignment


  1. 编译执行下面代码会出现什么?
package main

func test(x int) (func(),func())  {
    return func() {
        println(x)
        x+=10
    }, func() {
        println(x)
    }
}

func main()  {
    a,b:=test(100)
    a()
    b()
}

考点:
闭包引用相同变量
输出:
100
110

;