GO知识点总结

一、切片(slice)

1.slice的追加

使用append向Slice追加元素时, 如果Slice空间不足, 将会触发Slice扩容, 扩容实际上重新一配一块更大的内存, 将原Slice数据拷贝进新Slice, 然后返回新Slice, 扩容后再将数据追加进去。
扩容容量的选择遵循以下规则:

  • 如果原Slice容量小于1024, 则新Slice容量将扩大为原来的2倍
  • 如果原Slice容量大于等于1024, 则新Slice容量将扩大为原来的1.25倍

2.slice的拷贝

使用copy()内置函数拷贝两个切片,但是需要注意的是,copy 会将源切片的数据逐个拷贝到目的切片指向的数组中, 拷贝数量取两个切片长度的最小值。copy不会扩容,只有append才会扩容。
基于以上切片特性。编程过程需要注意:

  • 创建切片时可跟据实际需要预分配容量, 尽量避免追加过程中扩容操作, 有利于提升性能;
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组, 以防读写冲突

3. 数组与切片的区别?

  • 切片是指针类型,数组是值类型
  • 数组的长度是固定的,而切片不是(切片是动态的数组)
  • 切片的底层是数组。切片可以通过数组来初始化,也可以通过内置函数make()初始化。初始化时len=cap,在追加元素时如果容量cap不足时将进行扩容。如果原Slice容量小于1024, 则新Slice容量将扩大为原来的2倍;
    如果原Slice容量大于等于1024, 则新Slice容量将扩大为原来的1.25倍。

4. cap()和len()函数的区别?

len() 可以用来查看数组或slice的长度
cap()可以用来查看数组或slice的容量
在数组中由于长度固定不可变,因此len(arr)和cap(arr)的输出永远相同
在slice中,len(sli)表示可见元素有几个(也即直接打印元素看到的元素个数),而cap(sli)表示所有元素有几个

二、map

map底层使用哈希表来实现的,哈希过程产生冲突使用的冲突解决办法是链地址法。

1.map解决冲突的方法

使用链地址法:当多个键被哈希到了同一个bucket时,也就是产生了哈希冲突。由于每个bucket可以存放8个键值对, 所以同一个bucket存放超过8个键值对时就会创建一个桶, 用链表的方式将bucket关联起来。

2.map的扩容

因为不能放任它无休止的冲突下去,无休止冲突的话会影响读写性能,于是引入了负载因子的概念,计算方式为:
负载因子=键数量/桶数量,当负载因子达到指定的值就会进行扩容操作。
go语言中的哈希表触发扩容的条件有两个:

  • 负载因子 > 6.5时, 也即平均每个bucket存储的键值对达到6.5个
  • overflow数量 > 2^15时, 也即overflow数量超过32768时

第一种情况负载因子过大,使用增量扩容。
当负载因子过大时, 就新建一个bucket, 新的bucket长度是原来的2倍, 然后旧bucket数据搬迁到新的bucket。
考虑到如果map存储了数以亿计的key-value, 一次性搬迁将会造成比较大的延时, Go采用逐步搬迁策略, 即每次访问map时都会触发一次搬迁, 每次搬迁2个键值对。
第二种overflow数量过多,使用等量扩容。
所谓等量扩容, 实际上并不是扩大容量, buckets数量不变, 重新做一遍类似增量扩容的搬迁动作, 把松散的键值对重新排列一次, 以使bucket的使用率更高, 进而保证更快的存取。 在极端场景下, 比如不断的增删, 而键值对正好集中在一小部分的bucket, 这样会造成overflow的bucket数量增多, 但负载因子又不高, 从而无法执行增量搬迁的情况。

3.map并发

go语言的map数据结构并不是并发安全的。想要并发安全的使用map结构。
通常由几种方式:

  • 为map加读写锁
  • 使用concurrent-map(开源库)
  • 使用sync.map
    三者的区别在于第一种是为整个map加锁,加锁粒度较大。影响性能。
    第二个使用开源库concurrent-map,原理是对map分段加锁。加锁粒度相对减少,性能相对第一个有所提高。
    第三种是go1.9引入的官方库,使用了空间换时间策略,通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。

4. map如何顺序读取?

可以通过sort中的排序包进行对map中的key进行排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    var m = map[string]int{
        "hello":         0,
        "morning":       1,
        "my":            2,
        "girl":   		3,
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println("Key:", k, "Value:", m[k])
    }
}

5. go如何实现哈希映射和set?

根据go中map的keys的无序性和唯一性,可以将其作为set;
哈希表在Golang中相当于map,也就是哈希映射。

三、channel

channel是go语言协程间通信的管道。channel可用于协程同步,也可以协程间可以传递各种消息数据。

1. 什么是channel,为什么它可以做到线程安全?

Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯
(communication),Channel也可以理解是一个先进先出的队列,通过管道进行通信。
Golang的Channel,发送一个数据到Channel和从Channel接收一个数据都是原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的。

2.向channel写数据的过程

如果等待接收队列recvq不为空, 说明缓冲区中没有数据或者没有缓冲区, 此时直接从recvq取出G,并把数据写入, 最后把该G唤醒, 结束发送过程;
如果接受队列recvq为空,且缓冲区中有空余位置, 将数据写入缓冲区, 结束发送过程;
如果接受队列recvq为空,缓冲区中没有空余位置, 将待发送数据写入G, 将当前G加入sendq, 进入睡眠, 等待被读goroutine唤醒。

3.从一个channel读数据过程

如果等待发送队列sendq不为空, 且没有缓冲区, 直接从sendq中取出G, 把G中数据读出, 最后把G唤醒, 结束读取过程;
如果等待发送队列sendq不为空, 此时说明缓冲区已满, 从缓冲区中首部读出数据, 把G中数据写入缓冲区尾部, 把G唤醒, 结束读取过程;
如果缓冲区中有数据, 则从缓冲区取出数据, 结束读取过程;
将当前goroutine加入recvq, 进入睡眠, 等待被写goroutine唤醒;

4. 无缓冲 Chan 的发送和接收是否同步?

ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.
ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.
channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。

5. go语言的channel特性

  • 给一个 nil channel 发送数据,造成永远阻塞
  • 从一个 nil channel 接收数据,造成永远阻塞
  • 给一个已经关闭的 channel 发送数据,引起 panic
  • 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  • 无缓冲的channel是同步的,而有缓冲的channel是非同步的

6.防止channel超时机制

有时候会出现协程阻塞的情况,可以使用select来设置超时

func main() {
   c := make(chan int)
   o := make(chan bool)
   go func() {
      for {
         select {
         case v:= <-c:
            fmt.Println(v)
         //5秒钟自动关闭,避免长时间超时
         case <-time.After(5 * time.Second):
            fmt.Println("timeout")
            o<-true
            break
         }
      }
   }()
  //有值就主协程走,主协程走完就都没了
   <-o
   fmt.Println("程序结束")
}

四、其他

1. 解释以下命令的作用?

go env: #用于查看go的环境变量
go run: #用于编译并运行go源码文件
go build: #用于编译源码文件、代码包、依赖包
go get: #用于动态获取远程代码包
go install: #用于编译go文件,并将编译结构安装到bin、pkg目录
go clean: #用于清理工作目录,删除编译和安装遗留的目标文件
go version: #用于查看go的版本信息

2.select是怎么执行的?

  • select中的case语句必须是一个channel操作
  • select中的default子句总是可运行的,速度非常快。
  • 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
  • 如果没有可运行的case语句,且有default语句,处于非阻塞状态,那么就会执行default的动作。
  • 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行。

3.Go语言局部变量分配在栈还是堆?

局部变量的作用域是否逃出函数的作用域,要是没有,那么就放在栈上;
要是变量的作用域超出了函数的作用域,那么就自动放在堆上

4. make 和 new 的区别?

new:它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。
make:只用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型。

5. 说说go语言的main函数

  • main函数不能带参数
  • main函数不能定义返回值
  • main函数所在的包必须为main包
  • main函数中可以使用flag包来获取和解析命令行参数

6. JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?

首先JSON 标准库对 nil slice 和 空 slice 的处理是不一致.
通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。
var slice []int
slice[1] = 0
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:
slice := make([]int,0)
slice := []int{}
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。

7. struct能不能比较?

相同struct类型的可以比较;
不同struct类型的不可以比较,编译都不过,类型不匹配
因为是强类型语言,所以不同类型的结构不能作比较,但是同一类型的实例值是可以比较的,实例不可以比较,因为是指针类型

8. 什么是interface?

接口定义了对象的行为,在Go中,接口声明的是一组方法的集合。
interface{} 类型,空接口,所有类型都实现了空接口。如果一个函数以 interface{} 值作为参数,那么可以为该函数提供任何值。

9. context包的用途是什么?

Context通常被译作上下文,它是一个比较抽象的概念,
其本质,是【上下上下】存在上下层的传递,上会把内容传递给下。
在Go语言中,程序单元也就指的是Goroutine
context用于停止goroutine,协调多个goroutine的取消,设置超时取消等等。
停止goroutine的基本原理: 使用一个channel,在多个goroutine中使用select从channel中取值,context中实际上并不会放任何值到channel中而是关闭channel,这样就channel就不会阻塞,select就能走到case <-ctx.Done()这个分支。

//不停的将拿到的返回值v放到out这个channel中,直到发生错误或者收到取消信号
 func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        }
    }
 }

10. 数据共享

数据共享方式:通过通道共享、共享变量加锁

11. defer

defer函数属延迟执行,延迟到调用者函数执行 return 命令前被执行。多个defer之间按LIFO先进后出顺序执行。
return 会做几件事:

  • 给返回值赋值
  • 调用 defer 表达式
  • 返回给调用函数

若 defer 表达式有返回值,将会被丢弃。
在实际开发中,defer 的使用经常伴随着闭包与匿名函数的使用。

12. Printf()、Sprintf()、Fprintf()函数的区别?

1.printf()是把格式化字符串输出到标准输出(一般是屏幕).
2.sprintf()是把格式化字符串输出到指定字符串,所以参数比printf多了个char * ,那就是目标字符串的地址.
3.fprintf()是把格式化字符串输出到指定文件中,所以参数比printf多了个文件指针File * ,那是目标文件的文件描述符(文件流指针)

13. go语言中的for循环?

for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环
for循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量

14.go语言中的switch语句?

单个case中,可以出现多个结果选项
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case

15. go语言中没有隐藏的this指针,这句话是什么意思?

  • 方法施加的对象显式传递,没有被隐藏起来
  • golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达
  • 方法施加的对象不需要非得是指针,也不用非得叫this

16. go语言中的引用类型包含哪些?

指针、slice、map 、chan 、interface

17. go语言中指针运算有哪些?

通过“&”取指针的地址
通过“*”取指针指向的数据

18. go语言触发异常的场景有哪些?

空指针解析
下标越界
除数为0
调用panic函数

19. Go是否支持泛型?

不支持。泛型是方便的,但是它们在类型系统和运行时的复杂性方面付出了代价。
Go语言的解决方案就是使用interface{}替代任意类型,简单有效;
泛型
只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。
泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型
泛型的优点

  • 代码更加简洁【不用强制转换】
  • 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
  • 可读性和稳定性【在编写集合的时候,就限定了类型】

20. 如何获取 go 程序运行时的协程数量, gc 时间, 对象数, 堆栈信息?

调用接口 runtime.ReadMemStats 可以获取以上所有信息,

注意: 调用此接口会触发 STW(Stop The World)

https://golang.org/pkg/runtime/#ReadMemStats

如果需要打入到日志系统, 可以使用 go 封装好的包, 输出 json 格式.

https://golang.org/pkg/expvar/

21.主协程如何等其余协程完再操作?

使用channel进行通信,完成同步功能,context,select

waitgroup实现等待

sync.WaitGroup 内部是实现了一个计数器,它有三个方法

  • Add() 用来设置一个计数
  • Done() 用来在操作结束时调用,使计数减1
  • Wait() 用来等待所有的操作结束,即计数变为0。

