go 面试题

 1 gochannelclose后读的问题

在Go语言中,当我们调用close(ch)关闭一个channel后,仍然可以从该channel中读取数据,直到channel中的数据全部被读取完。但需要注意以下几点:

  1. 从关闭的channel中读取数据时,接收操作不会阻塞,会一直返回channel中剩余的数据,直到channel为空。
  2. 当channel关闭后,再次尝试从该channel中接收数据时,将得到对应类型的零值和一个布尔值false,这个布尔值用来表示是否成功从channel中获取到了有效数据。例如,对于整型channel ch := make(chan int), 从关闭的ch中读取数据可能是这样的:value, ok := <-ch,如果channel已经关闭并且没有数据可读,那么ok为false,value为0(int类型的零值)。

示例代码:


Go

1ch := make(chan int)
2go func() {
3    defer close(ch)
4    ch <- 1
5    ch <- 2
6}()
7
8for i := 0; i < 3; i++ {
9    value, ok := <-ch
10    if ok {
11        fmt.Println("Received:", value)
12    } else {
13        fmt.Println("Channel is closed and no more data to receive.")
14        break
15    }
16}

上述代码中,goroutine会向channel中发送两个整数然后关闭它,主goroutine则会从channel中接收三次数据,前两次能成功接收到值并打印出来,第三次由于channel已关闭且无数据可读,因此会输出"Channel is closed and no more data to receive."并退出循环。

2 在Linux系统中,如果你想要使用grep命令来查找日志文件中的相关内容,你可以按照以下基本语法操作:


Bash

1grep '搜索内容' /path/to/logfile.log

举例说明:

  1. 查找名为system.log的日志文件中包含字符串"error"的所有行:

Bash

1grep 'error' /var/log/system.log
  1. 忽略大小写查找(匹配时不论大小写):

Bash

1grep -i 'ERROR' /var/log/system.log
  1. 显示匹配行及其所在的文件名和行号:

Bash

