当前位置:   article > 正文

最新golang语言面试题总结(一)_golang面试题

golang面试题

1、go中make和new区别

new:

  1. 分配内存。内存里存的值是对应类型的零值。
  2. 只有一个参数。参数是分配的内存空间所存储的变量类型,Go语言里的任何类型都可以是new的参数,比如int, 数组,结构体,甚至函数类型都可以。
  3. 返回的是指针。

make:

  1. 分配和初始化内存。
  2. 只能用于slice, map和chan这3个类型,不能用于其它类型。如果是用于slice类型,make函数的第2个参数表示slice的长度,这个参数必须给值。
  3. 返回的是原始类型,也就是slice, map和chan,不是返回指向slice, map和chan的指针。

2、进程、线程、协程区别

进程:一个具有特定功能的程序运行在一个数据集上的一次动态过程。是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。(QQ、微信等等)

线程:一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会很多),有时被称为轻量级进程(Lightwight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。

协程:一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。                                                                                                                

3、go中channel底层是什么

什么是channel?

chan是Go中的一种特殊类型,不同的协程可以通过channel来进行数据交互。

channel分为有缓冲区与无缓冲区两种

channel底层?

 channel是基于环形队列实现的。

  1. type hchan struct {
  2. qcount uint // 队列中的总数据
  3. dataqsiz uint // 循环队列的大小
  4. buf unsafe.Pointer // 指向环形队列的元素数组
  5. elemsize uint16
  6. closed uint32
  7. elemtype *_type // 元素类型
  8. sendx uint // 发送指数
  9. recvx uint // 收到指数
  10. recvq waitq // 接收列表
  11. sendq waitq // 发生列表
  12. // lock protects all fields in hchan, as well as several
  13. // fields in sudogs blocked on this channel.
  14. //
  15. // Do not change another G's status while holding this lock
  16. // (in particular, do not ready a G), as this can deadlock
  17. // with stack shrinking.
  18. lock mutex
  19. }

图:未完待补充

4、go derfer执行顺序

 1、多个defer的执行顺序为“后进先出”;

 2、defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

我们知道defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。
与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。

5、go如何用两个协程交替打印出123456

  1. 协程1打印基数,协程2打印偶数,用一个channel实现
  2. 代码如下:
  3. package main
  4. import (
  5. "fmt"
  6. "sync"
  7. )
  8. //打印奇数
  9. func PrintOddNumber(wg *sync.WaitGroup, ch chan int, num int) {
  10. defer wg.Done()
  11. for i := 0; i <= num; i++ {
  12. ch <- i
  13. if i%2 != 0 {
  14. fmt.Println("奇数:", i)
  15. }
  16. }
  17. }
  18. //打印偶数
  19. func PrintEvenNumber(wg *sync.WaitGroup, ch chan int, num int) {
  20. defer wg.Done()
  21. for i := 1; i <= num; i++ {
  22. <-ch
  23. if i%2 == 0 {
  24. fmt.Println("偶数:", i)
  25. }
  26. }
  27. }
  28. func main() {
  29. wg := sync.WaitGroup{}
  30. ch := make(chan int)
  31. wg.Add(1)
  32. go PrintOddNumber(&wg, ch, 10)
  33. go PrintEvenNumber(&wg, ch, 10)
  34. wg.Wait()
  35. }
  36. 结果:
  37. 奇数: 1
  38. 偶数: 2
  39. 奇数: 3
  40. 偶数: 4
  41. 奇数: 5
  42. 偶数: 6
  43. 奇数: 7
  44. 偶数: 8
  45. 奇数: 9
  46. 偶数: 10

 6、go中slice和数组的区别

 1. 数组定长,定义的时候就需要确定。切片长度不定,append时会自动扩容

2. 相同大小数组可以赋值,会拷贝全部内容。slice赋值和指针一样。数组和slice之间不能相互赋值。当然slice有自己的copy函数

3. 数组也可以进行切片,返回值是一个slice,改变slice时会同步修改数组内容,相当于取得了这个数组的指针

7、go中channel在你们项目中使用的场景举例

1、消息传递、消息过滤

2、信号广播

3、事件订阅与广播

4、请求、响应转发

5、任务分发

8、go中使用chan要注意什么 

1、当需要不断从channel读取数据时,最好使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。 

  1. 例如:for x := range ch {
  2. //
  3. }

2、读已关闭的channel会造成零值 ,如果不确定channel,需要使用ok进行检测。

  1. if v, ok := <-chan;ok{
  2. ///
  3. }

3、select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的

  1. select {
  2. case <-ch:
  3. //
  4. default:
  5. //
  6. }

4、使代码更易读、更易维护,防止只读协程对通道进行写数据,但通道已关闭,造成panic。

5、有缓冲通道是异步的,无缓冲通道是同步的,有缓冲通道可供多个协程同时处理,在一定程度可提高并发性。

6、使用selecttime.After,看操作和定时器哪个先返回,处理先完成的,就达到了超时控制的效果

  1. func WorkWithOutTime(t time.Duration) (int, error) {
  2. select {
  3. case ret := <-Work():
  4. return ret, nil
  5. case <-time.After(t):
  6. return 0, errors.New("timeout")
  7. }
  8. }
  9. func Work() <-chan int {
  10. ch := make(chan int)
  11. go func() {
  12. //
  13. }()
  14. return ch
  15. }

7、是为操作加上超时的扩展,这里的操作是channel的读或写

  1. func OnlyRead(ch chan int) {
  2. select {
  3. case <-ch:
  4. //
  5. default:
  6. //
  7. }
  8. }

8、channel本质上传递的是数据的拷贝,拷贝的数据越小传输效率越高,传递结构体指针,比传递结构体更高效

9、说一下go中的CSP

CSP(communicating sequential processes)并发模型:Go语言特有且推荐使用的。

Go的CSP并发模型,是通过goroutine和channel来实现的。

不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。

普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。

Go的CSP并发模型,是通过goroutine和channel来实现的。

goroutine 是Go语言中并发的执行单位。可以理解为用户空间的线程。
channel是Go语言中不同goroutine之间的通信机制,即各个goroutine之间通信的”管道“,有点类似于Linux中的管道。 

 

M:是内核线程

P : 是调度协调,用于协调M和G的执行,内核线程只有拿到了 P才能对goroutine继续调度执行,一般都是通过限定P的个数来控制golang的并发度

G : 是待执行的goroutine,包含这个goroutine的栈空间

Gn : 灰色背景的Gn 是已经挂起的goroutine,它们被添加到了执行队列中,然后需要等待网络IO的goroutine,当P通过 epoll查询到特定的fd的时候,会重新调度起对应的,正在挂起的goroutine。

Golang为了调度的公平性,在调度器加入了steal working 算法 ,在一个P自己的执行队列,处理完之后,它会先到全局的执行队列中偷G进行处理,如果没有的话,再会到其他P的执行队列中抢G来进行处理
csp很好的例子:生产者和消费者。

10、说一下go中channel是不是线程安全的,什么情况下会变成线程不安全

 Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication),Channel也可以理解是一个先进先出的队列,通过管道进行通信。

Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的

11、说一下map是有序的吗?如何实现有序

1、由于golang map内部存储机制是以key为hash的结构来实现,所以顺序是混乱的

2、如果希望是有顺序的,可以把 key 转移至 slice,将slice 进行排序,然后输出。

  1. var keys []string
  2. for key := range maps {
  3. keys = append(keys, key)
  4. }
  5. sort.Strings(keys) //内值排序
  6. for _, key := range keys {
  7. fmt.Printf("%s:%v\n", key, maps[key])
  8. }

 12、如下程序添加一段代码让程序不报panic异常。

  1. func test() {
  2. //添加一段程序捕获panic异常
  3. }
  4. func main() {
  5. defer test()
  6. panic(1)
  7. }
  8. 答案:
  9. func test() {
  10. //添加一段程序捕获panic异常
  11. recover()
  12. }
  13. func main() {
  14. defer test()
  15. panic(1)
  16. }

13、使用go语言将数组中12300045变成12345000

  1. func MobileNnm(arr []int) []int {
  2. arr2 := make([]int, 0)
  3. oneCount := 0
  4. for _, v := range arr {
  5. if v == 0 {
  6. oneCount++
  7. } else {
  8. arr2 = append(arr2, v)
  9. }
  10. }
  11. for i := 0; i < oneCount; i++ {
  12. arr2 = append(arr2, 0)
  13. }
  14. return arr2
  15. }
  16. func main() {
  17. //使用go语言将12300045变成12345000
  18. val1 := []int{1, 2, 3, 0, 0, 0, 4, 5}
  19. val2 := MobileNnm(val1)
  20. fmt.Println(val2)
  21. }
  22. 结果:[1 2 3 4 5 0 0 0]

14、算法题

  1. 题目:1、(( 结果是false
  2. 2、(())() 是true
  3. 3、((()))())是falsego实现
  4. 思路:压栈找到匹配的出栈,找不到还放入栈中,直到栈为空,代表都匹配上了。
  5. 代码如下:(仅供参考)
  6. func main() {
  7. //题目:1、(( 结果是false
  8. //2、(())() true
  9. //3、((()))())false 用go实现
  10. s0 := []string{"(", "("}
  11. fmt.Println(IsMatch(s0))//false
  12. s1 := []string{"(", "(", ")", ")", "(", ")"}
  13. fmt.Println(IsMatch(s1)) //true
  14. s2 := []string{"(", "(", ")", ")", "(", ")", ")"}
  15. fmt.Println(IsMatch(s2))//false
  16. }
  17. func IsMatch(str []string) bool {
  18. //压栈的思想
  19. if len(str) == 0 {
  20. return false
  21. }
  22. stack := NewStack()
  23. for _, v := range str {
  24. if stack.IsEmpty() {
  25. stack.Push(v)
  26. } else {
  27. sp := stack.Pop()
  28. if sp == "(" && string(v) == ")" {
  29. continue
  30. } else {
  31. stack.Push(sp)
  32. stack.Push(v)
  33. }
  34. }
  35. }
  36. if stack.IsEmpty() {
  37. return true
  38. }
  39. return false
  40. }
  41. type Element interface{} //可存入任何类型
  42. type Stack struct {
  43. list []Element
  44. }
  45. //初始化栈
  46. func NewStack() *Stack {
  47. return &Stack{
  48. list: make([]Element, 0),
  49. }
  50. }
  51. //判断栈是否空
  52. func (s *Stack) IsEmpty() bool {
  53. if len(s.list) == 0 {
  54. return true
  55. } else {
  56. return false
  57. }
  58. }
  59. //入栈
  60. func (s *Stack) Push(x interface{}) {
  61. s.list = append(s.list, x)
  62. }
  63. //出栈
  64. func (s *Stack) Pop() Element {
  65. if len(s.list) <= 0 {
  66. fmt.Println("Stack is Empty")
  67. return nil
  68. } else {
  69. ret := s.list[len(s.list)-1]
  70. s.list = s.list[:len(s.list)-1]
  71. return ret
  72. }
  73. }

15、算法题

  1. 题目:上楼梯有一阶和两阶
  2. 例如3层楼梯有如下种
  3. 1 2 1
  4. 1 1 2
  5. 1
  6. n阶有多少种:
  7. 代码如下:(仅供参考)
  8. func main() {
  9. n := Upstairs(3)
  10. fmt.Println(n)
  11. }
  12. var mapData = make(map[int]int, 0)
  13. func Upstairs(n int) int {
  14. if n == 1 {
  15. return 1
  16. }
  17. if n == 2 {
  18. return 2
  19. }
  20. if _, ok := mapData[n]; !ok {
  21. mapData[n] = Upstairs(n-1) + Upstairs(n-2)
  22. }
  23. return mapData[n]
  24. }

16、用go实现单链表的反转

  1. 题目:单链表12345678910反转成10987654321
  2. 代码如下:(仅供参考)
  3. func main() {
  4. //实现单链表的反转例如12345678910变成10987654321
  5. var head = new(ListNode)
  6. CreateNode(head, 10)
  7. PrintNode("前:", head)
  8. yyy := reverseList(head)
  9. PrintNode("后:", yyy)
  10. }
  11. type ListNode struct {
  12. data interface{}
  13. next *ListNode
  14. }
  15. func reverseList(head *ListNode) *ListNode {
  16. cur := head
  17. var pre *ListNode
  18. for cur != nil {
  19. cur.next, pre, cur = pre, cur, cur.next
  20. }
  21. return pre
  22. }
  23. func CreateNode(node *ListNode, max int) {
  24. cur := node
  25. for i := 1; i <= max; i++ {
  26. cur.next = &ListNode{}
  27. cur.data = i
  28. cur = cur.next
  29. }
  30. }
  31. //打印链表的方法
  32. func PrintNode(str string, node *ListNode) {
  33. fmt.Print(str)
  34. for cur := node; cur != nil; cur = cur.next {
  35. if cur.data != nil {
  36. fmt.Print(cur.data, " ")
  37. }
  38. }
  39. fmt.Println()
  40. }
  41. 结果:
  42. 前:1 2 3 4 5 6 7 8 9 10
  43. 后:10 9 8 7 6 5 4 3 2 1

17、算法题(在线求答案)

  1. 题目:
  2. 输入文件构成规则如下:
  3. 1. 每行代表一条记录,字段之间以逗号(,)分隔
  4. 2. 若字段内容包含逗号(,),则以双引号包围该字段
  5. 3. 若字段内容包含双引号("),则以双引号包围该字段,字段内的双引号由一个变两个
  6. 请参照上面三条规则,编写一个解析程序,将解析后的记录内容按行输出,字段之间以TAB(\t)分隔,2小时内完成
  7. 示例:
  8. John,33,"足球,摄影",New York
  9. John,33,"足球,""摄影",New York
  10. 输出:
  11. John 33 足球,摄影 New York
  12. John 33 足球,"摄影 New York
  13. 输入:
  14. 2,John,45,"足球,摄影",New York
  15. 3,Carter Job,33,"""健身"",远足","河北,石家庄"
  16. 4,Steve,33,"大屏幕164""","DC""Home"""
  17. 5,"Jul,y",33,Football,Canada
  18. 求输出!
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. "strings"
  7. )
  8. func parseRecord(line string) string {
  9. var fields []string
  10. var inQuote bool
  11. var field string
  12. for i := 0; i < len(line); i++ {
  13. c := line[i]
  14. if c == ',' && !inQuote {
  15. fields = append(fields, field)
  16. field = ""
  17. } else if c == '"' {
  18. inQuote = !inQuote
  19. if i > 0 && line[i-1] == '"' {
  20. field += "\""
  21. }
  22. } else {
  23. field += string(c)
  24. }
  25. }
  26. fields = append(fields, field)
  27. return strings.Join(fields, "\t")
  28. }
  29. func main() {
  30. scanner := bufio.NewScanner(os.Stdin)
  31. for scanner.Scan() {
  32. line := scanner.Text()
  33. fmt.Println(parseRecord(line))
  34. }
  35. if err := scanner.Err(); err != nil {
  36. fmt.Fprintln(os.Stderr, "reading standard input:", err)
  37. }
  38. }

18、slice和map区别代码输出题

  1. 题目一:
  2. func main() {
  3. s1 := []int{1, 2, 3, 4, 5}
  4. changeslice(s1)
  5. fmt.Println(s1)//?s1的结果会变化吗?
  6. fmt.Println(len(s1), cap(s1))//?s1长度和容量会变化吗?
  7. m1 := map[int]int{1: 1, 2: 2, 3: 3, 4: 4}
  8. changeMap(m1)
  9. fmt.Println(m1) //?m1中key等于1的值会变化吗?
  10. fmt.Println(len(m1))//?m1长度和容量会变化吗?
  11. }
  12. func changeslice(s []int) {
  13. s[0] = 2
  14. s = append(s, 7, 8, 9, 10)
  15. }
  16. func changeMap(m map[int]int) {
  17. m[1] = 2
  18. m[5] = 5
  19. m[6] = 6
  20. }
  21. 答案:s1的值会变化,但是长度和容量不会变化。m1的值会变化,长度会变化。
  22. 切记:
  23. map 没有容量限制,所以内置函数 cap 也不接受 map 类型
  24. 题目二:
  25. func main() {
  26. m := make(map[int]int)
  27. mdMap(m)
  28. fmt.Println(m) //输出结果
  29. }
  30. func mdMap(m map[int]int) {
  31. m[1] = 100
  32. m[2] = 200
  33. }
  34. ---------------------------------
  35. func main() {
  36. var m1 map[int]int
  37. mdMap(m1)
  38. fmt.Println(m1)//输出的结果
  39. }
  40. func mdMap(m map[int]int) {
  41. m = make(map[int]int)
  42. m[1] = 100
  43. m[2] = 200
  44. }
  45. 答案:m结果map[2:200 1:100] m1的结果map[]

19、mysql有那几种存储引擎

MyISAM:

创建一个myisam存储引擎的表的时候回出现三个文件

1.tb_demo.frm,存储表定义;  2.tb_demo.MYD,存储数据;  3.tb_demo.MYI,存储索引。

MyISAM表无法处理事务,这就意味着有事务处理需求的表,不能使用MyISAM存储引擎。

MyISAM存储引擎特别适合在以下几种情况下使用:

1.选择密集型的表。MyISAM存储引擎在筛选大量数据时非常迅速,这是它最突出的优点。

2.插入密集型的表。MyISAM的并发插入特性允许同时选择和插入数据。例如:MyISAM存储引擎很适合管理邮件或Web服务器日志数据。

InnoDB:

InnoDB是一个健壮的事务型存储引擎MySQL 5.6.版本以后InnoDB就是作为默认的存储引擎。

InnoDB还引入了行级锁定和外键约束,在以下场合下,使用InnoDB是最理想的选择:

1. 更新密集的表。InnoDB存储引擎特别适合处理多重并发的更新请求。

2.事务。InnoDB存储引擎是支持事务的标准MySQL存储引擎。

3.自动灾难恢复。与其它存储引擎不同,InnoDB表能够自动从灾难中恢复。

4.外键约束。MySQL支持外键的存储引擎只有InnoDB。

5.支持自动增加列AUTO_INCREMENT属性。

MEMORY:

使用MySQL Memory存储引擎的出发点是速度。为得到最快的响应时间,采用的逻辑存储介质是系统内存。虽然在内存中存储表数据确实会提供很高的性能,但当mysqld守护进程崩溃时,所有的Memory数据都会丢失。获得速度的同时也带来了一些缺陷。它要求存储在Memory数据表里的数据使用的是长度不变的格式,这意味着不能使用BLOB和TEXT这样的长度可变的数据类型,VARCHAR是一种长度可变的类型,但因为它在MySQL内部当做长度固定不变的CHAR类型,所以可以使用。

一般在以下几种情况下使用Memory存储引擎:

1.目标数据较小,而且被非常频繁地访问。在内存中存放数据,所以会造成内存的使用,可以通过参数max_heap_table_size控制Memory表的大小,设置此参数,就可以限制Memory表的最大大小。

2.如果数据是临时的,而且要求必须立即可用,那么就可以存放在内存表中。

3.存储在Memory表中的数据如果突然丢失,不会对应用服务产生实质的负面影响。Memory同时支持散列索引和B树索引。B树索引的优于散列索引的是,可以使用部分查询和通配查询,也可以使用<、>和>=等操作符方便数据挖掘。散列索引进行“相等比较”非常快,但是对“范围比较”的速度就慢多了,因此散列索引值适合使用在=和<>的操作符中,不适合在<或>操作符中,也同样不适合用在order by子句中

MERGE:

MERGE存储引擎是一组MyISAM表的组合,这些MyISAM表结构必须完全相同,尽管其使用不如其它引擎突出,但是在某些情况下非常有用。说白了,Merge表就是几个相同MyISAM表的聚合器;Merge表中并没有数据,对Merge类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行操作。Merge存储引擎的使用场景。对于服务器日志这种信息,一般常用的存储策略是将数据分成很多表,每个名称与特定的时间端相关。例如:可以用12个相同的表来存储服务器日志数据,每个表用对应各个月份的名字来命名。当有必要基于所有12个日志表的数据来生成报表,这意味着需要编写并更新多表查询,以反映这些表中的信息。与其编写这些可能出现错误的查询,不如将这些表合并起来使用一条查询,之后再删除Merge表,而不影响原来的数据,删除Merge表只是删除Merge表的定义,对内部的表没有任何影响。

ARCHIVE:

rchive是归档的意思,在归档之后很多的高级功能就不再支持了,仅仅支持最基本的插入和查询两种功能。在MySQL 5.5版以前,Archive是不支持索引,但是在MySQL 5.5以后的版本中就开始支持索引了。Archive拥有很好的压缩机制,它使用zlib压缩库,在记录被请求时会实时压缩,所以它经常被用来当做仓库使用。

20、mysql中事务隔离级别有哪几种

  • 读未提交(READ UNCOMITTED)
  • 读提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 串行化(SERIALIZABLE)
  • 隔离级别脏读不可重复读幻读
    READ UNCOMITTED
    READ COMMITTED×
    REPEATABLE READ××
    SERIALIZABLE×××
    mysql数据库事务的隔离级别有4个,而默认的事务处理级别就是【REPEATABLE-READ】,也就是可重复读

21、 B树、B+tree、Hash有什么区别

B树是一种多路自平衡搜索树,它类似普通的二叉树,但是B书允许每个节点有更多的子节点。B树示意图如下:

B树的特点:

  1. 所有键值分布在整个树中
  2. 任何关键字出现且只出现在一个节点中
  3. 搜索有可能在非叶子节点结束
  4. 在关键字全集内做一次查找,性能逼近二分查找算法
  5. 树深度会很深,因为树顶放到元素比较少导致,检索元素比较慢。

 缺点:

       业务数据的大小可能远远超过了索引数据的大小,每次为了查找对比计算,需要把数据加载到内存以及 CPU 高速缓存中时,都要把索引数据和无关的业务数据全部查出来。本来一次就可以把所有索引数据加载进来,现在却要多次才能加载完。如果所对比的节点不是所查的数据,那么这些加载进内存的业务数据就毫无用处,全部抛弃

 B+Tree: 

  

从图中也可以看到,B+树与B树的不同在于:

  1. 所有关键字存储在叶子节点,非叶子节点不存储真正的data
  2. 为所有叶子节点增加了一个链指针
  3. 树顶可以放很多元素,树的深度比较矮,检索元素比较快。

 缺点:

       仍然有一个致命的缺陷,那就是它的索引数据与业务绑定在一块,而业务数据的大小很有可能远远超过了索引数据,这会大大减小一次 I/O 有用数据的获取,间接的增加 I/O 次数去获取有用的索引数据 

Hash:

  

特点:数组+链表

          1、查询单条数据很快,先解析出hash值,根据hash找到链表,然后找到索引最后根据索引找到数据。

  缺点:

  1.  容易hash碰撞
  2. Hash索引仅仅能满足“=”,“IN”,“<=>”查询,不能使用范围查询
  3. 联合索引中,Hash索引不能利用部分索引键查询。
  4. Hash索引无法避免数据的排序操作
  5. Hash索引任何时候都不能避免表扫描
  6. Hash索引遇到大量Hash值相等的情况后性能会下降

22、 Mysql 中 MyISAM 和 InnoDB 的区别有哪些?

  1.  InnoDB支持事务,MyISAM不支持
  2. 对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
  3. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
  4. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。
  5. 但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此主键不应该过大,因为主键太大,其他索引也都会很大。
  6. 而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  7. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  8. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高 

23、 go向关闭的channel发送和读取数据是否报错

  1. package main
  2. import "fmt"
  3. //向已关闭的通道读取数不会报错
  4. func main1() {
  5. var ch = make(chan int)
  6. go func() {
  7. close(ch)
  8. }()
  9. fmt.Println(<-ch)
  10. }
  11. //向已关闭的通道发送数据报panic: send on closed channel
  12. func main2() {
  13. var ch = make(chan int)
  14. go func() {
  15. close(ch)
  16. }()
  17. ch <- 1
  18. }
  19. //关闭通道向有缓存区接收数据会报错
  20. func main3() {
  21. var ch = make(chan int, 10)
  22. go func() {
  23. close(ch)
  24. }()
  25. fmt.Println(<-ch)
  26. }
  27. //关闭通道向有缓冲区发送数据会报错panic:send on closed channel
  28. func main4() {
  29. var ch = make(chan int, 10)
  30. go func() {
  31. close(ch)
  32. }()
  33. ch <- 1
  34. }
  35. //关闭通道向有缓冲区循环发送数据会报错 panic: send on closed channel
  36. func main() {
  37. var ch = make(chan int, 10)
  38. go func() {
  39. close(ch)
  40. }()
  41. for {
  42. ch <- 1
  43. }
  44. }

24、Golang并发模型有几种

控制并发有三种种经典的方式,一种是通过channel通知实现并发控制 一种是WaitGroup,另外一种就是Context。

1、无缓冲通道

无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。

从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。

正式通过无缓冲通道来实现多 goroutine 并发控制

  1. func main() {
  2. ch := make(chan instruct{})
  3. go func() {
  4. ch <- struct{}{}
  5. }()
  6. fmt.Println(<-ch)
  7. }

当主 goroutine 运行到 <-ch 接受 channel 的值的时候,如果该 channel 中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制

2. 通过sync包中的WaitGroup实现并发控制

在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成,在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。

  1. func main() {
  2. var wg sync.WaitGroup
  3. // 开N个后台打印线程
  4. for i := 0; i < 10; i++ {
  5. wg.Add(1)
  6. go func() {
  7. fmt.Println("你好, 世界")
  8. wg.Done()
  9. }()
  10. }
  11. // 等待N个后台线程完成
  12. wg.Wait()
  13. }

3. 在Go 1.7 以后引进的强大的Context上下文,实现并发控制

3.1 简介

在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。

3.2 package context

context 包的核心是 struct Context,接口声明如下:

  1. // A Context carries a deadline, cancelation signal, and request-scoped values
  2. // across API boundaries. Its methods are safe for simultaneous use by multiple
  3. // goroutines.
  4. type Context interface {
  5. // Done returns a channel that is closed when this `Context` is canceled
  6. // or times out.
  7. Done() <-chan struct{}
  8. // Err indicates why this Context was canceled, after the Done channel
  9. // is closed.
  10. Err() error
  11. // Deadline returns the time when this Context will be canceled, if any.
  12. Deadline() (deadline time.Time, ok bool)
  13. // Value returns the value associated with key or nil if none.
  14. Value(key interface{}) interface{}
  15. }
  • Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
  • Err() 在Done() 之后,返回context 取消的原因。
  • Deadline() 设置该context cancel的时间点
  • Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。

Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。

一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。
背后的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。
一个典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。

3.4 context例子

当然,想要知道 Context 包是如何工作的,最好的方法是看一个例子。

  1. func childFunc(cont context.Context, num *int) {
  2. ctx, _ := context.WithCancel(cont)
  3. for {
  4. select {
  5. case <-ctx.Done():
  6. fmt.Println("child Done : ", ctx.Err())
  7. return
  8. }
  9. }
  10. }
  11. func main() {
  12. gen := func(ctx context.Context) <-chan int {
  13. dst := make(chan int)
  14. n := 1
  15. go func() {
  16. for {
  17. select {
  18. case <-ctx.Done():
  19. fmt.Println("parent Done : ", ctx.Err())
  20. return // returning not to leak the goroutine
  21. case dst <- n:
  22. n++
  23. go childFunc(ctx, &n)
  24. }
  25. }
  26. }()
  27. return dst
  28. }
  29. ctx, cancel := context.WithCancel(context.Background())
  30. for n := range gen(ctx) {
  31. fmt.Println(n)
  32. if n >= 5 {
  33. break
  34. }
  35. }
  36. cancel()
  37. time.Sleep(5 * time.Second)
  38. }

在上面的例子中,主要描述的是通过一个channel实现一个为循环次数为5的循环,
在每一个循环中产生一个goroutine,每一个goroutine中都传入context,在每个goroutine中通过传入ctx创建一个Context,并且通过select一直监控该Context的运行情况,当在父Context退出的时候,代码中并没有明显调用子ContextCancel函数,但是分析结果,子Context还是被正确合理的关闭了,这是因为,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

3.5 Context 使用原则

  • 不要把Context放在结构体中,要以参数的方式传递
  • Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  • ContextValue相关方法应该传递必须的数据,不要什么数据都使用这个传递
  • Context是线程安全的,可以放心的在多个goroutine中传递

 25、go分布式锁有几种

1 、进程内加锁

  1. 想要得到正确的结果的话,要把对计数器(counter)的操作代码部分加上锁:
  2. // ... 省略之前部分
  3. var wg sync.WaitGroup
  4. var l sync.Mutex
  5. for i := 0; i < 1000; i++ {
  6. wg.Add(1)
  7. go func () {
  8. defer wg.Done()
  9. l.Lock()
  10. counter++
  11. l.Unlock()
  12. }()
  13. }
  14. wg.Wait()
  15. println(counter)
  16. // ... 省略之后部分
  17. 这样就可以稳定地得到计算结果了:
  18. ❯❯❯ go run local_lock.go
  19. 1000

2、trylock

在某些场景,我们只是希望一个任务有单一的执行者。而不像计数器场景一样,所有goroutine都执行
成功。后来的goroutine在抢锁失败后,需要放弃其流程。这时候就需要trylock了。
trylock顾名思义,尝试加锁,加锁成功执行后续流程,如果加锁失败的话也不会阻塞,而会直接返回
加锁的结果。在Go语言中我们可以用大小为1的Channel来模拟trylock:

  1. package main
  2. import (
  3. "sync"
  4. )
  5. type Lock struct {
  6. c chan struct{}
  7. }
  8. // NewLock generate a try lock
  9. func NewLock() Lock {
  10. var l Lock
  11. l.c = make(chan struct{}, 1)
  12. l.c <- struct{}{}
  13. return l
  14. }
  15. // Lock try lock, return lock result
  16. func (l Lock) Lock() bool {
  17. lockResult := false
  18. select {
  19. case <-l.c:
  20. lockResult = true
  21. default:
  22. }
  23. return lockResult
  24. }
  25. // Unlock , Unlock the try lock
  26. func (l Lock) Unlock() {
  27. l.c <- struct{}{}
  28. }

因为我们的逻辑限定每个goroutine只有成功执行了 Lock  才会继续执行后续逻辑,因此
在 Unlock  时可以保证Lock结构体中的channel一定是空,从而不会阻塞,也不会失败。上面的代
码使用了大小为1的channel来模拟trylock,理论上还可以使用标准库中的CAS来实现相同的功能且
成本更低,读者可以自行尝试。
在单机系统中,trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源
浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁,而非执行任务上,从而程序整体
的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。

3、基于Redis的setnx

在分布式场景下,我们也需要这种“抢占”的逻辑,这时候怎么办呢?我们可以使用Redis提供
的 setnx  命令:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. "github.com/go-redis/redis"
  7. )
  8. func incr() {
  9. client := redis.NewClient(&redis.Options{
  10. Addr: "localhost:6379",
  11. Password: "", // no password set
  12. DB: 0, // use default DB
  13. })
  14. var lockKey = "counter_lock"
  15. var counterKey = "counter"
  16. // lock
  17. resp := client.SetNX(lockKey, 1, time.Second*5)
  18. lockSuccess, err := resp.Result()
  19. if err != nil || !lockSuccess {
  20. fmt.Println(err, "lock result: ", lockSuccess)
  21. return
  22. }
  23. // counter ++
  24. getResp := client.Get(counterKey)
  25. cntValue, err := getResp.Int64()
  26. if err == nil {
  27. cntValue++
  28. resp := client.Set(counterKey, cntValue, 0)
  29. _, err := resp.Result()
  30. if err != nil {
  31. // log err
  32. println("set value error!")
  33. }
  34. }
  35. println("current counter is ", cntValue)
  36. delResp := client.Del(lockKey)
  37. unlockSuccess, err := delResp.Result()
  38. if err == nil && unlockSuccess > 0 {
  39. println("unlock success!")
  40. } else {
  41. println("unlock failed", err)
  42. }
  43. }
  44. func main() {
  45. var wg sync.WaitGroup
  46. for i := 0; i < 10; i++ {
  47. wg.Add(1)
  48. go func() {
  49. defer wg.Done()
  50. incr()
  51. }()
  52. }
  53. wg.Wait()
  54. }
  55. 看看运行结果:
  56. 1. ❯❯❯ go run redis_setnx.go
  57. 2. <nil> lock result: false
  58. 3. <nil> lock result: false
  59. 4. <nil> lock result: false
  60. 5. <nil> lock result: false
  61. 6. <nil> lock result: false
  62. 7. <nil> lock result: false
  63. 8. <nil> lock result: false
  64. 9. <nil> lock result: false
  65. 10. <nil> lock result: false
  66. 11. current counter is 2028
  67. 12. unlock success!

通过代码和执行结果可以看到,我们远程调用 setnx  实际上和单机的trylock非常相似,如果获取
锁失败,那么相关的任务逻辑就不应该继续向前执行。
setnx  很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,
而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户
设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是
我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。
所以,我们需要依赖于这些请求到达Redis节点的顺序来做正确的抢锁操作。如果用户的网络环境比较
差,那也只能自求多福了。

4、基于ZooKeeper

  1. package main
  2. import (
  3. "time"
  4. "github.com/samuel/go-zookeeper/zk"
  5. )
  6. func main() {
  7. c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
  8. if err != nil {
  9. panic(err)
  10. }
  11. l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll))
  12. err = l.Lock()
  13. if err != nil {
  14. panic(err)
  15. }
  16. println("lock succ, do your business logic")
  17. time.Sleep(time.Second * 10)
  18. // do some thing
  19. l.Unlock()
  20. println("unlock succ, finish business logic")
  21. }

基于ZooKeeper的锁与基于Redis的锁的不同之处在于Lock成功之前会一直阻塞,这与我们单机场景
中的 mutex.Lock  很相似。
其原理也是基于临时Sequence节点和watch API,例如我们这里使用的是 /lock  节点。Lock会在
该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有watch该节点的程
序。这时候程序会检查当前节点下最小的子节点的id是否与自己的一致。如果一致,说明加锁成功了。
这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。按照
Google的Chubby论文里的阐述,基于强一致协议的锁适用于 粗粒度  的加锁操作。这里的粗粒度指
锁占用时间较长。我们在使用时也应思考在自己的业务场景中使用是否合适。

