代码题

以n个协程,去输出[]string{“a”,“b”,“c”,“d”,“e”, “f”, “g”}。 代码题真得很简单,自己手生的很,竟然没写出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main()  {
	var wg sync.WaitGroup
	sem := make(chan struct{}, 5) // 创建一个大小为5的信号量
	letters := []string{"a", "b", "c", "d", "e", "f", "g"}

	for _, letter := range letters {
		sem <- struct{}{} // 尝试写入信号量,如果已满,则阻塞
		wg.Add(1)
		go func(l string) {
			defer wg.Done()
			fmt.Println(l)
			time.Sleep(5 * time.Second)
			<-sem // 认领信号量
		}(letter)
	}

	wg.Wait()

八股文

  1. go的map可不可以一边遍历,一边delete。为什么?怎么解决 在Go语言中,不能够同时遍历和修改(包括删除)map,因为在遍历过程中如果对map进行了修改或删除,会导致遍历结果不确定性,甚至会导致程序崩溃。

    如果一边遍历一边删除map中的元素,会导致遍历器遍历到的元素和map中实际存在的元素不一致,可能会漏掉一些元素或者重复遍历某些元素,从而影响程序的正确性。 为了解决这个问题,可以采用以下方案:

    遍历map时,将需要删除的key保存到一个slice中,然后遍历slice进行删除操作。

1
2
3
4
5
6
7
8
m := map[int]string{1:"a", 2:"b", 3:"c"}
keys := make([]int, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    delete(m, k)
}

使用并发安全的map,如sync.Map。sync.Map使用了类似于读写锁的机制,可以在不影响读操作的情况下进行删除操作。

1
2
3
4
5
6
7
8
var m sync.Map
m.Store(1, "a")
m.Store(2, "b")
m.Store(3, "c")
m.Range(func(k, v interface{}) bool {
    m.Delete(k)
    return true
})
  1. go里面的context有没有用过?有什么用处 Go语言中的context包提供了一种可以跨API处理请求范围数据、取消信号和截止日期的机制。你可以将一个Context对象作为参数进行传递。所有的goroutine都可以从Context对象中读取参数、取消请求或者完成请求。

以下是context包的常见用途:

  • 传递请求范围的值 很多时候,我们需要在请求处理过程中传递一些值,比如请求ID、认证信息等等。使用Context将这些值绑定到一个请求链中,所有处理这个请求的函数或者方法都可以访问这些值,可以避免需要手动将这些值传递给每一个函数的不必要麻烦,并减少因为全局变量引入的副作用。

  • 取消处理请求 有时候,当一个请求正在处理时,用户提交了一个取消请求。这时候,我们可以使用Context机制来实现优雅地取消请求。Context被严格限制时间、处理过程,如果当前请求超时、终止、禁止会导致其对Context对应的goroutine发送信号,以支持适当的终止。这种机制可以避免不必要的等待,也可以是你的程序具备可撤销属性,保证系统更加健壮。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func processRequest(w http.ResponseWriter, req *http.Request) {
    // 从上下文中获取请求ID
    requestID := req.Context().Value("requestID").(string)
    
    // 创建一个可被取消的Context对象
    ctx, cancel := context.WithTimeout(req.Context(), 5 * time.Second)
    defer cancel()
    
    // 处理请求
    result, err := process(ctx, requestID)
    if err != nil {
        // 如果Context超时或被取消了,则不将出错信息写入响应
        select {
        case <-ctx.Done():
            log.Println("request canceled:", ctx.Err())
            return
        default:
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
    // 将处理结果写入响应
    fmt.Fprintf(w, "result: %q", result)
}
  1. channel的底层实现? 里面有锁么? 在 Go 语言中,channel 是一种用来在 goroutine 之间进行通信的方法,是非常重要的并发编程工具。本质上,channel 是一种阻塞式的队列,底层是通过一种称为 Wait-based 机制的方式实现的。具体来说,当一个 goroutine 向 channel 发送消息时,它会被阻塞,直到另一个 goroutine 从 channel 中取出了这条消息。同样地,当一个 goroutine 从 channel 中接收消息时,如果此时 channel 中没有消息可接收,该 goroutine 也会被阻塞,直到另一个 goroutine 向 channel 中发送了一条消息。

对于 channel 的底层实现,可以简单概括为:channel 底层是一个带锁的环形队列。

具体来说,当我们使用 make(chan T) 或者 make(chan T, n) 创建一个 channel 时,Go 语言运行时会为它分配内存,并在内存中创建一个 channel 结构体,该结构体里包含了以下几个元素:

buf:一个长度为 n 的 T 类型数组,这是 channel 在底层实现中所使用的环形队列; elemSize:元素大小(即 T 类型的长度); closed:一个 flag,表示 channel 是否已经被关闭; qcount/closed:一个 uint32 类型的变量,表示队列中已有的元素个数或者 channel 是否已经被关闭; dataqsiz:队列的容量,即 channel 可以容纳的元素个数。 在操作 channel 时,编译器会将对 channel 的读写操作转换为对 Go 语言运行时的函数调用。具体来说,发送操作会转换为 runtime.chansend 函数的调用,接收操作会转换为 runtime.chanrecv 函数的调用。

runtime.chansend 函数负责向 channel 中发送元素,其内部逻辑简述如下:

锁住 channel 结构体中的 Mutex; 判断 channel 是否已经被关闭或队列已满,如果是则解锁 channel 并直接返回一个 false,否则继续; 将元素写入队列中,并修改 qcount 变量的值,表示队列中已有的元素个数增加了一个; 解锁 channel,并返回一个 true。 runtime.chanrecv 函数负责从 channel 中接收元素,其内部逻辑简述如下:

锁住 channel 结构体中的 Mutex; 判断 channel 是否已经被关闭或队列已空,如果是则解锁 channel 并直接返回一个 nil 进行流程; 从队列中取出一个元素,并修改 qcount 变量的值,表示队列中已有的元素个数减少了一个; 解锁 channel,并将取出的元素返回。 需要注意的是,在 Go 1.5 之前的版本中,channel 底层是由单向链表实现的,在最新版本中已经变为环形队列。另外,当前 Go 语言运行库中使用了多种加锁机制来保证 channel 的并发安全,包括 Spin Lock、Semaphores 等。 4.协程与线程的区别 6. panic后defer会不会执行 在 Go 语言中,当程序执行到一个 panic 语句时,程序就会停止当前执行流程,并进入到 panic 所在的 goroutine 的 defer 执行阶段。这时,所有在该 goroutine 中延迟执行的 defer 语句都会被执行。

需要注意的是,在执行完 defer 语句后,当前 goroutine 的执行不会继续进行,而是结束掉。这时,运行时会将控制权交给上一层调用栈中的代码,并在那里进行恐慌处理。

下面是一个示例程序的代码,展示了在执行过程中当 panic 被触发的时候 defer 是否会执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
    defer func() {
        fmt.Println("defer in main")
        if r := recover(); r != nil {
            fmt.Println("recovered in main:", r)
        }
    }()
    
    innerFunc()
    fmt.Println("main function end.")
}

func innerFunc() {
    defer func() {
        fmt.Println("defer in innerFunc")
    }()
    
    panic("innerFunc panic.")
}
1
2
3
defer in innerFunc
defer in main
recovered in main: innerFunc panic.

这段代码主要利用了 defer 和 panic/recover 两个关键字。 在 main 函数中,首先定义了一个 defer 函数,用于在 main 函数结束前打印一句话。然后调用了 innerFunc 函数。 在 innerFunc 函数中,也定义了一个 defer 函数,用于在 innerFunc 函数结束前打印一句话。接着,通过 panic 抛出了一个异常。 由于 innerFunc 函数没有进行异常处理,因此该异常会被传递到调用该函数的地方,也就是 main 函数中。在 main 函数的 defer 函数中,首先打印了一句话,然后通过 recover 恢复了异常,并打印了异常信息。 7. zset使用过么?主要用在什么地方 zset 是 Redis 中的一种数据结构,也称为有序集合(Sorted Set)。和 set 类型一样,zset 也是一个字符串集合,不同的是每个字符串都有一个分数(score),表示该字符串的排序权重。 zset 常用于需要排序的场景,比如排行榜、热门文章列表等。通过设置不同字符串的分数,可以实现按照一定规则排序。在 Redis 中,zset 内部使用跳跃表(Skip List)实现,查找、插入、删除的时间复杂度都是 O(log N),性能较高。 zset 不仅支持基本的集合操作,比如添加、删除、查找某个元素,还能根据分数范围查找元素,获取排名、分数等信息。此外,zset 还支持多个元素之间的交集、并集、差集等操作。 8. redis为什么运行的那么快 Redis 之所以运行那么快,主要是因为以下几个原因:

纯内存操作: Redis 是一个基于内存的数据存储系统。相对于传统的关系型数据库,Redis不需要进行磁盘I/O操作,因而具有很高的读写速度。

异步式非阻塞I/O:Redis 使用 I/O 多路复用技术来处理多个客户端请求,并在内部利用单线程的方式来处理这些请求。这种方式可以保证在大量请求时的高性能,避免了同步阻塞I/O所引起的性能瓶颈。

单线程处理: Redis 是单线程的数据处理引擎,这意味着 Redis 不会在并发环境中出现任何锁问题,从而尽可能地避免了锁竞争的问题。

持久化的灵活配置: Redis 提供两种持久化方式,即RDB(快照方式)和AOF(追加文件方式),并支持对不同场景进行不同的配置。

总的来说,Redis在设计的时候,考虑了内存操作的高效,I/O多路复用的高并发,单线程无锁处理的高效性等多个方面,从而可以达到高性能的运行效果。 9. 缓存穿透与缓存雪崩,怎么解决?如果时间不太好控制, 怎么处理 缓存穿透和缓存雪崩是 Redis 中常见的问题,下面是针对这两个问题的解决方案:

缓存穿透的解决方案:

设置布隆过滤器:在缓存层之前增加一个布隆过滤器,用于快速判断请求的 key 是否存在于缓存中,如果不存在,则直接返回,避免了对数据库的查询操作。

缓存空对象:如果查询数据库结果为空,也可以将结果缓存起来,但是缓存时间要短,避免浪费内存。

对于恶意攻击,可设置 IP 黑名单或者限流措施,限制访问频率。

缓存雪崩的解决方案:

设置不同的过期时间:将缓存的过期时间设置为不同的时间,避免同时失效导致的请求全部落到数据库上。

限流措施:对于流量突然增大的情况,可以采用限流措施,例如设置并发访问的最大数量,超过限制则直接返回错误信息。

数据预热:在系统低峰期,可以预先将热门数据加载到缓存中,避免在高峰期缓存失效导致的请求全部落到数据库上。 如果时间无法控制,则可以考虑采用缓存自动刷新机制,例如在缓存过期时自动刷新缓存,避免缓存失效导致的请求全部落到数据库上。此外,还可以适当增加缓存的过期时间,避免缓存过早失效。 10. innodb的存储引擎是什么?相比其他的树有什么优点 11. 什么是复合索引?用的时候该怎么,abc三个字段,只查bc,索引下推 12. kafka如何保证消息的顺序一致性 13. es,如果操作10000条,出现深分页,怎么解决。 使用游标分页:Elasticsearch 支持使用游标分页(Cursor-Based Pagination)的方式进行分页处理,这种分页方式不仅可以提高查询性能,同时还可以避免深分页的问题。游标分页可以利用 Livy API 或 Elasticsearch Scroll API 来实现。

优化查询条件:深度分页的根本原因是要遍历较多的文档来满足查询的结果,因此优化查询条件可以减少文档数量,从而避免深分页的问题。可以尽量缩小查询范围,更精确地定位查询的文档。

使用分页的深度限制:Elasticsearch 默认不允许从搜索上下文(Scroll API)中请求超过10000个文档,这是基于性能的考虑。如果您一定要处理这么多文档,你可以使用 scroll_size 参数进行配置,从而控制它处理的文档数,这样可以避免深分页的风险。

使用 Doc Values 进行排序:使用 Doc Values 进行排序能够加速排序操作,从而避免深分页的问题。通常深度分页是由于大量排序操作导致的,如果使用 DocValues,可以获得更快的排序速度,从而减少深度分页。

总的来说,在 Elasticsearch 中,要避免深分页问题可以通过使用游标分页、优化查询条件、使用分页深度限制和使用 Doc Values 进行排序等方法来解决。 14. mysql事务的隔离级别是什么。