22. Golang的方法集?

一个类型会有一个与它关联的方法集。interface类型的方法集就是接口本身。
其他任意类型T的方法集由接收者为T类型的全部方法构成。对应的指针类型T的方法集是由接收者为T或T的全部方法构成的

23.说说go语言的同步锁?

(1) 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex
(2) RWMutex在读锁占用的情况下,会阻止写,但不阻止读
(3) RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

24、panic和err的区别

处理错误并且在函数发生错误的地方给用户返回错误信息:照这样处理就算真的出了问题,你的程序也能继续运行并且通知给用户。
panic and recover是用来处理真正的异常(无法预测的错误)而不是普通的错误。
当发生像数组下标越界或类型断言失败这样的运行错误时,Go运行时会触发运行时panic,伴随着程序的崩溃抛出一个runtime.Error接口类型的值。这个错误值有个RuntimeError()方法用于区别普通错误。

25.介绍下你平时都是怎么调试 golang 的 bug 以及性能问题的?

panic 调用栈
pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // 引入pprof,调用init方法
)

func main() {

	// 生产环境应仅在本地监听pprof
	go func() {
		ip := "127.0.0.1:9527"
		if err := http.ListenAndServe(ip, nil); err != nil {
			fmt.Println("开启pprof失败", ip, err)
		}
	}()

	// 业务代码运行中
	http.ListenAndServe("0.0.0.0:8081", nil)
}

火焰图(配合压测)
使用go run -race 或者 go build -race 来进行竞争检测
查看系统 磁盘IO/网络IO/内存占用/CPU 占用(配合压测)

五、内存&垃圾回收

1. 一个通过make()命令创建的缓冲区被分配了一块内存后。如何销毁缓冲区并收回内存?

  • buffer = nil
    在运行时,buffer = nil将启动垃圾回收。
  • make()能创建的类型包括:切片,map,管道,三者都是引用类型;(追问:Go语言的引用类型)
  • 引用类型的值的内存分配是在堆中的,栈中使用一个地址指向之;
    需要回收时,将栈中的指针/引用指向即可nil;

2. Golang的内存模型,为什么小对象多了会造成gc压力?

通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配

3. 简述一下你对Go垃圾回收机制的理解?

常见的垃圾回收方法:
引用计数
对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。
优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
代表语言:Python、PHP

标记-清除
从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
优点:解决了引用计数的缺点。
缺点:需要STW,即要暂时停掉程序运行。
代表语言:Golang(其采用三色标记法)

分代收集
按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
优点:回收性能好
缺点:实现复杂
代表语言: JAVA
三色标记法
灰色:对象已被标记,但这个对象包含的子对象未标记
黑色:对象已被标记,且这个对象包含的子对象也已标记
白色:对象未被标记

  • 初始时,所有对象都在 【白色集合】中;
  • 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
  • 从灰色集合中获取对象:
    将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
    将本对象 挪到 【黑色集合】里面。
  • 重复步骤3,直至【灰色集合】为空时结束。
  • 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。

4. Go语言的栈空间管理是怎么样的?

Go语言的运行环境(runtime)尝试在goroutine需要的时候动态地分配栈空间,而不是给每个goroutine分配固定大小的内存空间。这样就避免了需要程序员来决定栈的大小。

5. 简述一下golang的协程调度原理?

M(machine): 代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。
P(processor): 表示逻辑processor,是线程M的执行的上下文。
G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。

6. 介绍下 golang 的 runtime 机制?

Runtime 负责管理任务调度,垃圾收集及运行环境。同时,Go提供了一些高级的功能,如goroutine, channel, 以及Garbage collection。
这些高级功能需要一个runtime的支持。 runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个用户态的独立的可执行文件。
从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。
runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。
从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。
Go runtime的一个重要的组成部分是goroutine scheduler
他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。
因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。

7. Golang中Goroutine 如何调度(GMP模型)?

