golang面试归纳总结 - golang篇(持续更新)

278 阅读13分钟

637c30b5b2fe4382.jpg

归纳一下近期面试,没有回答好的几个问题。多次面试的总体反馈,非单次。 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,那么用户有权终止这个任务。

  1. goroutine通过通道来通信 coroutine通过让出和恢复操作来通信。
  2. goroutine协程间不完全同步,可以利用多核并行运行,具体要看channel的设计 coroutine协程间完全同步,不会并行。
  3. goroutine可以在多个协程在多个线程上切换,既可以用到多核,又可以减少切换开销; coroutine在一个线程中运行。
  4. 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],但不会扩容 输出结果如下:
image.png

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)
}

输出结果符合预期:
image.png

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)
}

输出结果符合预期
image.png

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()
}
29. 算法题三数之和
30. 用golang实现二分查找
30.1可以用递归实现吗?
31. 如何求一个64进制数的2进制长度?
32. 给定一个字符串,求字符串中的最长回文字符串
33. grpc的Protobuf的兼容性问题怎么解决?允许改字段类型吗?
34. 链路追踪实现?链路追踪的数据怎么存储的,有哪些存储方案?
35. 了解分布式事务里面的CAP和Base理论吗?说说你的理解。
36. aqs了解吗,什么是aqs?
37. jvm中堆,栈,方法区作用
38. 看你写过限流算法,介绍下?
39. 你们系统的认证和鉴权是怎么做的