Golang 面试题搜集
Golang 常见面试题目解析
- 交替打印数字和字母
- 判断字符串中字符是否全都不同
- 翻转字符串
- 判断两个给定的字符串排序后是否一致
- 字符串替换问题
- 机器人坐标计算
- 语法题目一
- 语法题目二
- goroutine和channel使用一
- 实现阻塞读的并发安全Map
- 高并发下的锁与map读写问题
- 定时与 panic 恢复
- 为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能.
- 七道语法找错题目
- golang 并发题目测试
- 记一道字节跳动的算法面试题
- 多协程查询切片问题
- 对已经关闭的的chan进行读写,会怎么样?为什么?
- 简单聊聊内存逃逸?
- 字符串转成byte数组,会发生内存拷贝吗?
- http包的内存泄漏
- sync.Map 的用法
- Golang基础语法相关题目
- go 面试题:连接字符串有几种方法
- Golang 基础语法题汇集
Golang 理论
- Go语言的GPM调度器是什么?
- Goroutine调度策略
- goroutine调度器概述
- Golang的垃圾回收机制是如何工作的?介绍.md)
- golang中如果出现了内存泄露, 怎么排查?一般都有哪些情况可能导致内存泄露
- 讲讲GMP模型
- Golang常见语法面试题总结
Redis基础
- Redis 基础数据结构
- Redis中的底层数据结构
- Redis持久化的原理及优化
- Redis中内存淘汰算法实现
- Redis主从复制原理
- AOF 和 RDB 是什么?AOF 持久化策略有哪三种?你们是怎么选的?AOF 什么时候重写?为什么重写?
- Redis 6 里的 Threaded I/O 是想解决什么问题?
MySQL相关
- MySQL数据库经典面试题解析
- MySQL InnoDB MVCC 机制的原理及实现
- 为什么MySQL使用B+树做索引?
- MySQL 单列索引,联合索引的存储区别?
- 了解索引下推吗?什么情况下会下推到引擎去处理?什么情况下索引会失效?
面试必备算法
排序算法
大模型基础
Elasticsearch 场景面试题及解析
架构设计
- 设计一个关注人和被关注人的场景,怎么更合理更快的查询?
- 详细解释下 Kafka 消费的顺序的原理。
- 对一个电商平台来说突然的秒杀场景,如何进行库存扣减。
- Redis缓存和MySQL数据如何做到一致性?
- 一个定时调度系统,如何进行准时的触发。
- 对商品拆分库存行后,现在 1 单要买 2 个商品,这两个库存分别在 2 个库存行怎么处理?
- 面试题:如果你是架构师,如何保证系统的可用性?
- IO 多路复用是什么?多路是什么?复用了什么?
- 面试题:怎么理解可观测性(Observability)?
- 短链接唯一 ID 设计这块挑战是什么?
- 讲一下 Trace 的数据模型,什么是 Span,Trace ID 和 Span ID 是怎么设计的?
- Metrics 的数据模型有了解吗?
- LSM-Tree 和 B+ Tree 的本质区别是什么?
q001
交替打印数字和字母
问题描述
使用两个 goroutine 交替打印序列,一个 goroutine 打印数字, 另外一个 goroutine 打印字母, 最终效果如下:
12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
解题思路
问题很简单,使用 channel 来控制打印的进度。使用两个 channel ,来分别控制数字和字母的打印序列, 数字打印完成后通过 channel 通知字母打印, 字母打印完成后通知数字打印,然后周而复始的工作。
源码参考
letter,number := make(chan bool),make(chan bool)
wait := sync.WaitGroup{}
go func() {
i := 1
for {
select {
case <-number:
fmt.Print(i)
i++
fmt.Print(i)
i++
letter <- true
}
}
}()
wait.Add(1)
go func(wait *sync.WaitGroup) {
i := 'A'
for{
select {
case <-letter:
if i >= 'Z' {
wait.Done()
return
}
fmt.Print(string(i))
i++
fmt.Print(string(i))
i++
number <- true
}
}
}(&wait)
number<-true
wait.Wait()
源码解析
这里用到了两个channel负责通知,letter负责通知打印字母的goroutine来打印字母,number用来通知打印数字的goroutine打印数字。wait用来等待字母打印完成后退出循环。
也可以分别使用三个 channel 来控制数字,字母以及终止信号的输入.
package main
import "fmt"
func main() {
number := make(chan bool)
letter := make(chan bool)
done := make(chan bool)
go func() {
i := 1
for {
select {
case <-number:
fmt.Print(i)
i++
fmt.Print(i)
i++
letter <- true
}
}
}()
go func() {
j := 'A'
for {
select {
case <-letter:
if j >= 'Z' {
done <- true
} else {
fmt.Print(string(j))
j++
fmt.Print(string(j))
j++
number <- true
}
}
}
}()
number <- true
for {
select {
case <-done:
return
}
}
}
q002
判断字符串中字符是否全都不同
问题描述
请实现一个算法,确定一个字符串的所有字符【是否全都不同】。这里我们要求【不允许使用额外的存储结构】。 给定一个string,请返回一个bool值,true代表所有字符全都不同,false代表存在相同的字符。 保证字符串中的字符为【ASCII字符】。字符串的长度小于等于【3000】。
解题思路
这里有几个重点,第一个是ASCII字符,ASCII字符字符一共有256个,其中128个是常用字符,可以在键盘上输入。128之后的是键盘上无法找到的。
然后是全部不同,也就是字符串中的字符没有重复的,再次,不准使用额外的储存结构,且字符串小于等于3000。
如果允许其他额外储存结构,这个题目很好做。如果不允许的话,可以使用golang内置的方式实现。
源码参考
通过strings.Count 函数判断:
func isUniqueString(s string) bool {
if strings.Count(s,"") > 3000{
return false
}
for _,v := range s {
if v > 127 {
return false
}
if strings.Count(s,string(v)) > 1 {
return false
}
}
return true
}
通过strings.Index和strings.LastIndex函数判断:
func isUniqueString2(s string) bool {
if strings.Count(s,"") > 3000{
return false
}
for k,v := range s {
if v > 127 {
return false
}
if strings.Index(s,string(v)) != k {
return false
}
}
return true
}
通过位运算判断
func isUniqString3(s string) bool {
if len(s) == 0 || len(s) > 3000 {
return false
}
// 256 个字符 256 = 64 + 64 + 64 + 64
var mark1, mark2, mark3, mark4 uint64
var mark *uint64
for _, r := range s {
n := uint64(r)
if n < 64 {
mark = &mark1
} else if n < 128 {
mark = &mark2
n -= 64
} else if n < 192 {
mark = &mark3
n -= 128
} else {
mark = &mark4
n -= 192
}
if (*mark)&(1<<n) != 0 {
return false
}
*mark = (*mark) | uint64(1<<n)
}
return true
}
源码解析
以上三种方法都可以实现这个算法。
第一个方法使用的是golang内置方法strings.Count,可以用来判断在一个字符串中包含的另外一个字符串的数量。
第二个方法使用的是golang内置方法strings.Index和strings.LastIndex,用来判断指定字符串在另外一个字符串的索引未知,分别是第一次发现位置和最后发现位置。
第三个方法使用的是位运算来判断是否重复,时间复杂度为o(n),相比前两个方法时间复杂度低
q003
翻转字符串
问题描述
请实现一个算法,在不使用【额外数据结构和储存空间】的情况下,翻转一个给定的字符串(可以使用单个过程变量)。
给定一个string,请返回一个string,为翻转后的字符串。保证字符串的长度小于等于5000。
解题思路
翻转字符串其实是将一个字符串以中间字符为轴,前后翻转,即将str[len]赋值给str[0],将str[0] 赋值 str[len]。
源码参考
func reverString(s string) (string, bool) {
str := []rune(s)
l := len(str)
if l > 5000 {
return s, false
}
for i := 0; i < l/2; i++ {
str[i], str[l-1-i] = str[l-1-i], str[i]
}
return string(str), true
}
源码解析
以字符串长度的1/2为轴,前后赋值
q004
判断两个给定的字符串排序后是否一致
问题描述
给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。 这里规定【大小写为不同字符】,且考虑字符串重点空格。给定一个string s1和一个string s2,请返回一个bool,代表两串是否重新排列后可相同。 保证两串的长度都小于等于5000。
解题思路
首先要保证字符串长度小于5000。之后只需要一次循环遍历s1中的字符在s2是否都存在即可。
源码参考
func isRegroup(s1,s2 string) bool {
sl1 := len([]rune(s1))
sl2 := len([]rune(s2))
if sl1 > 5000 || sl2 > 5000 || sl1 != sl2{
return false
}
for _,v := range s1 {
if strings.Count(s1,string(v)) != strings.Count(s2,string(v)) {
return false
}
}
return true
}
源码解析
这里还是使用golang内置方法 strings.Count 来判断字符是否一致。
q005
字符串替换问题
问题描述
请编写一个方法,将字符串中的空格全部替换为“%20”。 假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度(小于等于1000),同时保证字符串由【大小写的英文字母组成】。 给定一个string为原始的串,返回替换后的string。
解题思路
两个问题,第一个是只能是英文字母,第二个是替换空格。
源码参考
func replaceBlank(s string) (string, bool) {
if len([]rune(s)) > 1000 {
return s, false
}
for _, v := range s {
if string(v) != " " && unicode.IsLetter(v) == false {
return s, false
}
}
return strings.Replace(s, " ", "%20", -1), true
}
源码解析
这里使用了golang内置方法unicode.IsLetter判断字符是否是字母,之后使用strings.Replace来替换空格。
q006
机器人坐标问题
问题描述
有一个机器人,给一串指令,L左转 R右转,F前进一步,B后退一步,问最后机器人的坐标,最开始,机器人位于 0 0,方向为正Y。 可以输入重复指令n : 比如 R2(LF) 这个等于指令 RLFLF。 问最后机器人的坐标是多少?
解题思路
这里的一个难点是解析重复指令。主要指令解析成功,计算坐标就简单了。
源码参考
package main
import (
"unicode"
)
const (
Left = iota
Top
Right
Bottom
)
func main() {
println(move("R2(LF)", 0, 0, Top))
}
func move(cmd string, x0 int, y0 int, z0 int) (x, y, z int) {
x, y, z = x0, y0, z0
repeat := 0
repeatCmd := ""
for _, s := range cmd {
switch {
case unicode.IsNumber(s):
repeat = repeat*10 + (int(s) - '0')
case s == ')':
for i := 0; i < repeat; i++ {
x, y, z = move(repeatCmd, x, y, z)
}
repeat = 0
repeatCmd = ""
case repeat > 0 && s != '(' && s != ')':
repeatCmd = repeatCmd + string(s)
case s == 'L':
z = (z + 1) % 4
case s == 'R':
z = (z - 1 + 4) % 4
case s == 'F':
switch {
case z == Left || z == Right:
x = x - z + 1
case z == Top || z == Bottom:
y = y - z + 2
}
case s == 'B':
switch {
case z == Left || z == Right:
x = x + z - 1
case z == Top || z == Bottom:
y = y + z - 2
}
}
}
return
}
源码解析
这里使用三个值表示机器人当前的状况,分别是:x表示x坐标,y表示y坐标,z表示当前方向。 L、R 命令会改变值z,F、B命令会改变值x、y。 值x、y的改变还受当前的z值影响。
如果是重复指令,那么将重复次数和重复的指令存起来递归调用即可。
q007
常见语法题目 一
1、下面代码能运行吗?为什么。
type Param map[string]interface{}
type Show struct {
Param
}
func main1() {
s := new(Show)
s.Param["RMB"] = 10000
}
解析
共发现两个问题:
main函数不能加数字。new关键字无法初始化Show结构体中的Param属性,所以直接对s.Param操作会出错。
2、请说出下面代码存在什么问题。
type student struct {
Name string
}
func zhoujielun(v interface{}) {
switch msg := v.(type) {
case *student, student:
msg.Name
}
}
解析:
golang中有规定,switch type的case T1,类型列表只有一个,那么v := m.(type)中的v的类型就是T1类型。
如果是case T1, T2,类型列表中有多个,那v的类型还是多对应接口的类型,也就是m的类型。
所以这里msg的类型还是interface{},所以他没有Name这个字段,编译阶段就会报错。具体解释见:
3、写出打印的结果。
type People struct {
name string `json:"name"`
}
func main() {
js := `{
"name":"11"
}`
var p People
err := json.Unmarshal([]byte(js), &p)
if err != nil {
fmt.Println("err: ", err)
return
}
fmt.Println("people: ", p)
}
解析:
按照 golang 的语法,小写开头的方法、属性或 struct 是私有的,同样,在json 解码或转码的时候也无法上线私有属性的转换。
题目中是无法正常得到People的name值的。而且,私有属性name也不应该加json的标签。
4、下面的代码是有问题的,请说明原因。
type People struct {
Name string
}
func (p *People) String() string {
return fmt.Sprintf("print: %v", p)
}
func main() {
p := &People{}
p.String()
}
解析:
在golang中String() string 方法实际上是实现了String的接口的,该接口定义在fmt/print.go 中:
type Stringer interface {
String() string
}
在使用 fmt 包中的打印方法时,如果类型实现了这个接口,会直接调用。而题目中打印 p 的时候会直接调用 p 实现的 String() 方法,然后就产生了循环调用。
5、请找出下面代码的问题所在。
func main() {
ch := make(chan int, 1000)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
go func() {
for {
a, ok := <-ch
if !ok {
fmt.Println("close")
return
}
fmt.Println("a: ", a)
}
}()
close(ch)
fmt.Println("ok")
time.Sleep(time.Second * 100)
}
解析:
在 golang 中 goroutine 的调度时间是不确定的,在题目中,第一个写 channel 的 goroutine 可能还未调用,或已调用但没有写完时直接 close 管道,可能导致写失败,既然出现 panic 错误。
6、请说明下面代码书写是否正确。
var value int32
func SetValue(delta int32) {
for {
v := value
if atomic.CompareAndSwapInt32(&value, v, (v+delta)) {
break
}
}
}
解析:
atomic.CompareAndSwapInt32 函数不需要循环调用。
7、下面的程序运行后为什么会爆异常。
type Project struct{}
func (p *Project) deferError() {
if err := recover(); err != nil {
fmt.Println("recover: ", err)
}
}
func (p *Project) exec(msgchan chan interface{}) {
for msg := range msgchan {
m := msg.(int)
fmt.Println("msg: ", m)
}
}
func (p *Project) run(msgchan chan interface{}) {
for {
defer p.deferError()
go p.exec(msgchan)
time.Sleep(time.Second * 2)
}
}
func (p *Project) Main() {
a := make(chan interface{}, 100)
go p.run(a)
go func() {
for {
a <- "1"
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second * 100000000000000)
}
func main() {
p := new(Project)
p.Main()
}
解析:
有一下几个问题:
time.Sleep的参数数值太大,超过了1<<63 - 1的限制。defer p.deferError()需要在协程开始出调用,否则无法捕获panic。
8、请说出下面代码哪里写错了
func main() {
abc := make(chan int, 1000)
for i := 0; i < 10; i++ {
abc <- i
}
go func() {
for a := range abc {
fmt.Println("a: ", a)
}
}()
close(abc)
fmt.Println("close")
time.Sleep(time.Second * 100)
}
解析:
协程可能还未启动,管道就关闭了。
9、请说出下面代码,执行时为什么会报错
type Student struct {
name string
}
func main() {
m := map[string]Student{"people": {"zhoujielun"}}
m["people"].name = "wuyanzu"
}
解析:
map的value本身是不可寻址的,因为map中的值会在内存中移动,并且旧的指针地址在map改变时会变得无效。故如果需要修改map值,可以将map中的非指针类型value,修改为指针类型,比如使用map[string]*Student.
10、请说出下面的代码存在什么问题?
type query func(string) string
func exec(name string, vs ...query) string {
ch := make(chan string)
fn := func(i int) {
ch <- vs[i](https://raw.githubusercontent.com/lifei6671/interview-go/HEAD/name)
}
for i, _ := range vs {
go fn(i)
}
return <-ch
}
func main() {
ret := exec("111", func(n string) string {
return n + "func1"
}, func(n string) string {
return n + "func2"
}, func(n string) string {
return n + "func3"
}, func(n string) string {
return n + "func4"
})
fmt.Println(ret)
}
解析:
依据4个goroutine的启动后执行效率,很可能打印111func4,但其他的111func*也可能先执行,exec只会返回一条信息。
11、下面这段代码为什么会卡死?
package main
import (
"fmt"
"runtime"
)
func main() {
var i byte
go func() {
for i = 0; i <= 255; i++ {
}
}()
fmt.Println("Dropping mic")
// Yield execution to force executing other goroutines
runtime.Gosched()
runtime.GC()
fmt.Println("Done")
}
解析:
Golang 中,byte 其实被 alias 到 uint8 上了。所以上面的 for 循环会始终成立,因为 i++ 到 i=255 的时候会溢出,i <= 255 一定成立。
也即是, for 循环永远无法退出,所以上面的代码其实可以等价于这样:
go func() {
for {}
}
正在被执行的 goroutine 发生以下情况时让出当前 goroutine 的执行权,并调度后面的 goroutine 执行:
- IO 操作
- Channel 阻塞
- system call
- 运行较长时间
如果一个 goroutine 执行时间太长,scheduler 会在其 G 对象上打上一个标志( preempt),当这个 goroutine 内部发生函数调用的时候,会先主动检查这个标志,如果为 true 则会让出执行权。
main 函数里启动的 goroutine 其实是一个没有 IO 阻塞、没有 Channel 阻塞、没有 system call、没有函数调用的死循环。
也就是,它无法主动让出自己的执行权,即使已经执行很长时间,scheduler 已经标志了 preempt。
而 golang 的 GC 动作是需要所有正在运行 goroutine 都停止后进行的。因此,程序会卡在 runtime.GC() 等待所有协程退出。
q008
常见语法题目 二
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 关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。
在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。
因此,题目最后输出顺序应该是defer 定义顺序的倒序。panic 错误并不能终止 defer 的执行。
2、 以下代码有什么问题,说明原因
type student struct {
Name string
Age int
}
func pase_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
}
}
解析:
golang 的 for ... range 语法中,stu 变量会被复用,每次循环会将集合中的值复制给这个变量,因此,会导致最后m中的map中储存的都是stus最后一个student的值。
3、下面的代码会输出什么,并说明原因
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("i: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("i: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
解析:
这个输出结果决定来自于调度器优先调度哪个G。从runtime的源码可以看到,当创建一个G时,会优先放入到下一个调度的runnext字段上作为下一次优先调度的G。因此,最先输出的是最后创建的G,也就是9.
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, argp, siz, gp, pc)
_p_ := getg().m.p.ptr()
//新创建的G会调用这个方法来决定如何调度
runqput(_p_, newg, true)
if mainStarted {
wakep()
}
})
}
...
if next {
retryNext:
oldnext := _p_.runnext
//当next是true时总会将新进来的G放入下一次调度字段中
if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
goto retryNext
}
if oldnext == 0 {
return
}
// Kick the old runnext out to the regular run queue.
gp = oldnext.ptr()
}
4、下面代码会输出什么?
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()
}
解析:
输出结果为showA、showB。golang 语言中没有继承概念,只有组合,也没有虚方法,更没有重载。因此,*Teacher 的 ShowB 不会覆写被组合的 People 的方法。
5、下面代码会触发异常吗?请详细说明
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)
}
}
解析:
结果是随机执行。golang 在多个case 可读的时候会公平的选中一个执行。
6、下面代码输出什么?
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
}
解析:
输出结果为:
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
defer 在定义的时候会计算好调用函数的参数,所以会优先输出10、20 两个参数。然后根据定义的顺序倒序执行。
7、请写出以下输入内容
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
解析:
输出为 0 0 0 0 0 1 2 3。
make 在初始化切片时指定了长度,所以追加数据时会从len(s) 位置开始填充数据。
8、下面的代码有什么问题?
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
}
解析:
在执行 Get方法时可能被thorw。
虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。
因此,在 Get 中也需要加锁,因为这里只是读,建议使用读写锁 sync.RWMutex。
9、下面的迭代会有什么问题?
func (set *threadSafeSet) Iter() <-chan interface{} {
ch := make(chan interface{})
go func() {
set.RLock()
for elem := range set.s {
ch <- elem
}
close(ch)
set.RUnlock()
}()
return ch
}
解析:
默认情况下 make 初始化的 channel 是无缓冲的,也就是在迭代写时会阻塞。
10、以下代码能编译过去吗?为什么?
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
解析:
编译失败,值类型 Student{} 未实现接口People的方法,不能定义为 People类型。
在 golang 语言中,Student 和 *Student 是两种类型,第一个是表示 Student 本身,第二个是指向 Student 的指针。
11、以下代码打印出来什么内容,说出为什么。。。
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")
}
}
解析:
跟上一题一样,不同的是*Student 的定义后本身没有初始化值,所以 *Student 是 nil的,但是*Student 实现了 People 接口,接口不为 nil。
q009
在 golang 协程和channel配合使用
写代码实现两个 goroutine,其中一个产生随机数并写入到 go channel 中,另外一个从 channel 中读取数字并打印到标准输出。最终输出五个随机数。
解析
这是一道很简单的golang基础题目,实现方法也有很多种,一般想答让面试官满意的答案还是有几点注意的地方。
goroutine在golang中式非阻塞的channel无缓冲情况下,读写都是阻塞的,且可以用for循环来读取数据,当管道关闭后,for退出。- golang 中有专用的
select case语法从管道读取数据。
示例代码如下:
func main() {
out := make(chan int)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
out <- rand.Intn(5)
}
close(out)
}()
go func() {
defer wg.Done()
for i := range out {
fmt.Println(i)
}
}()
wg.Wait()
}
如果不想使用 sync.WaitGroup, 也可以用一个 done channel.
package main
import (
"fmt"
"math/rand"
)
func main() {
random := make(chan int)
done := make(chan bool)
go func() {
for {
num, ok := <-random
if ok {
fmt.Println(num)
} else {
done <- true
}
}
}()
go func() {
defer close(random)
for i := 0; i < 5; i++ {
random <- rand.Intn(5)
}
}()
<-done
close(done)
}
q010
实现阻塞读且并发安全的map
GO里面MAP如何实现key不存在 get操作等待 直到key存在或者超时,保证并发安全,且需要实现以下接口:
type sp interface {
Out(key string, val interface{}) //存入key /val,如果该key读取的goroutine挂起,则唤醒。此方法不会阻塞,时刻都可以立即执行并返回
Rd(key string, timeout time.Duration) interface{} //读取一个key,如果key不存在阻塞,等待key存在或者超时
}
解析:
看到阻塞协程第一个想到的就是channel,题目中要求并发安全,那么必须用锁,还要实现多个goroutine读的时候如果值不存在则阻塞,直到写入值,那么每个键值需要有一个阻塞goroutine 的 channel。
type Map struct {
c map[string]*entry
rmx *sync.RWMutex
}
type entry struct {
ch chan struct{}
value interface{}
isExist bool
}
func (m *Map) Out(key string, val interface{}) {
m.rmx.Lock()
defer m.rmx.Unlock()
item, ok := m.c[key]
if !ok {
m.c[key] = &entry{
value: val,
isExist: true,
}
return
}
item.value = val
if !item.isExist {
if item.ch != nil {
close(item.ch)
item.ch = nil
}
}
return
}
q011
高并发下的锁与map的读写
场景:在一个高并发的web服务器中,要限制IP的频繁访问。现模拟100个IP同时并发访问服务器,每个IP要重复访问1000次。
每个IP三分钟之内只能访问一次。修改以下代码完成该过程,要求能成功输出 success:100
package main
import (
"fmt"
"time"
)
type Ban struct {
visitIPs map[string]time.Time
}
func NewBan() *Ban {
return &Ban{visitIPs: make(map[string]time.Time)}
}
func (o *Ban) visit(ip string) bool {
if _, ok := o.visitIPs[ip]; ok {
return true
}
o.visitIPs[ip] = time.Now()
return false
}
func main() {
success := 0
ban := NewBan()
for i := 0; i < 1000; i++ {
for j := 0; j < 100; j++ {
go func() {
ip := fmt.Sprintf("192.168.1.%d", j)
if !ban.visit(ip) {
success++
}
}()
}
}
fmt.Println("success:", success)
}
解析
该问题主要考察了并发情况下map的读写问题,而给出的初始代码,又存在for循环中启动goroutine时变量使用问题以及goroutine执行滞后问题。
因此,首先要保证启动的goroutine得到的参数是正确的,然后保证map的并发读写,最后保证三分钟只能访问一次。
多CPU核心下修改int的值极端情况下会存在不同步情况,因此需要原子性的修改int值。
下面给出的实例代码,是启动了一个协程每分钟检查一下map中的过期ip,for启动协程时传参。
package main
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
)
type Ban struct {
visitIPs map[string]time.Time
lock sync.Mutex
}
func NewBan(ctx context.Context) *Ban {
o := &Ban{visitIPs: make(map[string]time.Time)}
go func() {
timer := time.NewTimer(time.Minute * 1)
for {
select {
case <-timer.C:
o.lock.Lock()
for k, v := range o.visitIPs {
if time.Now().Sub(v) >= time.Minute*1 {
delete(o.visitIPs, k)
}
}
o.lock.Unlock()
timer.Reset(time.Minute * 1)
case <-ctx.Done():
return
}
}
}()
return o
}
func (o *Ban) visit(ip string) bool {
o.lock.Lock()
defer o.lock.Unlock()
if _, ok := o.visitIPs[ip]; ok {
return true
}
o.visitIPs[ip] = time.Now()
return false
}
func main() {
success := int64(0)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ban := NewBan(ctx)
wait := &sync.WaitGroup{}
wait.Add(1000 * 100)
for i := 0; i < 1000; i++ {
for j := 0; j < 100; j++ {
go func(j int) {
defer wait.Done()
ip := fmt.Sprintf("192.168.1.%d", j)
if !ban.visit(ip) {
atomic.AddInt64(&success, 1)
}
}(j)
}
}
wait.Wait()
fmt.Println("success:", success)
}
q012
1. 写出以下逻辑,要求每秒钟调用一次proc并保证程序不退出?
package main
func main() {
go func() {
// 1 在这里需要你写算法
// 2 要求每秒钟调用一次proc函数
// 3 要求程序不能退出
}()
select {}
}
func proc() {
panic("ok")
}
解析
题目主要考察了两个知识点:
- 定时执行执行任务
- 捕获 panic 错误
题目中要求每秒钟执行一次,首先想到的就是 time.Ticker对象,该函数可每秒钟往chan中放一个Time,正好符合我们的要求。
在 golang 中捕获 panic 一般会用到 recover() 函数。
package main
import (
"fmt"
"time"
)
func main() {
go func() {
// 1 在这里需要你写算法
// 2 要求每秒钟调用一次proc函数
// 3 要求程序不能退出
t := time.NewTicker(time.Second * 1)
for {
select {
case <-t.C:
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
proc()
}()
}
}
}()
select {}
}
func proc() {
panic("ok")
}
q013
为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能.`
`
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
c := make(chan struct{})
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int, close <-chan struct{}) {
defer wg.Done()
<-close
fmt.Println(num)
}(i, c)
}
if WaitTimeout(&wg, time.Second*5) {
close(c)
fmt.Println("timeout exit")
}
time.Sleep(time.Second * 10)
}
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
// 要求手写代码
// 要求sync.WaitGroup支持timeout功能
// 如果timeout到了超时时间返回true
// 如果WaitGroup自然结束返回false
}
解析
首先 sync.WaitGroup 对象的 Wait 函数本身是阻塞的,同时,超时用到的time.Timer 对象也需要阻塞的读。
同时阻塞的两个对象肯定要每个启动一个协程,每个协程去处理一个阻塞,难点在于怎么知道哪个阻塞先完成。
目前我用的方式是声明一个没有缓冲的chan,谁先完成谁优先向管道中写入数据。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
c := make(chan struct{})
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int, close <-chan struct{}) {
defer wg.Done()
<-close
fmt.Println(num)
}(i, c)
}
if WaitTimeout(&wg, time.Second*5) {
close(c)
fmt.Println("timeout exit")
}
time.Sleep(time.Second * 10)
}
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
// 要求手写代码
// 要求sync.WaitGroup支持timeout功能
// 如果timeout到了超时时间返回true
// 如果WaitGroup自然结束返回false
ch := make(chan bool, 1)
go time.AfterFunc(timeout, func() {
ch <- true
})
go func() {
wg.Wait()
ch <- false
}()
return <- ch
}
q014
语法找错题
写出以下代码出现的问题
package main
import (
"fmt"
)
func main() {
var x string = nil
if x == nil {
x = "default"
}
fmt.Println(x)
}
golang 中字符串是不能赋值 nil 的,也不能跟 nil 比较。
写出以下打印内容
package main
import "fmt"
const (
a = iota
b = iota
)
const (
name = "menglu"
c = iota
d = iota
)
func main() {
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
找出下面代码的问题
package main
import "fmt"
type query func(string) string
func exec(name string, vs ...query) string {
ch := make(chan string)
fn := func(i int) {
ch <- vs[i](https://raw.githubusercontent.com/lifei6671/interview-go/HEAD/name)
}
for i, _ := range vs {
go fn(i)
}
return <-ch
}
func main() {
ret := exec("111", func(n string) string {
return n + "func1"
}, func(n string) string {
return n + "func2"
}, func(n string) string {
return n + "func3"
}, func(n string) string {
return n + "func4"
})
fmt.Println(ret)
}
上面的代码有严重的内存泄漏问题,出错的位置是 go fn(i),实际上代码执行后会启动 4 个协程,但是因为 ch 是非缓冲的,只可能有一个协程写入成功。而其他三个协程会一直在后台等待写入。
写出以下打印结果,并解释下为什么这么打印的。
package main
import (
"fmt"
)
func main() {
str1 := []string{"a", "b", "c"}
str2 := str1[1:]
str2[1] = "new"
fmt.Println(str1)
str2 = append(str2, "z", "x", "y")
fmt.Println(str1)
}
golang 中的切片底层其实使用的是数组。当使用str1[1:] 使,str2 和 str1 底层共享一个数组,这回导致 str2[1] = "new" 语句影响 str1。
而 append 会导致底层数组扩容,生成新的数组,因此追加数据后的 str2 不会影响 str1。
但是为什么对 str2 复制后影响的确实 str1 的第三个元素呢?这是因为切片 str2 是从数组的第二个元素开始,str2 索引为 1 的元素对应的是 str1 索引为 2 的元素。
写出以下打印结果
package main
import (
"fmt"
)
type Student struct {
Name string
}
func main() {
fmt.Println(&Student{Name: "menglu"} == &Student{Name: "menglu"})
fmt.Println(Student{Name: "menglu"} == Student{Name: "menglu"})
}
个人理解:指针类型比较的是指针地址,非指针类型比较的是每个属性的值。
写出以下代码的问题
package main
import (
"fmt"
)
func main() {
fmt.Println([...]string{"1"} == [...]string{"1"})
fmt.Println([]string{"1"} == []string{"1"})
}
数组只能与相同纬度长度以及类型的其他数组比较,切片之间不能直接比较。。
下面代码写法有什么问题?
package main
import (
"fmt"
)
type Student struct {
Age int
}
func main() {
kv := map[string]Student{"menglu": {Age: 21}}
kv["menglu"].Age = 22
s := []Student{{Age: 21}}
s[0].Age = 22
fmt.Println(kv, s)
}
golang中的map 通过key获取到的实际上是两个值,第一个是获取到的值,第二个是是否存在该key。因此不能直接通过key来赋值对象。
q015
golang 并发题目测试
题目来源: Go并发编程小测验: 你能答对几道题?
1 Mutex
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var chain string
func main() {
chain = "main"
A()
fmt.Println(chain)
}
func A() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> A"
B()
}
func B() {
chain = chain + " --> B"
C()
}
func C() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> C"
}
- A: 不能编译
- B: 输出 main --> A --> B --> C
- C: 输出 main
- D: panic
2 RWMutex
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.RWMutex
var count int
func main() {
go A()
time.Sleep(2 * time.Second)
mu.Lock()
defer mu.Unlock()
count++
fmt.Println(count)
}
func A() {
mu.RLock()
defer mu.RUnlock()
B()
}
func B() {
time.Sleep(5 * time.Second)
C()
}
func C() {
mu.RLock()
defer mu.RUnlock()
}
- A: 不能编译
- B: 输出 1
- C: 程序hang住
- D: panic
3 Waitgroup
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(time.Millisecond)
wg.Done()
wg.Add(1)
}()
wg.Wait()
}
- A: 不能编译
- B: 无输出,正常退出
- C: 程序hang住
- D: panic
4 双检查实现单例
package doublecheck
import (
"sync"
)
type Once struct {
m sync.Mutex
done uint32
}
func (o *Once) Do(f func()) {
if o.done == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
o.done = 1
f()
}
}
- A: 不能编译
- B: 可以编译,正确实现了单例
- C: 可以编译,有并发问题,f函数可能会被执行多次
- D: 可以编译,但是程序运行会panic
5 Mutex
package main
import (
"fmt"
"sync"
)
type MyMutex struct {
count int
sync.Mutex
}
func main() {
var mu MyMutex
mu.Lock()
var mu2 = mu
mu.count++
mu.Unlock()
mu2.Lock()
mu2.count++
mu2.Unlock()
fmt.Println(mu.count, mu2.count)
}
- A: 不能编译
- B: 输出 1, 1
- C: 输出 1, 2
- D: panic
6 Pool
package main
import (
"bytes"
"fmt"
"runtime"
"sync"
"time"
)
var pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
func main() {
go func() {
for {
processRequest(1 << 28) // 256MiB
}
}()
for i := 0; i < 1000; i++ {
go func() {
for {
processRequest(1 << 10) // 1KiB
}
}()
}
var stats runtime.MemStats
for i := 0; ; i++ {
runtime.ReadMemStats(&stats)
fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc)
time.Sleep(time.Second)
runtime.GC()
}
}
func processRequest(size int) {
b := pool.Get().(*bytes.Buffer)
time.Sleep(500 * time.Millisecond)
b.Grow(size)
pool.Put(b)
time.Sleep(1 * time.Millisecond)
}
- A: 不能编译
- B: 可以编译,运行时正常,内存稳定
- C: 可以编译,运行时内存可能暴涨
- D: 可以编译,运行时内存先暴涨,但是过一会会回收掉
7 channel
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ch chan int
go func() {
ch = make(chan int, 1)
ch <- 1
}()
go func(ch chan int) {
time.Sleep(time.Second)
<-ch
}(ch)
c := time.Tick(1 * time.Second)
for range c {
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
}
}
- A: 不能编译
- B: 一段时间后总是输出
#goroutines: 1 - C: 一段时间后总是输出
#goroutines: 2 - D: panic
8 channel
package main
import "fmt"
func main() {
var ch chan int
var count int
go func() {
ch <- 1
}()
go func() {
count++
close(ch)
}()
<-ch
fmt.Println(count)
}
- A: 不能编译
- B: 输出 1
- C: 输出 0
- D: panic
9 Map
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.LoadOrStore("a", 1)
m.Delete("a")
fmt.Println(m.Len())
}
- A: 不能编译
- B: 输出 1
- C: 输出 0
- D: panic
10 happens before
package main
var c = make(chan int)
var a int
func f() {
a = 1
<-c
}
func main() {
go f()
c <- 0
print(a)
}
- A: 不能编译
- B: 输出 1
- C: 输出 0
- D: panic
答案
1. D
会产生死锁panic,因为Mutex 是互斥锁。
2. D
会产生死锁panic,根据sync/rwmutex.go 中注释可以知道,读写锁当有一个协程在等待写锁时,其他协程是不能获得读锁的,而在A和C中同一个调用链中间需要让出读锁,让写锁优先获取,而A的读锁又要求C调用完成,因此死锁。
3. D
WaitGroup 在调用 Wait 之后是不能再调用 Add 方法的。
4. C
在多核CPU中,因为CPU缓存会导致多个核心中变量值不同步。
5. D
加锁后复制变量,会将锁的状态也复制,所以mu1 其实是已经加锁状态,再加锁会死锁。
6. C
个人理解,在单核CPU中,内存可能会稳定在256MB,如果是多核可能会暴涨。
7. C
因为 ch 未初始化,写和读都会阻塞,之后被第一个协程重新赋值,导致写的ch 都阻塞。
8. D
ch 未有被初始化,关闭时会报错。
9. A
sync.Map 没有 Len 方法。
10. B
c <- 0 会阻塞依赖于 f() 的执行。
q016
记一道字节跳动的算法面试题
题目
这其实是一道变形的链表反转题,大致描述如下 给定一个单链表的头节点 head,实现一个调整单链表的函数,使得每K个节点之间为一组进行逆序,并且从链表的尾部开始组起,头部剩余节点数量不够一组的不需要逆序。(不能使用队列或者栈作为辅助)
例如:
链表:1->2->3->4->5->6->7->8->null, K = 3。那么 6->7->8,3->4->5,1->2各位一组。调整后:1->2->5->4->3->8->7->6->null。其中 1,2不调整,因为不够一组。
解析
原文: