go 面试题分享

1 判断字符串中字符是否全都不同
问题描述
    请实现一个算法,确定一个字符串的所有字符【是否全都不同】。这里我们要
求【不允许使用额外的存储结构】。给定一个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
}
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
}




源码解析
以上两种方法都可以实现这个算法。
第一个方法使用的是golang内置方法strings.Count,可以用来判断在一个字符
串中包含的另外一个字符串的数量。
第二个方法使用的是golang内置方法strings.Index和strings.LastIndex,用来判
断指定字符串在另外一个字符串的索引未知,分别是第一次发现位置和最后发
现位置。

2 翻转字符串
问题描述
    请实现一个算法,在不使用【额外数据结构和储存空间】的情况下,翻转一个
给定的字符串(可以使用单个过程变量)。
给定一个string,请返回一个string,为翻转后的字符串。保证字符串的长度
小于等于5000。
解题思路
翻转字符串其实是将一个字符串以中间字符为轴,前后翻转,即将str[len]赋
值给str[0],将str[0]赋值str[len]。
源码参考

func reverseString(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为轴,前后赋值

3 判断两个给定的字符串排序后是否一致
问题描述
     给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否
变成另一个字符串。这里规定【大小写为不同字符】,且考虑字符串重点空
格。给定一个strings1和一个strings2,请返回一个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来判断字符是否一致。

4 字符串替换问题
问题描述
请编写一个方法,将字符串中的空格全部替换为“%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.ReplaceAll(s, "", "##"), true
    res := strings.Replace(s, " ", "%20", -1)
    return res, true
}


源码解析
这里使用了golang内置方法unicode.IsLetter判断字符是否是字母,之后使用
strings.Replace来替换空格。

5 机器人坐标问题
问题描述
有一个机器人,给一串指令,L左转R右转,F前进一步,B后退一步,问最
后机器人的坐标,最开始,机器人位于00,方向为正Y。可以输入重复指令
n:比如R2(LF)这个等于指令RLFLF。问最后机器人的坐标是多少?
解题思路
这里的一个难点是解析重复指令。主要指令解析成功,计算坐标就简单了。
源码参考
packagemain
import(
"unicode"
)
const(
Left=iota
Top
Right
Bottom
)
funcmain(){
println(move("R2(LF)",0,0,Top))
}
funcmove(cmdstring,x0int,y0int,z0int)(x,y,zint){
x,y,z=x0,y0,z0
repeat:=0
repeatCmd:=""
for_,s:=rangecmd{
switch{
caseunicode.IsNumber(s):
repeat=repeat*10+(int(s)-'0')
cases==')':
fori:=0;i<repeat;i++{
x,y,z=move(repeatCmd,x,y,z)
}
repeat=0
repeatCmd=""
caserepeat>0&&s!='('&&s!=')':
repeatCmd=repeatCmd+string(s)
cases=='L':
z=(z+1)%4
cases=='R':
z=(z-1+4)%4
cases=='F':
switch{
casez==Left||z==Right:
x=x-z+1
casez==Top||z==Bottom:
y=y-z+2
}
cases=='B':
switch{
casez==Left||z==Right:
x=x+z-1
casez==Top||z==Bottom:
y=y+z-2
}
}
}
return
}
源码解析
这里使用三个值表示机器人当前的状况,分别是:x表示x坐标,y表示y坐
标,z表示当前方向。L、R命令会改变值z,F、B命令会改变值x、y。值
x、y的改变还受当前的z值影响。
如果是重复指令,那么将重复次数和重复的指令存起来递归调用即可。

常见语法题目一
1、下面代码能运行吗?为什么。
typeParammap[string]interface{}
typeShowstruct{
Param
}
funcmain1(){
s:=new(Show)
s.Param["RMB"]=10000
}
解析
共发现两个问题:
1.main函数不能加数字。
2.new关键字无法初始化Show结构体中的Param属性,所以直接
对s.Param操作会出错。

2、请说出下面代码存在什么问题。
typestudentstruct{
Namestring
}
funczhoujielun(vinterface{}){
switchmsg:=v.(type){
case*student,student:
msg.Name
}
}
解析:
golang中有规定,switchtype的caseT1,类型列表只有一个,那么v:=m.(type)
中的v的类型就是T1类型。
如果是caseT1,T2,类型列表中有多个,那v的类型还是多对应接口的类型,也
就是m的类型。
所以这里msg的类型还是interface{},所以他没有Name这个字段,编译阶段就
会报错。具体解释见:https://golang.org/ref/spec#Type_switches

3、写出打印的结果。
typePeoplestruct{
namestring`json:"name"`
}
funcmain(){
js:=`{
"name":"11"
}`
varpPeople
err:=json.Unmarshal([]byte(js),&p)
iferr!=nil{
fmt.Println("err:",err)
return
}
fmt.Println("people:",p)
}
解析:
按照golang的语法,小写开头的方法、属性或struct是私有的,同样,在
json解码或转码的时候也无法上线私有属性的转换。
题目中是无法正常得到People的name值的。而且,私有属性name也不应该加
json的标签

4、下面的代码是有问题的,请说明原因。
typePeoplestruct{
Namestring
}
func(p*People)String()string{
returnfmt.Sprintf("print:%v",p)
}
funcmain(){
p:=&People{}
p.String()
}
解析:
在golang中String()string方法实际上是实现了String的接口的,该接口定义
在fmt/print.go中:
typeStringerinterface{
String()string
}
在使用fmt包中的打印方法时,如果类型实现了这个接口,会直接调用。而题
目中打印p的时候会直接调用p实现的String()方法,然后就产生了循环调
用。

