Go 面试

Golang基础

Golang vs Java
1、go不允许函数重载
2、go速度更快
3、go没有继承、不允许多态

Golang优势:在语言层面支持高并发

基础语法

1、main函数注意事项

main函数不能带参数。
main函数不能定义返回值。
main函数所在的包必须为main包。
main函数中可以使用flag包来获取和解析命令行参数

2、new 和 make的区别
make和new都是用于内存分配

new :作用是初始化一个指向某类型的指针(*Type ),使用new函数来分配空间。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。new既可以开辟基本数据类型的空间也可以开辟结构体类型的空间,但他不能初始化赋值。
make :作用是为 slicemake([]int,len,cap) s1:=make([]int,5),mapmake(map[string]int,10) 或 chanmake(chan int,1) 初始化并返回引用他们(他们本身就是引用)。

2.1、引用类型
切片、map、chanel、interface

2.2、内存管理、逃逸分析
Golang 通过逃逸分析,对内存管理进行的优化和简化,解决了内存的分配和销毁,它可以决定一个变量是分配到堆还栈上。
逃逸分析:是指编译器根据代码的特征和生命周期,自动的把变量分配到堆或者是栈上面。从而提高内存的使用效率。通过逃逸分析,那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了不但同时减少 GC 的压力,还减轻了内存分配的开销。
常见逃逸:func数据类型、interface{}空接口类型、指针类型、

3、switch

单个case中,可以出现多个结果选项
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case。
即相当于默认java中的switch每个分支都默认有break

//switch后面的表达式甚至不是必需的,单个case中,可以出现多个结果选项
switch { 
    case 0 <= Num && Num <= 3: 
        fmt.Printf("0-3") 
    case 4 <= Num && Num <= 6: 
        fmt.Printf("4-6") 
    case 7 <= Num && Num <= 9: 
        fmt.Printf("7-9") //http://www.cnblogs.com/osfipin/
}  

当满足多个case时,执行第一个满足的case后退出switch-case语句块

func main() {
	num := 8
	switch {
	case num < 10:
		fmt.Println("num < 10")
	case num > 6:
		fmt.Println("num > 6")
	}
}//输出:num < 10

4、for循环

for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环

func main() {

   // 不使用标记
   fmt.Println("---- break ----")
   for i := 1; i <= 3; i++ {
      fmt.Printf("i: %d\n", i)
      for i2 := 11; i2 <= 13; i2++ {
         fmt.Printf("i2: %d\n", i2)
         break
      }
   }

   // 使用标记
   fmt.Println("---- break label ----")
   re:
      for i := 1; i <= 3; i++ {
         fmt.Printf("i: %d\n", i)
         for i2 := 11; i2 <= 13; i2++ {
         fmt.Printf("i2: %d\n", i2)
         break re
      }
   }
}

5、init函数
作用
初始化不能采用初始化表达式初始化的变量
程序运行前的注册
实现sync.Once功能
特点
init函数先于main函数自动执行,不能被其他函数调用
init函数没有输入参数、返回值
每个包可以有多个init函数
包的每个源文件也可以有多个init函数
同一个包的init执行可能是乱序
不同包的init函数按照包导入的依赖关系决定执行顺序
执行顺序
变量初始化 -> init() -> main()

6、defer

在函数中使用defer关键字,是函数在执行结束后再执行defer修饰的语句。
同一函数中有多个defer语句时,会按照栈数据结构处理,先进后出 倒序执行

defer和return:

return 语句不是原子操作,而是被拆成了两步:1和3
给返回值赋值
调用 defer 表达式
返回给调用函数(ret)

func main() {
	fmt.Println(increase(1))
}

func increase(d int) (ret int) {
	defer func() {
		ret++
	}()

	return d
}//输出:2

如果有Panic:

func defer_call() {
	defer func() { fmt.Println("打印前") }()
	defer func() { fmt.Println("打印中") }()
	panic("触发异常")
	defer func() { fmt.Println("打印后") }()
}//输出:
//打印中
//打印前
//panic: 触发异常

7、nil和empty

以slice为例

var slice1 []int
slice1[1] = 0//报错

slice := make([]int,0)
slice := []int{}

8、不同Struct之间不能比较;同一结构体变量如果包含不可被比较的类型(slice、map、func)也不可以比较

func main() {
    type A struct {
        a int
    }
    type B struct {
        a int
    }
    a := A{1}
    //b := A{1}
    b := B{1}
    if a == b {
        fmt.Println("a == b")
    }else{
        fmt.Println("a != b")
    }
} //编译报错:Invalid operation: a == b (mismatched types A and B)

9、切片
Go得切片又称为 动态数组,它实际上是基于数组做的一层封装。
Go语言的数组是值类型,将数组赋值给另一数组,是将其拷贝一份。因此Golang中传递数组就没有传递指针效率高了。并且Golang中数组的长度是固定的,且不同长度的数组是不同类型,这样的限制就有很多局限性,而切片是已拥有相同类型的元素的可变长度序列,可以方便的进行扩容和传递,切片也是引用类型。
切片结构包括指向底层数组的指针、len长度、cap容量

9.1、切片如何扩容

slice内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。slice本身是一个只读对象,其工作机制类似数组指针的一种封装
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组

9.2、切片遍历
使用range遍历,一个值接受则为下标;两个值 i v 接受则为下标+值。这时的v并不是切片 i 位置,而是将 i 位置的值拷贝到 v。

9.3、切片拷贝
使用copy函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片中,拷贝数量取两个切片的最小值。(copy方法是深拷贝,会创造一个新对象)
slice2 := slice1是浅拷贝

10、指针接收者和值接受者

Go中的方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。
需要修改变量的时候,只能使用指针接收者

11、如何实现继承
可以通过结构体组合的方式来实现继承属性

12、map底层实现
map使用Hash表和搜索树作为底层实现,一个Hash表可以有多个bucket,而每个bucket保存了map中的一个或一组键值对。
先根据key哈希运算的结果分配到一个bucket,再使用链表解决冲突。
bucket桶最多会装8个key,通过key哈希计算后的高8位来决定key落在桶的那个位置。当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。这时会把bmap的 overflow 移动到 extra 字段来
参考文章

12.1、map是线程安全的吗
不是。并发访问会panic。
map使用的大多数情况是不需要线程安全的,如果为了少数场景而加锁降低效率是不合理的。
如需使用线程安全的map,可以使用sync.Map

13、类型断言
a、value, ok := a.(string)
b、结合switch 使用

switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T", t)       // %T prints whatever type t has    break
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool    break
case int:
    fmt.Printf("integer %d\n", t)             // t has type int    break
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool    break
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int    break
}

GMP 协程 锁

0、概念

只需要在函数调用前添加go关键字即可实现go的协程,创建并发任务。(关键字go并非执行并发任务,而是创建一个并发任务单元)
协程之间通过channel来进行协程间的通信。
G(Goroutine): 代表协程,存储了 Goroutine 的执行栈信息、Goroutine 状态以及 Goroutine 的任务函数等。创建一个 G 的初始栈大小为2-4K。
M(Machine): 对OS内核态线程的封装(通常M个数为服务器线程数)。M在绑定有效的 P 后,进入一个调度循环,而调度循环的机制大致是从 P 的本地运行队列以及全局队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。
P (Processor): 逻辑处理器,即为G和M的调度对象,用来调度G到M上执行,其数量可通过 GOMAXPROCS()来设置(通常P个数为服务器核心数)。P 的数量决定了系统内最大可并行的 G 的数量
Sched 调度器结构,维护存储M和G的全局队列

GO调度器的调度过程,首先创建一个G对象,然后G被保存在P的本地队列或者全局队列(global queue)。这时P会唤醒一个M。P按照它的执行顺序继续执行任务。M寻找一个空闲的P,如果找得到,将G与自己绑定。然后M执行一个调度循环:调用G对象->执行->清理线程->继续寻找Goroutine。
在M的执行过程中,上下文切换随时发生。当切换发生,任务的执行现场需要被保护,这样在下一次调度执行可以进行现场恢复。M的栈保存在G对象中,只有现场恢复需要的寄存器(SP,PC等),需要被保存到G对象。

0、协程和线程区别、为什么goroutine 为什么轻量
a、用户态的轻量级线程,协程的调度完全由用户控制。
b、协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
c、对于进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度。而对于协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制。

0.1、gmp当一个g堵塞时,g、m、p会发生什么
p会和m解绑,寻找下一个可用的m
g&m在阻塞结束之后会优先寻找之前的p,如果此时p已绑定其他m,当前m会进入休眠,g以可运行的状态进入全局队列

1、并发控制

通过sync包中的WaitGroup实现并发控制
Add增加1,Done减少1,Wait阻塞主线程等待WaitGroup减只0

var wg sync.WaitGroup

func hello(i int){
	defer wg.Done()//goroutine结束就登记-1
	fmt.Println("hello Goroutine",i)
}
func main(){
	for i:=0;i<10;i++{
		wg.Add(1)//启动一个goroutine就登记+1
		go hello(i)
	}
	//time.Sleep(3)//时间不够就可能有的goroutine执行不到
	wg.Wait()//等待所有登记的goroutine结束
}

1.1、等待所有goroutine结束
a、使用chanel(前提是知道所有goroutine数量)
b、使用waitgroup

2、互斥锁Mutex、读写互斥锁RWMutex、sync.Map安全锁

当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex
RWMutex在读锁占用的情况下,会阻止写,但不阻止读
RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占
golang中的sync.Map是并发安全的,其实也就是sync包中golang自定义的一个名叫Map的结构体,开箱即用var smap sync.Map

2.1、获取不到锁会一直等待吗
会。
Mutex 增加了饥饿模式,让锁变得更公平,不公平的等待时间限制在 1 毫秒。一旦等待者等待时间超过这个时间阈值,就可能会进入饥饿模式,优先让等待着先获取到锁。Mutex 锁不会容忍一个 goroutine 被落下,永远没有机会获取锁。

3、一次执行Once

在有些场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等
sync.Once提供的func (o *Once) Do(f func())方法,参数只能是没有参数列表且没有返回值列表的函数

以读取配置文件为例:

type Config struct {
	Server string
	Port   int64
}

var (
	once   sync.Once
	config *Config
)

func ReadConfig() *Config {
	once.Do(func() {
		var err error
		config = &Config{Server: os.Getenv("TT_SERVER_URL")}
		config.Port, err = strconv.ParseInt(os.Getenv("TT_PORT"), 10, 0)
		if err != nil {
			config.Port = 8080 // default port
        }
        log.Println("init config")
	})
	return config
}

func main() {
	for i := 0; i < 10; i++ {
		go func() {
			_ = ReadConfig()
		}()
	}
	time.Sleep(time.Second)
}

4、channel

语法:ch=make(chan int)make(chan int,16),也可以设置为单向通道。
不带缓冲的chanel只能写一个读一个,写满(1个)就会阻塞,这种情况只有其他协程中有对应的读才能解除阻塞。
带缓冲的chanel写满+1才阻塞。

//生产者
func producer(ch chan<- int){
	i:=0
	for{
		ch<-i
		i++
	}
}
//消费者
func customer(ch <-chan int){
	for{
		num:=<-ch
		fmt.Println(num)
	}
}
func main(){
	var ch=make(chan int,100)
	go producer(ch)
	go customer(ch)
	time.Sleep(time.Millisecond)
}

4.1、对已经关闭的channel进行读写操作会发生什么
a、读已经关闭的channel无影响。
如果在关闭前,通道内部有元素,会正确读到元素的值;如果关闭前通道无元素,则会读取到通道内元素类型对应的零值。
若遍历通道,如果通道未关闭,读完元素后,会报死锁的错误:fatal error: all goroutines are asleep - deadlock!
b、写已关闭的通道,会引发panic: send on closed channel
c、关闭已关闭的通道,会引发panic: close of closed channel

4.2、同一个协程中对无缓冲chanel同时发送和接受数据有什么问题
不能对无缓冲channel同时发送和接收数据,如果这么做会直接报错死锁。channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。

4.3、chanel 对比 锁
并发问题可以用chanel解决也可以用Mutex解决,但他们擅长解决的问题有些不同:
Chanel关注的是并发问题的数据流动,适用于数据在多个协程中流动的场景。
mutex关注的是某一场景下只给一个协程访问数据的权限,适用于数据位置固定的场景。

4.4、chanel使用场景
a、超时处理往chanel放入time.After(time.Second);定时任务
b、解耦生产者 消费者
c、控制并发数

4.5、如何判断chanel是否已经关闭
读:v,ok:= chanel关闭的时候ok为false
写:写之前通过读的方式判断是否chanel关闭

4.6、chanel是否线程安全
chanel是线程安全的。使用场景本身就是多线程的,所以必须设计成线程安全。
chanel底层使用了mutex来保证线程安全。

5、select

select可以同时响应多个通道的操作,每个case会对应一个通道的接受或发送过程
select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句

select{
	//选哪个case是随机的,不是从上到下
	case <-ch1:
		....
	case data:=<-ch2:
		...
	case ch3<-data
		...
	...
	default:
		...
}

6、协程池
使用chanel来限制最大协程数,使用WaitGroup来保证主线程阻塞等待所有协程完成

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

// Pool Goroutine Pool
type Pool struct {
	queue chan int
	wg *sync.WaitGroup
}

// New 新建一个协程池
func NewPool(size int) *Pool{
	if size <=0{
		size = 1
	}
	return &Pool{
		queue:make(chan int,size),
		wg:&sync.WaitGroup{},
	}
}