在这里插入图片描述
每次go调用的时候,都会:

  • 创建一个G对象,加入到本地队列或者全局队列
  • 如果还有空闲的P,则创建一个M
  • M会启动一个底层线程,循环执行能找到的G任务
  • G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找(一次性转移(全局G个数/P个数)个,再去其它P中找(一次性转移一半),
  • 以上的G任务执行是按照队列顺序(也就是go调用的顺序)执行的。

8. 怎么限制Goroutine的数量?

在我们开发过程中,如果不对goroutine加以控制而进行滥用的话,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者CPU使用率过高导致系统忙不过来。

  • 要在每一次执行go之前判断goroutine的数量,如果数量超了,就要阻塞go的执行。第一时间想到的就是使用通道。每次执行的go之前向通道写入值,直到通道满的时候就阻塞了
  • 需要用到sync.WaitGroup。使用WaitGroup等待所有的goroutine退出

9. Goroutine和线程的区别?

从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。

Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。

从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。

goroutine没有一个特定的标识
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。

10. Golang 中常用的并发模型?

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

从上面无缓冲的通道定义来看,发送goroutine和接收goroutine必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。
当主goroutine运行到<-ch接收channel的值的时候,如果该channel中没有数据,就会一直阻塞等待,直到有值。这样就可以简单实现并发控制
通过sync包中的waitgroup实现并发控制
Goroutine是异步执行的,有的时候为了防止在结束main函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用WaitGroup了,在sync包中,提供了WaitGroup,它会等待它收集的所有goroutine任务全部完成。在WaitGroup里主要有三个方法:
– Add,可以添加或减少Goroutine的数量
– Done,相当于Add(-1)
– Wait,执行后会阻塞主线程,直到WaitGroup里的值减至0

在主goroutine中Add(delta int)索要等待goroutine的数量,在每一个goroutine完成后Done()表示一个goroutine已经完成,当所有的goroutine都完成后,在主goroutine中WaitGroup返回。
在Go1.7以后引进的强大的Context上下文,实现并发控制
通常在一些简单场景下使用channel和WaitGroup已经足够了,但是当面临一些复杂多变的网络并发场景下channel和WaitGroup显得有些力不从心了。比如一个网络请求Request,每个request都需要开启一个goroutine做一些事情,这些goroutine有可能会开启其他goroutine,比如数据库和RPC服务。所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,他就是goroutine的上下文。它包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go讲这些封装在一个Context里,再将它传给要执行的goroutine。

context包主要使用来处理多个goroutine之间共享数据,及多个goroutine的管理。

11.协程相对线程有什么优势

  • 轻量级 (MB vs KB)
  • 切换代价低 (调度由程序控制,不需要进入内核空间。需要保存上下文一般少一些)
  • 切换频率低,协程协作式调度,线程调度由操作系统控制,需要保证公平性,当线程数量一多,切换频率相对比较高

12. LVS相关了解

Linux虚拟服务器,是由章文嵩博士主导的开源负载均衡项目,目前LVS已经被集成到Linux内核模块中。该项目在Linux内核中实现了基于IP的数据请求负载均衡调度方案。

13.负载均衡原理是什么?

14. 滑动窗口的概念以及应用?

15.让你设计一个web框架,你要怎么设计,说一下步骤?

六、锁

1、互斥锁,读写锁,死锁问题是怎么解决

互斥锁
互斥锁就是互斥变量mutex,用来锁住临界区的
条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行;读写锁,也类似,用于缓冲区等临界资源能互斥访问的。
读写锁
通常有些公共数据修改的机会很少,但其读的机会很多。并且在读的过程中会伴随着查找,给这种代码加锁会降低我们的程序效率。读写锁可以解决这个问题
注意:写独占,读共享,写锁优先级高
死锁
一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁。另外一种情况是:若线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图会得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和线程B永远处于挂起状态了。
死锁产生的四个必要条件
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

2. 分布式锁实现原理?

基于数据库实现方式
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
缺点:服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据。
基于Redis的实现方式
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

七、中间件

1.说一下中间件原理?

2.gorm 是如何实现的

3.gin 的 handler 底层数据结构

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值