5、请找出下面代码的问题所在。
funcmain(){
ch:=make(chanint,1000)
gofunc(){
fori:=0;i<10;i++{
ch<-i
}
}()
gofunc(){
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(deltaint32){
for{
v:=value
if atomic.CompareAndSwapInt32(&value,v,(v+delta)){
break
}
}
}
解析:
atomic.CompareAndSwapInt32函数不需要循环调用。

7、下面的程序运行后为什么会爆异常。
typeProjectstruct{}
func(p*Project)deferError(){
iferr:=recover();err!=nil{
fmt.Println("recover:",err)
}
}
func(p*Project)exec(msgchanchaninterface{}){
formsg:=rangemsgchan{
m:=msg.(int)
fmt.Println("msg:",m)
}
}
func(p*Project)run(msgchanchaninterface{}){
for{
deferp.deferError()
gop.exec(msgchan)
time.Sleep(time.Second*2)
}
}
func(p*Project)Main(){
a:=make(chaninterface{},100)
gop.run(a)
gofunc(){
for{
a<-"1"
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second*100000000000000)
}
funcmain(){
p:=new(Project)
p.Main()
}
解析:
有一下几个问题:
1.time.Sleep的参数数值太大,超过了1<<63-1的限制。
2.deferp.deferError()需要在协程开始出调用,否则无法捕获panic。

8、请说出下面代码哪里写错了
funcmain(){
abc:=make(chanint,1000)
fori:=0;i<10;i++{
abc<-i
}
gofunc(){
fora:=rangeabc{
fmt.Println("a:",a)
}
}()
close(abc)
fmt.Println("close")
time.Sleep(time.Second*100)
}
解析:
协程可能还未启动,管道就关闭了。

9、请说出下面代码,执行时为什么会报错
typeStudentstruct{
namestring
}
funcmain(){
m:=map[string]Student{"people":{"zhoujielun"}}
m["people"].name="wuyanzu"
}
解析:
map的value本身是不可寻址的,因为map中的值会在内存中移动,并且旧
的指针地址在map改变时会变得无效。故如果需要修改map值,可以将map
中的非指针类型value,修改为指针类型,比如使用map[string]*Student.

10、请说出下面的代码存在什么问题?
typequeryfunc(string)string
funcexec(namestring,vs...query)string{
ch:=make(chanstring)
fn:=func(iint){
ch<-vs[i](name)
}
fori,_:=rangevs{
gofn(i)
}
return<-ch
}
funcmain(){
ret:=exec("111",func(nstring)string{
returnn+"func1"
},func(nstring)string{
returnn+"func2"
},func(nstring)string{
returnn+"func3"
},func(nstring)string{
returnn+"func4"
})
fmt.Println(ret)
}
解析:
依据4个goroutine的启动后执行效率,很可能打印111func4,但其他的
111func*也可能先执行,exec只会返回一条信息。

11、下面这段代码为什么会卡死?
packagemain
import(
"fmt"
"runtime"
)
funcmain(){
varibyte
gofunc(){
fori=0;i<=255;i++{
}
}()
fmt.Println("Droppingmic")
//Yieldexecutiontoforceexecutingothergoroutines
runtime.Gosched()
runtime.GC()
fmt.Println("Done")
}
解析:
Golang中,byte其实被alias到uint8上了。所以上面的for循环会始终
成立,因为i++到i=255的时候会溢出,i<=255一定成立。
也即是,for循环永远无法退出,所以上面的代码其实可以等价于这样:
gofunc(){
for{}
}
正在被执行的goroutine发生以下情况时让出当前goroutine的执行权,并
调度后面的goroutine执行:
IO操作
Channel阻塞
systemcall
运行较长时间
如果一个goroutine执行时间太长,scheduler会在其G对象上打上一个
标志(preempt),当这个goroutine内部发生函数调用的时候,会先主动
检查这个标志,如果为true则会让出执行权。
main函数里启动的goroutine其实是一个没有IO阻塞、没有Channel
阻塞、没有systemcall、没有函数调用的死循环。
也就是,它无法主动让出自己的执行权,即使已经执行很长时间,scheduler
已经标志了preempt。
而golang的GC动作是需要所有正在运行goroutine都停止后进行的。因
此,程序会卡在runtime.GC()等待所有协程退出。

常见语法题目二


1、写出下面代码输出内容。
packagemain
import(
"fmt"
)
funcmain(){
defer_call()
}
funcdefer_call(){
deferfunc(){fmt.Println("打印前")}()
deferfunc(){fmt.Println("打印中")}()
deferfunc(){fmt.Println("打印后")}()
panic("触发异常")
}
解析:
defer关键字的实现跟go关键字很类似,不同的是它调用的是
runtime.deferproc而不是runtime.newproc。
在defer出现的地方,插入了指令callruntime.deferproc,然后在函数返回之前的
地方,插入指令callruntime.deferreturn。
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将
需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依
次从defer表中出栈并执行。
因此,题目最后输出顺序应该是defer定义顺序的倒序。panic错误并不能终
止defer的执行。

2、以下代码有什么问题,说明原因
typestudentstruct{
Namestring
Ageint
}
funcpase_student(){
m:=make(map[string]*student)
stus:=[]student{
{Name:"zhou",Age:24},
{Name:"li",Age:23},
{Name:"wang",Age:22},
}
for_,stu:=rangestus{
m[stu.Name]=&stu
}
}
解析:
golang的for...range语法中,stu变量会被复用,每次循环会将集合中的值复
制给这个变量,因此,会导致最后m中的map中储存的都是stus最后一个
student的值。

3、下面的代码会输出什么,并说明原因
funcmain(){
runtime.GOMAXPROCS(1)
wg:=sync.WaitGroup{}
wg.Add(20)
fori:=0;i<10;i++{
gofunc(){
fmt.Println("i:",i)
wg.Done()
}()
}
fori:=0;i<10;i++{
gofunc(iint){
fmt.Println("i:",i)
wg.Done()
}(i)
}
wg.Wait()
}
解析:
这个输出结果决定来自于调度器优先调度哪个G。从runtime的源码可以看
到,当创建一个G时,会优先放入到下一个调度的runnext字段上作为下一次
优先调度的G。因此,最先输出的是最后创建的G,也就是9.
funcnewproc(sizint32,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)
ifmainStarted{
wakep()
}
})
}
...
ifnext{
retryNext:
oldnext:=_p_.runnext
//当next是true时总会将新进来的G放入下一次调度字段中
if!_p_.runnext.cas(oldnext,guintptr(unsafe.Pointer(gp))){
gotoretryNext
}
ifoldnext==0{
return
}
//Kicktheoldrunnextouttotheregularrunqueue.
gp=oldnext.ptr()
}