5、基于etcd

etcd是分布式系统中,功能上与ZooKeeper类似的组件,这两年越来越火了。上面基于ZooKeeper我
们实现了分布式阻塞锁,基于etcd,也可以实现类似的功能:

  1. package main
  2. import (
  3. "log"
  4. "github.com/zieckey/etcdsync"
  5. )
  6. func main() {
  7. m, err := etcdsync.New("/lock", 10, []string{"http://127.0.0.1:2379"})
  8. if m == nil || err != nil {
  9. log.Printf("etcdsync.New failed")
  10. return
  11. }
  12. err = m.Lock()
  13. if err != nil {
  14. log.Printf("etcdsync.Lock failed")
  15. return
  16. }
  17. log.Printf("etcdsync.Lock OK")
  18. log.Printf("Get the lock. Do something here.")
  19. err = m.Unlock()
  20. if err != nil {
  21. log.Printf("etcdsync.Unlock failed")
  22. } else {
  23. log.Printf("etcdsync.Unlock OK")
  24. }
  25. }

etcd中没有像ZooKeeper那样的Sequence节点。所以其锁实现和基于ZooKeeper实现的有所不同。
在上述示例代码中使用的etcdsync的Lock流程是:
1. 先检查 /lock  路径下是否有值,如果有值,说明锁已经被别人抢了
2. 如果没有值,那么写入自己的值。写入成功返回,说明加锁成功。写入时如果节点被其它节点写
入过了,那么会导致加锁失败,这时候到 3
3. watch  /lock  下的事件,此时陷入阻塞
4. 当 /lock  路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有
者主动unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。