1grep -Hn 'error' /var/log/*.log

上述命令会查找/var/log/目录下所有.log扩展名的文件,并显示包含"error"的行及其完整路径和行号。

  1. 使用正则表达式进行更复杂的匹配:

Bash

1grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}.*error.*$' /var/log/system.log

这个例子会查找以四位年份、两位月份、两位日期开头且包含"error"的行。

  1. 递归查找当前目录及其子目录下所有文件中包含指定内容的行:

Bash

1grep -rnw '/path/to/somewhere/' -e 'pattern'

其中:

  • -r 或 -R 表示递归查找。
  • -n 表示输出行号。
  • -w 表示只匹配整个单词。
  • -e 指定要匹配的模式或正则表达式。

请根据实际需求调整上述命令参数及搜索内容

3. 数组与slice 的区别

在Go语言中,数组和切片(slice)是两种不同的序列类型,它们有以下主要区别:

  1. 数组

    • 数组是一个具有固定长度的元素集合,一旦创建后,其长度不可变。
    • 声明数组时需要指定元素类型和长度,例如 var arr [5]int 表示一个包含5个整数的数组。
    • 数组是值类型,当作为函数参数传递时,会复制整个数组内容;修改副本不会影响原始数组。
    • 数组可以通过索引直接访问每个元素。
  2. 切片(Slice)

    • 切片是动态大小的、灵活的序列,它基于底层数组实现,并提供对数组部分或全部元素的引用。
    • 切片没有固定的长度,可以扩展或缩小。
    • 创建切片不需要预先知道元素数量,声明时不指定长度,如:var s []int。或者通过数组创建:s := arr[0:3],这将创建一个原数组从索引0开始到索引2(不包括3)的新切片。
    • 切片是引用类型,当作为函数参数传递时,传递的是指向底层数组的指针以及长度和容量信息,因此函数内对切片元素的修改会影响到原始切片。
    • 切片支持和数组类似的下标操作,并且可以使用内置函数len()获取切片长度,cap()获取切片容量(即它可以扩展到的最大长度而不重新分配内存)。

总结来说,数组更适合于已知大小并且不需动态增长或收缩的情况,而切片则更适用于大多数日常编程需求,因为它提供了更大的灵活性。

4 make 与new的区别

在Go语言中,makenew是两个用于内存分配的内建函数,但它们有着不同的用途和行为:

  1. new()

    • new()函数会为指定类型分配内存,并初始化为零值。它返回的是指向新分配内存的指针,即类型为T的指针,其中T是传入的类型。
     Go 
    1var x *int = new(int) // 分配一个int类型的内存空间并初始化为0,x是一个指向该内存空间的指针
  2. make()

    • make()函数主要用于创建 slice、map 和 channel 这三种内建引用类型,这些类型虽然也是通过指针访问,但它们内部包含了一些额外的状态信息,因此不能直接使用new()来创建。
     Go 
    1var s []int = make([]int, 5) // 创建一个长度为5的int类型的切片
    2var m map[string]int = make(map[string]int) // 创建一个空的字符串到整数映射
    3var c chan int = make(chan int) // 创建一个无缓冲的int类型通道

总结来说,在Go语言中:

  • 如果你想分配一个基础类型或结构体类型的内存,并将其初始化为零值,你应该使用new()函数。
  • 如果你需要创建一个slice、map或channel,应该使用make()函数。

5 B+树结构and为什么

B+树是一种自平衡的树数据结构,广泛应用于数据库和文件系统中作为索引结构。选择B+树作为数据库索引结构的原因主要包括以下几个关键点:

  1. 磁盘IO效率

    • B+树每个节点可以存储更多的键值对(通常远大于二叉搜索树),这意味着在同样大小的数据块上,B+树能拥有更少的层级深度。由于数据库中的索引通常存储在磁盘上,而磁盘I/O是非常耗时的操作,减少树的高度就意味着查询时需要访问的磁盘次数显著降低,从而提高了查询效率。
  2. 顺序访问优化

    • B+树的所有叶子节点形成了一个有序链表,这使得范围查询极其高效。当进行区间查询或全表扫描时,无需回溯到父节点即可连续读取所有相关的键值对,非常利于大量连续数据的读取操作,如排序、分页查询等。
  3. 内部节点仅存键值

    • B+树的非叶子节点并不存储实际的数据记录,它们只存储键值以及指向子节点的指针。这样设计的好处是每个节点可以容纳更多键,进一步减少了树的层级,同时也意味着索引结构占用空间相对较小。
  4. 聚集索引与非聚集索引

    • 在数据库中,使用B+树可以实现聚集索引(即叶子节点直接包含行数据或者指向行数据的物理地址)和非聚集索引(叶子节点存储键值及其对应的主键值)。B+树的特性允许它既能用于快速定位单行记录,也能支持高效的范围查找和排序。

综上所述,B+树之所以成为数据库索引的理想选择,是因为其设计能够有效利用磁盘预读机制,最大程度地减少随机I/O,提升大数据量下的检索性能,并且方便进行大规模数据的处理和查询。

5 io多路复用,epoll和select的区别

IO多路复用技术允许单个线程同时监听多个文件描述符(通常是网络套接字)的事件,从而在大量并发连接中实现高效的资源利用率。selectpoll 和 epoll 是Linux系统下实现这一功能的三种不同的机制:

  1. select

    • select函数需要一个包含所有待监视文件描述符集合的三个位图,分别对应读就绪、写就绪和异常状态。
    • 每次调用select时都需要将所有要监控的文件描述符复制到内核空间,当文件描述符数量很大时,这会导致较大的内存拷贝开销。
    • 当有事件发生时,select返回并告诉用户哪些描述符准备好了,但是它并不直接提供具体的事件类型,需要再次遍历所有描述符来确定具体是哪个描述符发生了什么类型的事件。
  2. poll

    • poll改进了select的缺点,它使用一个pollfd结构体数组来存储待监控的文件描述符及其事件,不再需要维护位图。
    • 同样支持同时监控多个文件描述符,并且没有最大文件描述符数量的限制(不像早期select版本有限制)。
    • 虽然poll解决了select的大数量描述符问题,但在处理大量描述符场景下依然效率不高,因为每次调用poll时,无论多少描述符准备好,都得遍历整个结构体数组。
  3. epoll

    • epoll在Linux 2.6内核及以后版本中引入,是一种更高级别的IO多路复用机制。
    • epoll通过创建一个epoll实例(称为epoll句柄),然后向这个句柄添加或删除感兴趣的文件描述符。
    • epoll有两种工作模式:LT(水平触发,Level Triggered)和ET(边缘触发,Edge Triggered)。
    • 当有文件描述符就绪时,epoll_wait仅返回就绪的描述符,而不是全部,因此减少了无谓的遍历操作。
    • epoll使用事件驱动模型,当描述符状态变化时,内核会主动通知用户空间,避免了每次轮询时都要检查所有描述符的状态。
    • epoll可以以O(1)的时间复杂度管理大量的文件描述符,对于高并发场景非常高效。

总结来说,epoll相比select和poll的主要优势在于:

  • 更好的性能:只关注发生的事件,而不是所有待监控的描述符;
  • 更少的系统调用:支持批量添加/删除事件,以及一次性获取多个就绪描述符;
  • 更低的内存消耗:无需每次都复制整个描述符集合,尤其是对大连接数服务尤为有利。

6 计网七层协议、线程进程区别

  1. 七层网络协议模型: 七层网络协议模型,也称OSI(Open Systems Interconnection)模型,是理论上较为完整和全面的网络通信参考模型,将网络通信分为七个层次。每个层次完成特定的功能,并向其上层提供服务,同时依赖于下层提供的服务。这七个层次从低到高分别为:

    • 第一层:物理层(Physical Layer) 负责传输比特流,定义了设备之间数据传输的电气、机械和功能特性,如电缆规格、信号速率、编码方式等。

    • 第二层:数据链路层(Data Link Layer) 提供相邻节点间可靠的数据传输,负责错误检测与修正,帧同步以及寻址,例如以太网MAC地址和LLC子层。

    • 第三层:网络层(Network Layer) 负责路径选择和IP地址路由,实现不同网络之间的通信,比如IP协议。

    • 第四层:传输层(Transport Layer) 确保端到端的数据传输,提供可靠性保障,TCP协议和UDP协议就工作在这个层面上。

    • 第五层:会话层(Session Layer) 主要负责建立、管理和终止会话,但在实际应用中这一层在TCP/IP模型中通常被合并到了其他层。

    • 第六层:表示层(Presentation Layer) 处理数据格式化、加密解密、压缩解压等功能,确保应用程序能够正确理解数据。

    • 第七层:应用层(Application Layer) 直接为用户提供服务,支持各种应用程序,如HTTP、FTP、SMTP、DNS等协议。

  2. 线程与进程的区别

    • 进程(Process)是操作系统资源分配的基本单位,它拥有独立的虚拟内存空间、系统资源(如文件描述符)和一个完整的运行环境。每个进程执行一个独立的程序,包含至少一个线程,多个进程之间互不影响,各自运行自己的上下文。

    • 线程(Thread)是操作系统调度的基本单位,它是进程中的一条执行路径或控制流程。同一进程内的所有线程共享相同的进程上下文,包括堆、全局变量等内存区域,但每个线程有自己的栈空间。线程可以并发执行,使得在同一进程内部可以实现多任务并行处理。

    区别要点

    • 资源分配:进程间资源是独立且受保护的;而同一进程内的线程共享大部分资源。
    • 创建开销:进程创建时需要分配独立的资源,开销较大;线程创建成本较低,因为它复用了进程的许多资源。
    • 通信复杂度:进程间通信相对复杂,往往需要借助IPC机制(如管道、消息队列、共享内存等);而同一进程内的线程间通信简单高效,可以直接访问共享内存。
    • 上下文切换:进程间的上下文切换涉及更多的状态保存和恢复;线程间的上下文切换开销较小,但仍需维护线程特有的寄存器上下文。

7 zookeeper 实现分布式锁

Apache ZooKeeper是一个分布式的、开源的协调服务,它可以用来实现分布式锁。在ZooKeeper中,通过创建临时有序节点的方式来实现分布式锁,通常有两种主要方式:互斥锁(Mutex Lock) 和 读写锁(Read-Write Lock)

  1. 基于临时有序节点的互斥锁

    • 创建一个名为/lock的父节点。
    • 当客户端需要获取锁时,在/lock下创建一个临时有序节点,如/lock/0000000001,序号由ZooKeeper自动维护。
    • 客户端获取所有子节点,并找出序号最小且不是自己的节点作为前驱节点。
    • 如果当前客户端创建的节点序号是最小的,那么它就获得了锁;否则,它需要监听前驱节点的删除事件,当前驱节点被删除后,轮到自己获得锁。
  2. 释放锁

    • 当客户端完成操作后,只需删除自己创建的临时有序节点即可。由于是临时节点,所以如果客户端进程挂掉,ZooKeeper会自动清理该节点,从而保证锁能够被其他客户端获取。
  3. 读写锁

    • 实现读写锁时,可以在/lock下创建两个子节点,比如/lock/readLocks/lock/writeLocks
    • 读锁:多个客户端可以在/lock/readLocks下创建临时有序节点来同时持有读锁,原理与互斥锁类似,但不需要等待其他读锁持有者释放锁。
    • 写锁:只有一个客户端能在/lock/writeLocks下持有写锁,同时,任何写锁请求到来时,都会阻塞后续所有的读锁请求,以确保数据一致性。

这种基于ZooKeeper实现的分布式锁具有高可用性、强一致性等特点,适用于分布式系统中的资源同步场景。

8 什么情况下redis 的分布式锁被别人抢走

Redis 分布式锁在某些特定情况下可能被其他客户端抢走,这些情况包括但不限于以下几点:

  1. 锁过期

    • 当客户端获取锁后,并未在锁的有效期内完成任务或续期(如通过EXPIRE命令为锁设置的TTL过期),那么当锁过期时,其他客户端可以使用SETNX(设置不存在的键)或其他分布式锁算法再次获取到锁。
  2. 网络延迟

    • 在释放锁的过程中,如果持有锁的客户端与Redis服务器之间的网络出现延迟或故障,导致解锁操作没有成功执行,而锁已过期,则其他客户端有可能在此期间获取锁。
  3. 客户端异常

    • 如果持有锁的客户端因为某种原因崩溃或进程终止,而未正常释放锁,一旦锁过期,这个锁也会变为可被获取的状态。
  4. 锁释放逻辑不严谨

    • 如果客户端在释放锁时采用的是非原子性的操作,例如先删除锁再检查是否成功,中间环节可能存在并发问题,导致锁提前被其他客户端抢占。
  5. 竞争激烈

    • 在高并发场景下,尤其是在锁即将过期的瞬间,多个客户端可能同时尝试获取锁,尽管Redis提供了原子性操作,但在锁到期后的极短时间窗口内,可能会发生竞态条件。
  6. 误删

    • 错误的代码逻辑或者运维操作可能导致一个并未持有的锁被错误地删除,从而使得其他客户端能够获取到锁。

为了减少以上情况的发生,通常会采用一些优化措施来增强分布式锁的安全性和可靠性,例如:

  • 使用setnxexpire组合成原子操作(Lua脚本或者Redis 2.6.12及以后版本支持的set命令参数NXPX)。
  • 设置合理的锁超时时间,并结合锁自动续期机制以防止死锁。
  • 实现公平锁,确保锁被正确释放后才能被下一个等待者获取。

9 Redis 实现分布式公平锁通常依赖于其强大的数据结构和命令支持。Redisson 是一个基于 Redis 实现的在 Java 语言中的客户端库,它提供了丰富的数据结构和分布式服务,包括实现公平锁(Fair Lock)的功能。

Redisson 的公平锁原理大致如下:

  1. 基于有序集合(Sorted Set):Redisson 公平锁使用了多个 Redis 数据结构来协同工作,其中包括一个有序集合(ZSET),用于存储等待获取锁的线程及其等待时间戳,从而保证按照请求顺序分配锁。

  2. 队列(List):每个锁都有一个关联的队列,当锁被占用时,新的请求会被添加到这个队列中排队等待,即 threadsQueueName 键对应的列表结构。

  3. 超时机制:通过设置过期时间(TTL)确保锁能自动释放,并且在尝试获取锁时,可以指定最大等待时间,超过这个时间则放弃获取。

  4. 心跳机制:持有锁的客户端会定期发送心跳信号以更新锁的过期时间,防止因网络延迟或其他原因导致的锁意外释放。

  5. 公平性保证:当锁变为可用状态时,Redisson 不是简单的再次争夺锁,而是从有序集合中找到等待时间最长的线程并将其唤醒,使得等待时间最长的线程有更高的优先级获得锁,从而实现了公平锁的特性。

具体实现细节涉及复杂的逻辑,包括但不限于:

  • 使用 Lua 脚本保证原子操作。
  • 利用发布/订阅(Pub/Sub)模式通知等待线程锁的状态变化。
  • 内部维护线程状态以及重入计数等信息。

通过这些机制,Redisson 提供了一个高并发环境下的高效、可重入并且遵循公平原则的分布式锁。

10 线程怎么调度

线程调度是操作系统或者执行环境(如Java虚拟机)管理多个线程并发执行时,决定哪个线程应该获得CPU资源进行执行的过程。线程调度可以采用不同的策略和算法,下面简要介绍两种常见的线程调度模型:

  1. 分时调度(Time-Sharing Scheduling): 在分时调度模型中,系统将CPU时间划分为一系列的时间片(time slice或time quantum),所有线程按照某种顺序轮流使用一个时间片的CPU执行时间。当一个时间片用完后,操作系统会暂停当前正在运行的线程,并将其状态更改为就绪态,然后选择另一个处于就绪态的线程继续执行。这种调度方式下,所有线程理论上可以得到平等的执行机会。

  2. 抢占式调度(Preemptive Scheduling): 抢占式调度根据优先级或其他条件动态地为线程分配CPU使用权。即使某个线程正占用着CPU,但在更高优先级的线程进入就绪状态时,操作系统会立即中断当前运行的低优先级线程,并将CPU控制权交给高优先级线程。在优先级相同的线程之间,也可以通过时间片轮转的方式实现公平调度。

在Java环境中,线程调度由JVM和底层操作系统共同协作完成。Java默认使用的是非协同式的、基于优先级的抢占式调度。每个Java线程都有一个优先级,但实际调度仍取决于操作系统的线程调度机制。此外,Java提供了Thread.sleep()Thread.yield()等方法影响线程的执行,但并不能直接控制线程调度,而是提供了一种建议给调度器,例如sleep()可以让线程放弃其剩余时间片并进入等待状态。

线程调度的关键时机包括但不限于:

  • 当前线程的时间片结束。
  • 高优先级线程变为可运行状态。
  • 线程主动调用阻塞操作(如I/O请求)而进入阻塞态。
  • 发生了外部中断事件。
  • 线程完成了自己的任务,主动退出。

11 进程通信方法

进程通信(IPC,Inter-Process Communication)是指在操作系统中,不同进程之间交换信息或数据的方式。以下是几种常见的进程间通信方法:

  1. 管道(Pipe)

    • 无名管道(Anonymous Pipe):在同一台机器上的父子进程之间单向或双向传递数据,是内核提供的一种半双工通信方式。
    • 命名管道(FIFO,First-In-First-Out):也称命名管道或有名管道,可以在没有亲缘关系的进程之间进行通信,它在文件系统中作为一个特殊文件存在。
  2. 信号(Signal)

    • 信号是一种软件中断机制,用于通知接收进程发生了某种事件,但不能携带大量数据,主要用于简单控制和同步目的。
  3. 消息队列(Message Queue)

    • 消息队列允许进程间以异步的方式发送、接收数据块,每个数据块称为一个消息,可以带有优先级,并且能避免丢失信息。
  4. 共享内存(Shared Memory)

    • 共享内存是在同一台机器上多个进程可以直接访问的一段物理内存区域,通过映射到进程地址空间来实现高效的数据交换。
  5. 信号量(Semaphore)

    • 信号量是用来解决多进程对共享资源访问时的同步与互斥问题的机制,它可以是一个计数器,通过P(wait)和V(signal)操作来控制进程进入或离开临界区。
  6. 套接字(Socket)

    • 套接字主要应用于网络环境下的进程间通信,也可以用于同一主机的不同进程之间的通信。
  7. 内存映射文件(Memory-Mapped Files)

    • 内存映射文件允许进程将磁盘文件映射到自己的地址空间,这样就可以直接通过读写内存的方式来访问文件内容,从而实现进程间通信。
  8. 套接字pairs/UNIX域套接字(Socket Pairs / UNIX Domain Sockets)

    • 在Unix-like系统中,UNIX域套接字提供了一种在同一个主机上的进程间通信机制,类似于网络套接字,但是工作在文件系统领域而不是网络协议栈。

每种IPC机制都有其适用场景和优缺点,选择哪种方式取决于实际应用的需求,如数据量大小、实时性要求、同步需求以及是否跨网络等条件

你可能感兴趣的:(golang,java,前端)