4、下面代码会输出什么?
typePeoplestruct{}
func(p*People)ShowA(){
fmt.Println("showA")
p.ShowB()
}
func(p*People)ShowB(){
fmt.Println("showB")
}
typeTeacherstruct{
People
}
func(t*Teacher)ShowB(){
fmt.Println("teachershowB")
}
funcmain(){
t:=Teacher{}
t.ShowA()
}
解析:
输出结果为showA、showB。golang语言中没有继承概念,只有组合,也没有
虚方法,更没有重载。因此,*Teacher的ShowB不会覆写被组合的People的方
法。

5、下面代码会触发异常吗?请详细说明
funcmain(){
runtime.GOMAXPROCS(1)
int_chan:=make(chanint,1)
string_chan:=make(chanstring,1)
int_chan<-1
string_chan<-"hello"
select{
casevalue:=<-int_chan:
fmt.Println(value)
casevalue:=<-string_chan:
panic(value)
}
}
解析:
结果是随机执行。golang在多个case可读的时候会公平的选中一个执行。

6、下面代码输出什么?
funccalc(indexstring,a,bint)int{
ret:=a+b
fmt.Println(index,a,b,ret)
returnret
}
funcmain(){
a:=1
b:=2
defercalc("1",a,calc("10",a,b))
a=0
defercalc("2",a,calc("20",a,b))
b=1
}
解析:
输出结果为:
10123
20022
2022
1134
defer在定义的时候会计算好调用函数的参数,所以会优先输出10、20两个参
数。然后根据定义的顺序倒序执行。

7、请写出以下输入内容
funcmain(){
s:=make([]int,5)
s=append(s,1,2,3)
fmt.Println(s)
}
解析:
输出为00000123。
make在初始化切片时指定了长度,所以追加数据时会从len(s)位置开始填充数
据。

8、下面的代码有什么问题?
type UserAgesstruct{
ages map[string]int
sync.Mutex
}
func(ua*UserAges)Add(namestring,ageint){
ua.Lock()
deferua.Unlock()
ua.ages[name]=age
}
func(ua*UserAges)Get(namestring)int{
ifage,ok:=ua.ages[name];ok{
returnage
}
return-1
}
解析:
在执行Get方法时可能被panic。
虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于
引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变
量,此时同时读写资源存在竞争关系。会报错误信息:“fatalerror:
concurrentmapreadandmapwrite”。
因此,在Get中也需要加锁,因为这里只是读,建议使用读写
锁sync.RWMutex。

9、下面的迭代会有什么问题?
func(set*threadSafeSet)Iter()<-chaninterface{}{
ch:=make(chaninterface{})
gofunc(){
set.RLock()
forelem:=rangeset.s{
ch<-elem
}
close(ch)
set.RUnlock()
}()
returnch
}
解析:
默认情况下make初始化的channel是无缓冲的,也就是在迭代写时会阻塞。

10、以下代码能编译过去吗?为什么?
packagemain
import(
"fmt"
)
typePeopleinterface{
Speak(string)string
}
typeStudentstruct{}
func(stu*Student)Speak(thinkstring)(talkstring){
ifthink=="bitch"{
talk="Youareagoodboy"
}else{
talk="hi"
}
return
}
funcmain(){
varpeoPeople=Student{}
think:="bitch"
fmt.Println(peo.Speak(think))
}
解析:
编译失败,值类型Student{}未实现接口People的方法,不能定义为People类
型。
在golang语言中,Student和*Student是两种类型,第一个是表
示Student本身,第二个是指向Student的指针。

11、以下代码打印出来什么内容,说出为什么。。。
packagemain
import(
"fmt"
)
typePeopleinterface{
Show()
}
typeStudentstruct{}
func(stu*Student)Show(){
}
funclive()People{
varstu*Student
returnstu
}
funcmain(){
iflive()==nil{
fmt.Println("AAAAAAA")
}else{
fmt.Println("BBBBBBB")
}
}
解析:
跟上一题一样,不同的是*Student的定义后本身没有初始化值,所
以*Student是nil的,但是*Student实现了People接口,接口不为nil。

算法题