26、定时器的实现原理

 1、 时间堆

最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见图6-4

 小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所
有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复
杂度是 O(1)  。
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常
的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是 O(LgN)  。
Go自身的内置定时器就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆
在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样:

 2、时间轮

 用时间轮来实现定时器时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有
秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的任务列表是否有已经到期的
任务。
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那
么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。

3、任务分发

 

 每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那
些 task_id % shard_count = shard_id  的那些任务即可。
当这些定时任务被触发之后需要通知用户侧,有两种思路:
1. 将任务被触发的信息封装为一条消息,发往消息队列,由用户侧对消息队列进行监听。
2. 对用户预先配置的回调函数进行调用。

两种方案各有优缺点,如果采用1,那么如果消息队列出故障会导致整个系统不可用,当然,现在的消
息队列一般也会有自身的高可用方案,大多数时候我们不用担心这个问题。其次一般业务流程中间走消
息队列的话会导致延时增加,定时任务若必须在触发后的几十毫秒到几百毫秒内完成,那么采用消息队列就会有一定的风险。如果采用2,会加重定时任务系统的负担。我们知道,单机的定时器执行时最害
怕的就是回调函数执行时间过长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用
的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上
增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。

27、负载均衡有几种方式

如果我们不考虑均衡的话,现在有n个服务节点,我们完成业务流程实际上只需要从这n个中挑出其中的
一个。有几种思路:
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,
那么下一次从第一台开始。这种情况下我们可以把服务节点信息都存储在数组中,每次请求完成
下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
2. 随机挑一个: 每次都随机挑,真随机伪随机均可。假设选择第 x 台机器,那么x可描述
为 rand.Intn()%n  。
3. 根据某种权重,对下游节点进行排序,选择权重最大/小的那一个。
当然了,实际场景我们不可能无脑轮询或者无脑随机,如果对下游请求失败了,我们还需要某种机制来
进行重试,如果纯粹的随机算法,存在一定的可能性使你在下一次仍然随机到这次的问题节点。 