// Add 新增一个执行
func (p *Pool)Add(delta int){
	// delta为正数就添加
	for i :=0;i<delta;i++{
		p.queue <-1
	}
	// delta为负数就减少
	for i:=0;i>delta;i--{
		<-p.queue
	}
	p.wg.Add(delta)
}

// Done 执行完成减一
func (p *Pool) Done(){
	<-p.queue
	p.wg.Done()
}

// Wait 等待Goroutine执行完毕
func (p *Pool) Wait(){
	p.wg.Wait()
}

func main(){
	// 这里限制5个并发
	pool := NewPool(5)
	fmt.Println("the NumGoroutine begin is:",runtime.NumGoroutine())
	for i:=0;i<20;i++{
		pool.Add(1)
		go func(i int) {
			time.Sleep(time.Second)
			fmt.Println("the NumGoroutine continue is:",runtime.NumGoroutine())
			pool.Done()
		}(i)
	}
	pool.Wait()
	fmt.Println("the NumGoroutine done is:",runtime.NumGoroutine())
}

6.1、WaitGroup注意事项
add一个负数:当计数器的值小于0会直接panic
WaitGroup有nocopy字段,不能被复制,也就不能作为参数传递

7、G0

在Go中 g0作为一个特殊的goroutine,为 scheduler 执行调度循环提供了场地(栈)。对于一个线程来说,g0 总是它第一个创建的 goroutine。
线程会不断地寻找其他普通的 goroutine 来执行,直到进程退出。
g0 其他的一些“职责”有:创建 goroutine、deferproc 函数里新建 _defer、垃圾回收相关的工作(例如 stw、扫描 goroutine 的执行栈、一些标识清扫的工作、栈增长)等等

8、goroutine和线程区别
一个线程可以有多个协程
线程、进程都是同步机制,协程可以异步
协程可以保留上一次调用时的状态,当过程重入时,相当于进入了上一次的调用状态
协程是需要线程来承载运行的,所以协程并不能取代线程,线程是被分割的CPU资源,协程是组织好的代码流程

1、读写锁底层怎么实现的
普通的Mutex是不区分 goroutine 对共享资源的操作行为的,无论是读操作还是写操作都会上锁。RWMutex 读写锁的诞生为了区分读写操作,在进行读操作时,goroutine 就不必傻傻的等待了,而是可以并发地访问共享资源,将串行读变成了并行读,提高了读操作的性能。
Go 中的读写锁,工作模型是 Write-prferring 方案。优先写 防止写饥饿。

2、互斥锁Mutex、读写互斥锁RWMutex、sync.Map安全锁

当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex
RWMutex在读锁占用的情况下,会阻止写,但不阻止读
RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占
golang中的sync.Map是并发安全的,其实也就是sync包中golang自定义的一个名叫Map的结构体,开箱即用var smap sync.Map

2.1、sync.Map
采用读写分离和用空间换时间的策略保证 Map 的读写安全

3、如何实现一个线程安全的 map
读写锁、分片加锁、synv.Map

4、mutex不是可重入锁
Mutex 的实现中,没有记录哪个 goroutine 拥有这把锁。

零散

1、go 实现不重启热部署
根据系统的 SIGHUP 信号量,以此信号量触发进程重启,达到热更新的效果

2、go test
Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。
执行测试有以下原则:
文件名必须是_test.go结尾的
import testing这个包
测试用例函数必须是Test开头
压力测试用例函数必须是Benchmark开头

3、

context

1、作用
a、在上下文中传递除了业务参数之外的额外信息,尤其是在微服务中,整个业务链的信息可以放入context中。
b、控制子goroutine的运行

2、golang并发控制
a、chanel
b、waitgroup
c、context

GC

Go就不涉及调优问题,GC就是runtime实现。Golang的GC采用的三色标记法。
三色标记法的流程如下,它将对象通过白、灰、黑进行标记:
0、所有对象最开始都是白色
1、从 root 开始找到所有可达对象,标记为灰色,放入待处理队列
2、遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,其自身标记为黑色
3、循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象

三色标记法相对于普通标记清扫,减少了 STW 时间。这主要得益于标记过程是 “on-the-fly” 的,在标记过程中是不需要 STW 的,它与程序是并发执行的,这就大大缩短了 STW 的时间。

1、触发时机

主动触发(手动触发),通过调用runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕.
被动触发 分为两种方式: a、使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。 b、使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发。

框架

goconvey

一个支持golang的单元测试框架
能够自动监控文件修改并启动测试,可以将测试结果实时输出到web界面
提供了丰富的断言简化测试用例的编写

gin

web框架
1、路由是怎么处理的
Gin框架中的路由使用的是httprouter这个库。使用了类似前缀树的数据结构-压缩版前缀树:对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值