1 写代码实现两个goroutine,其中一个产生随机数并写入到gochannel中,
另外一个从channel中读取数字并打印到标准输出。最终输出五个随机数。
解析
这是一道很简单的golang基础题目,实现方法也有很多种,一般想答让面试
官满意的答案还是有几点注意的地方。
1.goroutine在golang中式非阻塞的
2.channel无缓冲情况下,读写都是阻塞的,且可以用for循环来读取数据,
当管道关闭后,for退出。
3.golang中有专用的selectcase语法从管道读取数据。
示例代码如下:
funcmain(){
out:=make(chanint)
wg:=sync.WaitGroup{}
wg.Add(2)
gofunc(){
deferwg.Done()
fori:=0;i<5;i++{
out<-rand.Intn(5)
}
close(out)
}()
gofunc(){
deferwg.Done()
fori:=rangeout{
fmt.Println(i)
}
}()
wg.Wait()
}

2 实现阻塞读且并发安全的map
GO里面MAP如何实现key不存在get操作等待直到key存在或者超时,
保证并发安全,且需要实现以下接口:
typespinterface{
Out(keystring,valinterface{})//存入key/val,如果该key读取的
goroutine挂起,则唤醒。此方法不会阻塞,时刻都可以立即执行并返回
Rd(keystring,timeouttime.Duration)interface{}//读取一个key,如果key
不存在阻塞,等待key存在或者超时
}
解析:
看到阻塞协程第一个想到的就是channel,题目中要求并发安全,那么必须用
锁,还要实现多个goroutine读的时候如果值不存在则阻塞,直到写入值,那么
每个键值需要有一个阻塞goroutine的channel。
实现如下:
typeMapstruct{
cmap[string]*entry
rmx*sync.RWMutex
}
typeentrystruct{
chchanstruct{}
valueinterface{}
isExistbool
}
func(m*Map)Out(keystring,valinterface{}){
m.rmx.Lock()
deferm.rmx.Unlock()
item,ok:=m.c[key]
if!ok{
m.c[key]=&entry{
value:val,
isExist:true,
}
return
}
item.value=val
if!item.isExist{
ifitem.ch!=nil{
close(item.ch)
item.ch=nil
}
}
return
}

3 高并发下的锁与map的读写