4、基于洗牌算法的负载均衡

在Go的标准库中实际上已经为我们内置了该算法:
 

  1.  func shuffle(n int) []int {
  2.   b := rand.Perm(n)
  3.   return b
  4.  }

5、ZooKeeper 集群的随机节点挑选问题

本节中的场景是从N个节点中选择一个节点发送请求,初始请求结束之后,后续的请求会重新对数组洗
牌,所以每两个请求之间没有什么关联关系。因此我们上面的洗牌算法,理论上不初始化随机库的种子
也是不会出什么问题的。
但在一些特殊的场景下,例如使用ZooKeeper时,客户端初始化从多个服务节点中挑选一个节点后,是
会向该节点建立长连接的。之后客户端请求都会发往该节点去。直到该节点不可用,才会在节点列表中
挑选下一个节点。在这种场景下,我们的初始连接节点选择就要求必须是“真”随机了。否则,所有客户
端起动时,都会去连接同一个ZooKeeper的实例,根本无法起到负载均衡的目的。如果在日常开发中,你的业务也是类似的场景,也务必考虑一下是否会发生类似的情况。为rand库设置种子的方法:
  rand.Seed(time.Now().UnixNano())

由于篇幅太长转到第二篇文章

地址:https://blog.csdn.net/IT_ziliang/article/details/123600841

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/89184
推荐阅读
相关标签
  

闽ICP备14008679号