归纳一下近期面试,没有回答好的几个问题。多次面试的总体反馈,非单次。 Jacky哥觉得有意思或者有价值的才会详细更新,大路货的题目就不打字了,哪里都能搜到。
1.什么是反射?什么时候用反射?反射可以做什么?
答:
反射是go语言中的一个重要的概念,其最重要的功能就是获取类型相关的信息,以及获取和修改原始数据的值。反射里最重要的两个概念是Type和Value,Type用于获取类型相关的信息(比如Slice的长度,struct的成员,函数的参数个数),Value用于获取和修改原始数据的值(比如修改slice和map中的元素,修改struct的成员变量)。它的一个最典型的应用举例就是gorm中的数据库字段映射。
2.能不能说说Java中的反射与golang中的反射的区别?
答:
在java中,通过类的描述,来获得method,由于该method是属于类级别的,所以,调用时,需要传入参数obj和args;
而golang中,method是对象级别的,所以,调用时,不需要参数obj,只需要args。
3.有用过interface吗?你在什么情况下用到interface的?
答:
接口是一组行为规范的集合,常做为抽象方法使用,当作为抽象方法时仅定义了方法名,参数值和返回值类型。
4.详细说说goroutine的生命周期?
5.所有的panic都能被捕获吗?如果不能,哪种情况捕获不了。
6.请用尽量详细的语言描述说说GMP模型。
7.什么是grpc,有没有用过grpc,能说说你对它的理解吗?他能拿来做什么呢?
答:
有用过 gRPC是Google开源的一种高性能、通用的远程过程调用(RPC)框架,基于Protocol Buffers序列化协议进行数据传输。通过官方的protoc-gen-go包,可以编写简单的的proto文件并通过一条指令生成对应的go文件,go文件中定义了服务名,服务接收的参数和返回值。
共分为客户端和服务器两个部分,服务器端开启长驻的goroutine,定义了地址/域名、调用方法以及通信的端口号。客户端通过grpc.Dial()与服务器连接上,并调用服务器端的方法。
8.gprc你们是用的什么场景,一次传输的数据量有多大?
移动端和服务器通信,qps的峰值是每秒20。
9.如果有个方法改变了Slice中的某个值,此时该Slice的内存有什么变化?如果同样的操作发生在数组上呢?
答:
你刚刚提到的是修改,而不是append,这就不触及到扩容问题。在不扩容的前提下,修改某个切片中的一个值,仅对底层数组对应的value进行操作,而其地址不变。
对于slice只要没有涉及到扩容,仅修改其值,不会改变底层数组的地址。
对于数组来说,修改它的值,意味着在这之前数组中已经有值,那么结果和slice一样,其地址不变,值改变。
10.golang中goroutine和python中coroutine和区别?
狭义地说,goroutine 可能发生在多线程环境下,goroutine 无法控制自己获取高优先度支持;coroutine 始终发生在单线程,coroutine 程序需要主动交出控制权,宿主才能获得控制权并将控制权交给其他 coroutine。
goroutine 间使用 channel 通信,coroutine 使用 yield 和 resume 操作。
goroutine 和 coroutine 的概念和运行机制都是脱胎于早期的操作系统。
coroutine 的运行机制属于协作式任务处理,早期的操作系统要求每一个应用必须遵守操作系统的任务处理规则,应用程序在不需要使用 CPU 时,会主动交出 CPU 使用权。如果开发者无意间或者故意让应用程序长时间占用 CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。
goroutine 属于抢占式任务处理,已经和现有的多线程和多进程任务处理非常类似。应用程序对 CPU 的控制最终还需要由操作系统来管理,操作系统如果发现一个应用程序长时间大量地占用 CPU,那么用户有权终止这个任务。
- goroutine通过通道来通信 coroutine通过让出和恢复操作来通信。
- goroutine协程间不完全同步,可以利用多核并行运行,具体要看channel的设计 coroutine协程间完全同步,不会并行。
- goroutine可以在多个协程在多个线程上切换,既可以用到多核,又可以减少切换开销; coroutine在一个线程中运行。
- goroutine操作系统如果发现一个应用程序长时间大量地占用 CPU,那么用户有权终止这个任务。 coroutine如果开发者无意间或者故意让应用程序长时间占用 CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。
11.如何使用golang实现url参数解析?
有对应的包,包名就是url,方法名Parse 实现方式
package main
import (
"log"
"net/url"
)
func main() {
site := "http://www.jacky.com/?name=xxx&name=222&age=xxx&token=dsjsdhggf"
r, err := url.Parse(site)
if err != nil {
log.Fatalln("parse url [%v] failed!", site)
}
log.Printf("parse url value is [%v]", r)
log.Printf("Schema is [%v]", r.Scheme)
log.Printf("Login is [%v]", r.User.Username())
if password, ok := r.User.Password(); ok {
log.Printf("Password is [%v]", password)
}
log.Printf("Address is [%v]", r.Hostname())
log.Printf("Port is [%v]", r.Port())
log.Printf("Resource is [%v]", r.Path)
log.Printf("Query is [%v]", r.Query())
log.Printf("Fragment is [%v]", r.Fragment)
}
12.Python多进程、多线程、多协程之间的优缺点对比?(课外题)
1.多进程Process(multiprocessing)
优点:可以利用多核CPU并行运算
缺点:占用资源多、可启动数目比线程少
适用于:CPU密集型计算2.多线程Thread(threading)
优点:相比进程,更轻量级、占用资源少
缺点:
相比进程,python多线程只能并发执行,不能利用多CPU(GIL);(只能使用一个CPU)
相比协程,启动数目有限制,占用内存资源,有线程切换开销
适用于:I/O密集型计算、同时运行的任务数目要求不多3.多协程Coroutine(asyncio)
优点:内存开销最少、启动协程数量最多
缺点:支持的库有限制(aiohttp可以用,requests不能用)、代码实现复杂
适用于:IO密集型计算、需超多任务运行、但有现成库支持的场景
13.说说以下代码的输出结果?
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
f(s)
fmt.Println(s)
}
func f(s []int) {
s[0] = 6
s = append(s, 10, 20, 30)
}
答:
原slice被修改为[6 2 3],但不会扩容
输出结果如下:
① s[0] = 6这一步还算是在修改原数组(切片底层的数据)
② s = append(s, 10, 20, 30)会开辟一个新的数组空间,把旧值copy过去
此时由于func f()没有返回值,并且main中也没有使用s来接收改变过后的变量。
因此原始数组仅有下标为0的值被修改到,后续操作没有改变原始数组。
14.简述下你以前项目的代码发布流程?
15.假如你用一个协程A开启了10个协程,其中一个协程出错了,需要关闭与它同级的9个协程,要怎么做?如果同时还要关闭创建它的协程呢?
答:控制协程比较优雅的方式有3种,①channel②context③sync.Cond
方案1:
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan struct{})
for i := 0; i < 10; i++ { //开启10个协程
go func(num int) {
fmt.Printf("Goroutine %d\n", num)
fmt.Printf("处理自己的业务%d\n", num)
<-quit //等待关闭信号
}(i)
}
time.Sleep(2 * time.Second)
//关闭goroutine
close(quit)
}
方案2:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 10; i++ {
go func(num int) {
//goroutine 1
fmt.Printf("Goroutine %d\n", num)
fmt.Printf("处理自己的业务%d\n", num)
select {
case <-ctx.Done(): //等待关闭信号
return
}
}(i)
}
time.Sleep(2 * time.Second)
//关闭goroutine
cancel()
}
方案3:
package main
import (
"fmt"
"sync"
"time"
)
var mu = sync.Mutex{}
func main() {
cond := sync.NewCond(&mu)
for i := 0; i < 10; i++ {
go func(num int) {
for {
mu.Lock() //等价于cond.L.Lock()
cond.Wait()
fmt.Printf("Goroutine %d\n", num)
fmt.Printf("处理自己的业务%d\n", num)
mu.Unlock() //等价于cond.L.Unlock()
}
}(i)
}
time.Sleep(2 * time.Second)
//关闭goroutine
cond.Broadcast()
}
16.有3三个协程a、b、c,必须按照a、b、c顺序执行,
答:三个协程分别定义为desk、floor、aircondition
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func print(str string, outputChan, inputChan chan struct{}) {
time.Sleep(1 * time.Second)
<-outputChan
fmt.Println(str)
inputChan <- struct{}{}
wg.Done()
}
func main() {
deskChan, floorChan, airConditionChan := make(chan struct{}, 1), make(chan struct{}), make(chan struct{})
start := time.Now().Unix()
wg.Add(3)
deskChan <- struct{}{}
go print("desk", deskChan, floorChan)
go print("floor", floorChan, airConditionChan)
go print("airCondition", airConditionChan, deskChan)
wg.Wait()
end := time.Now().Unix()
fmt.Printf("花费时间:%d\n", end-start)
}
输出结果符合预期:
17.如果有4个协程,前三个执行完才能执行最后一个,你会怎么安排?
答:这道题的答案与12题类似,在前三个goroutine结束后。开启第四个协程。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func print(str string, outputChan, inputChan chan struct{}) {
time.Sleep(1 * time.Second)
<-outputChan
fmt.Println(str)
inputChan <- struct{}{}
wg.Done()
}
func main() {
deskChan, floorChan, airConditionChan := make(chan struct{}, 1), make(chan struct{}), make(chan struct{})
start := time.Now().Unix()
wg.Add(3)
deskChan <- struct{}{}
go print("desk", deskChan, floorChan)
go print("floor", floorChan, airConditionChan)
go print("airCondition", airConditionChan, deskChan)
wg.Wait()
time.Sleep(1 * time.Second)
wg.Add(1)
go func() {
wg.Done()
fmt.Println("我来做第4件事")
}()
wg.Wait()
end := time.Now().Unix()
fmt.Printf("花费时间:%d\n", end-start)
}
输出结果符合预期
18.上题中,如果前三个协程中有一个执行时死锁了,最后一个能执行到吗?CPU此时的状态如何?
答:前一个协程死锁,后一个协程永远不会得到执行,因为通道阻塞。协程中没有无限循环或不断扩容的slice,不会导致cpu飙升。
19.有没有遇到过goroutine导致的内存泄露问题?如果遇到该怎么处理?
答:
在实际生产中没有遇到过。但我可以讲讲它的成因,控制和解决方式。
首先我们要记住一个原则,当你不知道如何关闭一个协程的时候,最好不要开启它。
协程控制的方法在第6问我们已经讨论过,这里不再赘述。
①.先说成因
Go内存泄露,相当多数都是goroutine泄露导致的。 虽然每个goroutine仅占用少量(栈)内存,但当大量goroutine被创建却不会释放时(即发生了goroutine泄露),也会消耗大量内存,造成内存泄露。
另外,如果goroutine里还有在堆上申请空间的操作,则这部分堆内存也不能被垃圾回收器回收。
常见原因有以下几种
1.从channel里读,但是同时没有写入操作。
2.向无缓冲 channel里写,但是同时没有读操作。
3.向已满的有缓冲channel里写,但是同时没有读操作。
4.select操作在所有case上都阻塞()。
5.goroutine进入死循环,一直结束不了。
6.创建了一个cap很大的slice,但只用到很少一部slice。这种slice大量存在,并且还在不停的创建。
7.time.Ticker造成内存泄漏。开启定时器后没有stop它。
8.不停的打开http连接,获取到response.Body之后没有关闭连接。
9.使用os.Open打开文件之后,没有关闭它。
②.再说检测与处理
发生泄漏前
1.发送者和接收者的数量最好要一致,channel记得初始化,不给程序发生内存泄漏的机会。
2.slice采用深拷贝,也就是copy方式来调整内存空间。 3.当你启一个定时器后,记得一定要关闭它。
发生泄漏后
1.go tool pprof分析内存的占用和变化。
2.er出品的goleak可以集成到单元测试中,能快速检测goroutine泄露,达到避免和排查的目的。
20.golang如何实现二叉树转链表
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func flatten(root *TreeNode) {
dfs(root)
}
func dfs(root *TreeNode) (head, tail *TreeNode) {
if root == nil {
return nil, nil
}
// 递归处理左右结点,并获取对应链表的头结点和尾结点
leftHead, leftTail := dfs(root.Left)
rightHead, rightTail := dfs(root.Right)
// 清空左子结点
root.Left = nil
// 当前结点目前既是头结点,也是尾结点
head, tail = root, root
// 将左半部分挂在链表尾部
if leftHead != nil {
tail.Right = leftHead
tail = leftTail
}
// 将右半部分挂在链表尾部
if rightHead != nil {
tail.Right = rightHead
tail = rightTail
}
// 返回当前子树转换成的链表头结点和尾结点
return head, tail
}
21.go实现生产者消费者,交替打印0-10
package main
import (
"fmt"
"sync"
)
var (
toOdd = make(chan struct{})
toEven = make(chan struct{})
wg1 = sync.WaitGroup{}
)
func main() {
wg1.Add(2)
go printOdd()
go printEven()
wg1.Wait()
}
func printOdd() {
defer wg1.Done()
for i := 1; i <= 10; i += 2 {
if i != 1 {
<-toOdd
}
fmt.Println(i)
toEven <- struct{}{}
}
}
func printEven() {
defer wg1.Done()
for i := 2; i <= 10; i += 2 {
<-toEven
fmt.Println(i)
if i != 10 {
toOdd <- struct{}{}
}
}
}
22.golang实现删除链表倒数第k个节点
第一种方式:(直接查找)
1.首先找到倒数第n个节点的前驱节点。
具体应该如何去找呢,可以这样考虑:首先定义一个变量length,作为记录链表长度的变量,这个变量的获取可以通过遍历链表,在while循环中对给定变量自增的操作得到;在这个基础上要找到链表的倒数第n个节点,那么对于从前往后的顺序,也就是正者数的(length - n)个节点,由于找到当前节点很难删除自身,所以应该找到待删除节点的前一个节点,也就是第(length - n - 1)个节点。
2.直接删除目标节点
这个就比较简单了,直接让该前驱节点的next节点指向next.next节点,跳过该节点,该节点被垃圾回收。删除成功。(由于第一种方法比较简单,也没涉及较多的思想,所以这里就不再进行代码演示了,感兴趣的话可以自己尝试一下)。
func getLength(head *ListNode) (length int) {
for ; head != nil; head = head.Next {
length++
}
return
}
func removeNthFromEnd(head *ListNode, n int) *ListNode {
length := getLength(head)
dummy := &ListNode{0, head}
cur := dummy
for i := 0; i < length-n; i++ {
cur = cur.Next
}
cur.Next = cur.Next.Next
return dummy.Next
}
第二种方法:(双指针)
1.头节点的考虑。对于待删除的节点,可能存在该节点是首节点的情况,这样就得考虑指定一个新的头节点,将原来的头节点接在新的头节点的后面,如果待删除节点是原首节点的话,也可以让新的头节点指向原首节点的next节点即可。同时我们也定义一个pre节点指向当前节点的前一个节点。(对于我上面所说的第一种方法,如果删除的是头节点,那么就由原头节点的下一个节点作为头节点,当然也是可以的)。
2.找到目标节点。这次我们采用不同于上面的思路重新考虑,以双指针的思想,可以先考虑让快指针先移动n个节点,然后再让快指针和慢指针一起移动,移动幅度相同,即每次都移动一个节点,这样当快指针到达链表末端的时候,慢指针正好到达倒数第n个节点。
3.删除目标节点。当找到了倒数第n个节点,也就是慢指针所指向的那个节点,由于有pre节点的存在,可以不考虑从该节点的前一个节点位置删除,直接让当前节点的前一个节点的next指针指向让当前节点的后一个节点即可。具体代码演示如下。
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{0, head}
first, second := head, dummy
for i := 0; i < n; i++ {
first = first.Next
}
for ; first != nil; first = first.Next {
second = second.Next
}
second.Next = second.Next.Next
return dummy.Next
}
23.golang比较字节数组是否相等比较两个slice是否相等
package main
import (
"fmt"
)
func main() {
var a []string
var b []string
a = append(a, "1", "2", "3")
b = append(b, "1", "2", "3")
rst := LoopCompare(a, b)
fmt.Println(rst)
}
func LoopCompare(a, b []string) bool {
if len(a) != len(b) {
return false
}
//与reflect.DeepEqual的结果保持一致:[]int{} != []int(nil)
if (a == nil) != (b == nil) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
24.go字符串转换成io.Reader
bytes.NewBuffer([]byte("aaaaa"))
25.golang多层嵌套循环,如何跳出这个这层循环
使用标记及循环控制语句break,具体示例如下: // 定于一个跳出循环标记
endfor := false
// 定义嵌套for循环
for i := 0; i < 10; i++ {
println("i == ", i)
for a := 0; a < 10; a++ {
println("a == ", a)
if a == 3 {
// 修改跳出循环标记
endfor = true
break
}
}
// 外层循环判断循环标记,true则结束循环
if endfor {
break
}
}
/*
输出结果
i == 0
a == 0
a == 1
a == 2
a == 3
*/
使用go语言的goto语句跳出多层循环:
// 定义嵌套for循环
for i := 0; i < 10; i++ {
println("i == ", i)
for a := 0; a < 10; a++ {
println("a == ", a)
if a == 3 {
// 跳转执行goto代码
goto endfor
}
}
println("before endfor")
endfor:
println("after endfor")
/*
输出结果
i ==0
a == 0
a == 1
a == 2
a == 3
after endfor
*/
}
26.用golang实现链表反转
package main
import (
"fmt"
)
type ListNode struct {
Val int
Next *ListNode
}
func main() {
head := &ListNode{Val: 1, Next: &ListNode{Val: 2, Next: &ListNode{Val: 3, Next: nil}}}
fmt.Println("Original Linked List:")
printList(head)
fmt.Println("Reversed Linked List:")
reversedHead := reverseList(head)
printList(reversedHead)
}
func printList(head *ListNode) {
for head != nil {
fmt.Printf("%d->", head.Val)
head = head.Next
}
fmt.Println("NULL")
}
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next
curr.Next = prev
prev = curr
curr = next
}
return prev
}
27.golang实现stack的pushpopsize
package main
import (
"errors"
"fmt"
)
type Stack struct {
elements []interface{}
size int
}
func (stack *Stack) Push(element interface{}) {
stack.elements = append(stack.elements, element)
stack.size++
}
func (stack *Stack) Pop() (interface{}, error) {
if stack.size == 0 {
return nil, errors.New("stack is empty")
}
element := stack.elements[stack.size-1]
stack.elements = stack.elements[:stack.size-1]
stack.size--
return element, nil
}
func main() {
stack := Stack{}
stack.Push(1)
stack.Push(2)
stack.Push(3)
for i := 0; i < 3; i++ {
element, _ := stack.Pop()
fmt.Println(element)
}
}
28.golang实现LRU算法
package main
import (
"fmt"
)
type Node struct {
Key interface{}
Value interface{}
pre *Node
next *Node
}
type LRUCache struct {
limit int
HashMap map[interface{}]*Node
head *Node
end *Node
}
func (l *LRUCache) removeNode(node *Node) interface{} {
if node == l.end {
l.end = l.end.pre
l.end.next = nil
} else if node == l.head {
l.head = l.head.next
l.head.pre = nil
} else {
node.pre.next = node.next
node.next.pre = node.pre
}
return node.Key
}
func (l *LRUCache) addNode(node *Node) {
if l.end != nil {
l.end.next = node
node.pre = l.end
node.next = nil
}
l.end = node
if l.head == nil {
l.head = node
}
}
func (l *LRUCache) refreshNode(node *Node) {
if node == l.end {
return
}
l.removeNode(node) // 从链表中的任意位置移除原来的位置
l.addNode(node) // 添加到链表的尾部
}
// 构造
func Constructor(capacity int) LRUCache {
lruCache := LRUCache{limit: capacity}
lruCache.HashMap = make(map[interface{}]*Node, capacity)
return lruCache
}
// 获取
func (l *LRUCache) Get(key interface{}) interface{} {
if v, ok := l.HashMap[key]; ok {
l.refreshNode(v)
return v.Value
} else {
return -1
}
}
func (l *LRUCache) Put(key, value interface{}) {
if v, ok := l.HashMap[key]; !ok {
if len(l.HashMap) >= l.limit {
oldKey := l.removeNode(l.head)
delete(l.HashMap, oldKey)
}
node := Node{Key: key, Value: value}
l.addNode(&node)
l.HashMap[key] = &node
} else {
v.Value = value
l.refreshNode(v)
}
}
func (l *LRUCache) getCache() {
for n := l.head; n != nil; n = n.next {
fmt.Println(n.Key, n.Value)
}
}
func main() {
cache := Constructor(3)
cache.Put(11, 1)
cache.Put(22, 2)
cache.Put(33, 3)
cache.Put(44, 4)
v := cache.Get(33)
fmt.Println(v)
fmt.Println("========== 获取数据之后 ===============")
cache.getCache()
}