场景:在一个高并发的web服务器中,要限制IP的频繁访问。现模拟100个IP同时
并发访问服务器,每个IP要重复访问1000次。
每个IP三分钟之内只能访问一次。修改以下代码完成该过程,要求能成功输出
success:100
packagemain
import(
"fmt"
"time"
)
typeBanstruct{
visitIPsmap[string]time.Time
}
funcNewBan()*Ban{
return&Ban{visitIPs:make(map[string]time.Time)}
}
func(o*Ban)visit(ipstring)bool{
if_,ok:=o.visitIPs[ip];ok{
returntrue
}
o.visitIPs[ip]=time.Now()
returnfalse
}
funcmain(){
success:=0
ban:=NewBan()
fori:=0;i<1000;i++{
forj:=0;j<100;j++{
gofunc(){
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启动
协程时传参。
packagemain
import(
"context"
"fmt"
"sync"
"sync/atomic"
"time"
)
typeBanstruct{
visitIPsmap[string]time.Time
locksync.Mutex
}
funcNewBan(ctxcontext.Context)*Ban{
o:=&Ban{visitIPs:make(map[string]time.Time)}
gofunc(){
timer:=time.NewTimer(time.Minute*1)
for{
select{
case<-timer.C:
o.lock.Lock()
fork,v:=rangeo.visitIPs{
iftime.Now().Sub(v)>=time.Minute*1{
delete(o.visitIPs,k)
}
}
o.lock.Unlock()
timer.Reset(time.Minute*1)
case<-ctx.Done():
return
}
}
}()
returno
}
func(o*Ban)visit(ipstring)bool{
o.lock.Lock()
defero.lock.Unlock()
if_,ok:=o.visitIPs[ip];ok{
returntrue
}
o.visitIPs[ip]=time.Now()
returnfalse
}
funcmain(){
success:=int64(0)
ctx,cancel:=context.WithCancel(context.Background())
defercancel()
ban:=NewBan(ctx)
wait:=&sync.WaitGroup{}
wait.Add(1000*100)
fori:=0;i<1000;i++{
forj:=0;j<100;j++{
gofunc(jint){
deferwait.Done()
ip:=fmt.Sprintf("192.168.1.%d",j)
if!ban.visit(ip){
atomic.AddInt64(&success,1)
}
}(j)
}
}
wait.Wait()
fmt.Println("success:",success)
}

4 写出以下逻辑,要求每秒钟调用一次proc并保证程序不退出?
packagemain
funcmain(){
gofunc(){
//1在这里需要你写算法
//2要求每秒钟调用一次proc函数
//3要求程序不能退出
}()
select{}
}
funcproc(){
panic("ok")
}
解析
题目主要考察了两个知识点:
1.定时执行执行任务
2.捕获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")
}

5 为sync.WaitGroup中Wait函数支持WaitTimeout功能.
packagemain
import(
"fmt"
"sync"
"time"
)
funcmain(){
wg:=sync.WaitGroup{}
c:=make(chanstruct{})
fori:=0;i<10;i++{
wg.Add(1)
gofunc(numint,close<-chanstruct{}){
deferwg.Done()
<-close
fmt.Println(num)
}(i,c)
}
ifWaitTimeout(&wg,time.Second*5){
close(c)
fmt.Println("timeoutexit")
}
time.Sleep(time.Second*10)
}
funcWaitTimeout(wg*sync.WaitGroup,timeouttime.Duration)bool{
//要求手写代码
//要求sync.WaitGroup支持timeout功能
//如果timeout到了超时时间返回true
//如果WaitGroup自然结束返回false
}
解析
首先sync.WaitGroup对象的Wait函数本身是阻塞的,同时,超时用到的
time.Timer对象也需要阻塞的读。
同时阻塞的两个对象肯定要每个启动一个协程,每个协程去处理一个阻塞,难点在于怎
么知道哪个阻塞先完成。
目前我用的方式是声明一个没有缓冲的chan,谁先完成谁优先向管道中写入数据。
packagemain
import(
"fmt"
"sync"
"time"
)
funcmain(){
wg:=sync.WaitGroup{}
c:=make(chanstruct{})
fori:=0;i<10;i++{
wg.Add(1)
gofunc(numint,close<-chanstruct{}){
deferwg.Done()
<-close
fmt.Println(num)
}(i,c)
}
ifWaitTimeout(&wg,time.Second*5){
close(c)
fmt.Println("timeoutexit")
}
time.Sleep(time.Second*10)
}
funcWaitTimeout(wg*sync.WaitGroup,timeouttime.Duration)bool{
//要求手写代码
//要求sync.WaitGroup支持timeout功能
//如果timeout到了超时时间返回true
//如果WaitGroup自然结束返回false
ch:=make(chanbool,1)
gotime.AfterFunc(timeout,func(){
ch<-true
})
gofunc(){
wg.Wait()
ch<-false
}()
return<-ch
}

go string 的底层数据结构

在Go语言中,字符串(string)的底层数据结构可以被理解为一个不可变的字节序列。具体来说:

Go语言中的字符串并不是直接存储字符数组,而是通过一个名为stringStruct的结构体来间接表示这个字节序列。这个结构体通常不直接暴露给用户,它在运行时包(runtime package)中定义,大致如下:

 

Go

1// 这是Go运行时内部对字符串类型的抽象,实际源码更为复杂且可能随Go版本变化
2type stringStruct struct {
3    str unsafe.Pointer // 指向实际字节数组的指针
4    len int           // 字节数组的长度
5}

因此,在Go中,一个字符串实际上是一个指向只读字节数组的指针以及该数组的长度。这意味着字符串一旦初始化后,其内容就不能被修改,因为尝试修改会触发运行时错误。由于这种设计,字符串可以安全地在多个goroutine之间共享而无需同步。

此外,Go语言编译器和运行时系统会对字符串做很多优化,例如在某些情况下字符串字面量可能会被编译器静态分配,并且相邻的字符串字面量在内存中可能会共享相同的底层数组以节省空间。

6 写出以下代码出现的问题
packagemain
import(
"fmt"
)
funcmain(){
varxstring=nil
ifx==nil{
x="default"
}
fmt.Println(x)
}
golang中字符串是不能赋值nil的,也不能跟nil比较。
 

写出以下代码出现的问题
packagemain
import(
"fmt"
)
funcmain(){
varxstring=nil
ifx==nil{
x="default"
}
fmt.Println(x)
}
golang中字符串是不能赋值nil的,也不能跟nil比较。

7 找出下面代码的问题
packagemain
import"fmt"
typequeryfunc(string)string
funcexec(namestring,vs...query)string{
ch:=make(chanstring)
fn:=func(iint){
ch<-vs[i](name)
}
fori,_:=rangevs{
gofn(i)
}
return<-ch
}
funcmain(){
ret:=exec("111",func(nstring)string{
returnn+"func1"
},func(nstring)string{
returnn+"func2"
},func(nstring)string{
returnn+"func3"
},func(nstring)string{
returnn+"func4"
})
fmt.Println(ret)
}
上面的代码有严重的内存泄漏问题,出错的位置是gofn(i),实际上代码执行后
会启动4个协程,但是因为ch是非缓冲的,只可能有一个协程写入成功。而
其他三个协程会一直在后台等待写入。

雪花算法(Snowflake)是一种高性能、高可用的分布式ID生成算法,由Twitter公司在2010年左右开发并开源。它主要用于生成全局唯一且趋势递增的ID序列,特别适合于分布式系统环境下的应用,比如数据库中的主键生成、消息队列的Message ID生成等场景。

雪花算法生成的ID是一个64位的整数,这个ID被划分为以下几个部分:

  • 时间戳:占41位,精确到毫秒级,可以表示从某个时间点(如2010-01-01 00:00:00 UTC)开始大约69年的跨度。
  • 数据中心标识(或机器/工作进程ID):占10位,用于区分不同的数据中心或者机器节点,理论上支持最多1024个节点。
  • 序列号:占12位,同一节点同一毫秒内生成的不同ID的计数器,每台机器在同一毫秒内最多可以生成4096个不同ID。

通过这种设计,雪花算法能够保证在分布式的环境下生成的ID具有以下特点:

  1. 全局唯一性:结合了时间戳、数据中心ID和序列号,确保即使在多节点同时生成ID的情况下也能避免冲突。
  2. 有序性:由于ID中包含了时间戳信息,所以生成的ID天然带有时间上的顺序,对于那些需要按照生成时间排序的业务非常有用。
  3. 高性能:无需依赖外部存储或服务,纯内存操作即可快速生成ID,适合高并发场景。

总的来说,雪花算法巧妙地利用了64位整数的空间,既满足了分布式系统对唯一标识符的需求,又保持了很好的性能与可扩展性。

大小顶堆(通常简称为大顶堆或小顶堆)是计算机科学中的一种特殊的数据结构,它是完全二叉树的一种。在大顶堆中,父节点的值总是大于或等于其所有子节点的值,因此堆顶元素始终是最大的;而在小顶堆中,父节点的值总是小于或等于其所有子节点的值,堆顶元素始终是最小的。

大顶堆(Max Heap)特性: 对于任意节点i:

  • 如果i有左孩子,则A[i] >= A[2*i + 1](左孩子的索引)
  • 如果i有右孩子,则A[i] >= A[2*i + 2](右孩子的索引)

小顶堆(Min Heap)特性: 对于任意节点i:

  • 如果i有左孩子,则A[i] <= A[2*i + 1]
  • 如果i有右孩子,则A[i] <= A[2*i + 2]

大小顶堆常用于实现优先队列,其中大顶堆适用于“最大优先”的场景,比如求解Top K问题;小顶堆适用于“最小优先”的场景,例如实现一个动态更新并始终保持最小值在顶部的操作系统任务调度算法。

堆的插入、删除(弹出堆顶元素并重新调整结构以满足堆性质)、以及堆排序等操作都有相应的高效算法。堆数据结构支持O(log n)的时间复杂度进行插入和删除操作,这是因为堆是一个近似完全平衡的二叉树,高度大致为log₂(n),所以对每个节点的操作至多需要向下调整到叶子节点。

B+树在数据库系统和文件系统中作为索引结构具有以下优点:

  1. 大扇出(High Fan-out):B+树每个节点可以拥有大量的子节点,通常为几百到几千个,这大大减少了树的高度,使得查询时磁盘I/O次数相对较少。相比于B树,B+树的内部节点(非叶子节点)只存储关键字信息和子节点指针,不存储数据,因此能容纳更多索引项。

  2. 顺序访问特性:B+树的所有叶子节点形成一个有序链表,并且叶子节点包含了全部的数据记录,这样非常适合进行范围查询或者全表扫描操作。当需要查找某个范围内的所有记录时,可以直接从某个叶子节点开始,沿着叶子节点间的链表顺序查找,无需回溯到父节点或遍历整个索引。

  3. 聚集索引:由于所有的数据都存储在叶子节点上,而且是物理排序的,因此对于基于主键的查询效率很高,也便于做覆盖索引优化(即查询结果所需的所有字段都可以直接从索引中获取,无需额外读取数据页)。

  4. 高效空间利用:因为内部节点不保存实际的数据行,所以单个数据块(比如磁盘块)能够存放更多的索引项,提高了空间利用率,进而降低树的高度,提升查询性能。

  5. 缓存友好:在现代数据库系统中,内存有限,而磁盘访问速度远低于内存访问速度。B+树的这些特性使得它更容易利用数据库系统的缓存机制,减少磁盘I/O,提高整体性能。

综上所述,B+树在实现大规模数据存储和检索时,尤其是对大量数据进行快速查找、范围查找及全表扫描等操作时,表现出了非常优秀的性能特点。

写出以下打印结果
packagemain
import(
"fmt"
)
typeStudentstruct{
Namestring
}
funcmain(){
fmt.Println(&Student{Name:"menglu"}==&Student{Name:"menglu"})
fmt.Println(Student{Name:"menglu"}==Student{Name:"menglu"})
}
个人理解:指针类型比较的是指针地址,非指针类型比较的是每个属性的值。

下面代码写法有什么问题?
packagemain
import(
"fmt"
)
typeStudentstruct{
Ageint
}
funcmain(){
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来赋值对象。

8 下面代码写法有什么问题?
packagemain
import(
"fmt"
)
typeStudentstruct{
Ageint
}
funcmain(){
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来赋值对象。

使用count(*)而不是count(列名)

在SQL查询中,COUNT(*)COUNT(列名)的主要区别在于统计的内容:

  • COUNT(*):计算表中的总行数,包括所有行,无论这些行的各列值是否为NULL。即使某一行的所有列都为NULL,它也会被计算在内。

     Sql 
    1SELECT COUNT(*) FROM table_name;
  • COUNT(列名):只计算指定列中非NULL值的数量。如果某一行该列的值为NULL,则这一行不会被计入总数。

     Sql 
    1SELECT COUNT(column_name) FROM table_name;

因此,在以下场景中更适合使用COUNT(*)

  • 当你需要获取表中的总行数,而不关心具体某一列是否有NULL值时。
  • 在需要统计所有行(包括NULL值)的情况下。

COUNT(列名)则适用于以下情况:

  • 你关注的是特定列中有实际值(非NULL)的行数量。
  • 对于大数据量表,如果你只想知道不为空(非NULL)的记录数,特别是当表设计允许列有NULL值时。

总的来说,根据你的实际需求来决定采用哪种计数方式。如果目标是得到表中的确切行数,包括可能包含NULL值的行,通常更推荐使用COUNT(*)

relaylog

在MySQL数据库的复制架构中,Relay Log(中继日志)是一个重要的概念。它主要用于主从复制场景,尤其是在基于行的复制模式下。

当MySQL服务器作为主库时,它会记录所有更改数据的SQL语句或者二进制日志事件到其自身的Binary Log(二进制日志)。而在从库上,有一个I/O线程负责连接到主库并读取主库的Binary Log内容,并将这些事件写入到从库本地的Relay Log。

从库上的SQL线程会顺序地读取Relay Log中的事件,并执行这些事件,从而实现主从数据的一致性。通过这种方式,主库的数据更改能够被可靠地传播到从库上。

总结来说,Relay Log是MySQL主从复制过程中,从库用于存储主库Binary Log内容的一个临时媒介,它起到了桥梁和中转站的作用,确保了主从数据同步的过程可以顺利进行。

  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值