golang面试题

目录

go版本新增功能

[Go 1.11](#Go 1.11)

[Go 1.18](#Go 1.18)

[Go 1.5](#Go 1.5)

[go关键字 :](#go关键字 :)

[1. 用于声明的关键字](#1. 用于声明的关键字)

[2. 控制流关键字](#2. 控制流关键字)

[3. 包相关关键字](#3. 包相关关键字)

[4. 并发相关关键字](#4. 并发相关关键字)

[5. 异常处理关键字](#5. 异常处理关键字)

[6. 接口和类型断言关键字](#6. 接口和类型断言关键字)

go数据类型:

复合数据类型

引用数据类型

接口类型

GC垃圾回收机制

工作流程

采用的方法

触发时机

深拷贝与浅拷贝

函数和方法的区别:

Go函数参数传递到底是值传递还是引用传递

为什么map遍历是无序的:

slice扩容机制

方法值类型接收者和指针类型接收者的区别

go的互斥锁抢锁的两种模式(sync.Mutex)

正常模式

饥饿模式

goroutine和线程的区别

[1. 内存占用](#1. 内存占用)

[2. 创建销毁开销](#2. 创建销毁开销)

[3. 调度](#3. 调度)

[4. 通信](#4. 通信)

[5. 并发规模](#5. 并发规模)

goroutine泄露

原因

危害

解决办法

GMP调度原理

基本概念

调度流程

[1. 启动阶段](#1. 启动阶段)

[2. 创建 Goroutine](#2. 创建 Goroutine)

[3. 调度执行](#3. 调度执行)

[4. 阻塞和唤醒](#4. 阻塞和唤醒)

[5. 系统调用](#5. 系统调用)

调度优势

什么是通道,它的底层是什么

通道是什么

通道底层结构

生产者和消费者怎么关闭管道

基本规则

不同场景关闭方式

如何检测通道关闭

通道和锁的区别

设计理念

使用场景

并发控制

代码复杂度

性能表现

数组和切片的区别

定义和长度特性

内存分配

长度和容量

使用灵活性


go版本新增功能

Go 1.11 、Go 1.18、Go 1.5

Go 1.11

  • 核心亮点:正式推出了模块(Module)系统,这是 Go 语言依赖管理的重大变革。在此之前,Go 语言的依赖管理比较混乱,模块系统的引入解决了依赖管理的难题,使得 Go 项目的依赖管理更加简单、可靠和可复现。
  • 其他特性:支持 WebAssembly(Wasm),允许 Go 代码在浏览器和其他支持 Wasm 的环境中运行,拓宽了 Go 语言的应用场景

Go 1.18

  • 核心亮点 :这一版本最大的亮点是引入泛型,这是 Go 语言发展历程中的一个重大里程碑。泛型允许开发者编写与具体类型无关的代码,极大提高了代码复用性,减少了代码冗余。例如,开发者可以编写一个通用的排序函数,用于对不同类型的切片进行排序,而无需为每种类型都编写一个特定的排序函数。

    // 泛型函数:返回两个同类型值中的最大值
    func Max[T int | float64](a, b T) T {
        if a > b {
            return a
        }
        return b
    }
    
  • 其他特性:还引入了模糊测试(Fuzzing)功能,帮助开发者更有效地发现代码中的潜在漏洞;工作区模式(Workspace Mode)则方便了多模块项目的管理;切片扩容机制也得到了优化,提升了内存使用效率和性能。

Go 1.5

  • 核心亮点:实现了自举(Self - Bootstrapping),即 Go 编译器完全用 Go 语言重写,摆脱了对 C 语言的依赖。这不仅简化了 Go 语言的编译过程,也让 Go 语言的开发和维护更加独立和灵活。
  • 其他特性:引入了全新的垃圾回收器(GC),大幅降低了垃圾回收时的停顿时间,提高了程序的响应性能和吞吐量。同时,Go 运行时(runtime)的调度器也得到了改进,优化了 goroutine 的调度算法,提高了并发性能。

go关键字

1. 用于声明的关键字
  • var :用于声明变量,如 var num int = 10
  • const :用于声明常量,如 const Pi = 3.14
  • type :用于声明自定义类型,如 type Person struct { Name string; Age int }
  • func :用于声明函数,如 func add(a, b int) int { return a + b }
2. 控制流关键字
  • ifelseelse if:用于条件判断。
  • for :用于循环操作,Go 语言中只有 for 一种循环结构,但可实现多种循环形式。
  • switchcasedefault:用于多条件分支判断。
  • break :用于跳出循环或 switch 语句。
  • continue:用于跳过当前循环的剩余部分,直接进入下一次循环。
  • goto:用于无条件跳转到指定的标签处,不过在实际编程中应谨慎使用,以免导致代码可读性变差。
3. 包相关关键字
  • package :用于声明当前文件所属的包,每个 Go 文件都必须以 package 声明开头。
  • import:用于导入其他包,以便使用该包中导出的函数、变量等。
4. 并发相关关键字
  • chan:用于声明通道(channel),通道是 Go 语言中用于在 goroutine 之间进行通信和同步的重要工具。
  • select :用于处理多个通道的并发操作,类似于 switch 语句,但用于通道的读写操作。
5. 异常处理关键字
  • defer :用于延迟函数的执行,无论包含 defer 语句的函数是正常返回还是发生异常,defer 后的函数都会在该函数返回前执行。
  • panic :用于触发一个运行时错误,程序会停止正常的执行流程,开始回溯调用栈并执行 defer 函数,直到程序崩溃。
  • recover :用于从 panic 中恢复,通常在 defer 函数中使用。
6. 接口和类型断言关键字
  • interface:用于声明接口类型。
  • struct:用于声明结构体类型。
  • map:用于声明映射类型。
  • fallthrough :在 switch 语句中使用,用于强制执行下一个 case 分支,而不进行条件判断。

go数据类型

  • 布尔型bool
  • 数值型
    • 整数intint8int16int32int64uintuint8uint16uint32uint64uintptr
    • 浮点数float32float64
    • 复数complex64complex128
    • 字节byte
    • 符文rune
  • 字符串型string

复合数据类型

  • 数组[n]Tn 为数组长度,T 为元素类型)
  • 结构体struct{...}

引用数据类型

  • 切片[]TT 为元素类型)
  • 映射map[K]VK 为键类型,V 为值类型)
  • 通道chan TT 为通道传递的数据类型)

接口类型

  • 接口interface{...}

GC垃圾回收机制

工作流程

  • 标记阶段:想象 GC 是一个拿着 "标记笔" 的人,从一些固定的地方(像全局变量、正在运行的函数里的变量等)开始找东西。找到一个能用的东西(对象)就给它做个标记,然后顺着这个东西里面的线索(引用)继续找下一个,直到把所有能找到的东西都标记好。
  • 清除阶段:标记完后,GC 会再走一遍所有的地方,看看哪些东西没有被标记,这些没标记的就是没人用的 "垃圾",GC 就会把它们清理掉,让这些被占用的空间可以被重新使用。

采用的方法

  • 三色标记法:GC 把所有东西分成白色、灰色和黑色。刚开始所有东西都是白色,不知道有没有用。然后从一些确定有用的地方开始找,找到的白色东西就变成灰色,放到一个 "待办清单" 里。接着 GC 会从 "待办清单" 里拿出灰色的东西,把它引用的白色东西也变成灰色,然后把自己变成黑色,表示已经检查过了。最后剩下的白色东西就是没用的 "垃圾"。
  • 分代回收:GC 把内存里的东西分成年轻的和年老的。新产生的东西一般放在年轻组,因为新东西很可能很快就没用了,所以会经常检查年轻组,把没用的清理掉。有些东西一直没被清理,就会被放到年老组,年老组检查的次数会少一些。这样可以提高清理效率。
  • 并发回收:GC 不会一直让程序停下来等它清理垃圾,而是会在程序运行的时候,找机会一起做清理工作。就像一个人一边做自己的事情,一边利用空闲时间打扫卫生,这样不会耽误太多正常做事的时间。

触发时机

  • 定时触发:GC 有个 "闹钟",每隔一段时间就会响,响了之后 GC 就会开始工作,检查有没有垃圾要清理。
  • 内存分配触发:当程序要分配新的内存空间,如果发现已经用掉的内存太多了,达到了一个规定的数量,就会马上让 GC 来清理垃圾,腾出空间

深拷贝与浅拷贝

  • 浅拷贝:浅拷贝只复制对象的一层属性。对于基本数据类型(如整数、字符串、布尔值等),会直接复制其值;而对于引用数据类型(如对象、数组),复制的是该引用数据类型的内存地址,而不是对象本身。这意味着浅拷贝后,新对象和原对象会共享引用数据类型的内部对象,修改其中一个对象的引用类型属性,另一个对象也会受到影响。
  • 深拷贝:深拷贝会递归地复制对象的所有层级属性。它会创建一个完全独立的新对象,新对象和原对象在内存中是完全分离的,拥有各自独立的属性值。对于引用数据类型,深拷贝会深入到对象内部,将其所有嵌套的对象也复制一份,因此对新对象的任何修改都不会影响原对象,反之亦然

函数和方法的区别

函数是独立的代码单元,不与特定类型绑定,直接通过函数名调用以完成通用任务;而方法是与特定类型关联的函数,需通过该类型的实例调用,用于操作该类型实例并处理其相关逻辑

Go函数参数传递到底是值传递还是引用传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数

Go 语言中有一些数据类型,如切片(slice)、映射(map)、通道(channel)等,当它们作为参数传递时,看起来好像是引用传递,但实际上仍然是值传递。这些类型本身是引用类型,它们的值包含了一个指向底层数据结构的指针,当作为参数传递时,传递的是这个指针的副本

为什么map遍历是无序的:

Go 语言的设计者有意让 map 遍历时无序,这主要是为了避免开发者在代码里依赖 map 元素的顺序。因为在不同版本的 Go 编译器或者运行环境下,map 内部的实现细节可能会发生变化,如果代码依赖了 map 元素的顺序,就可能导致程序行为不稳定。

map 底层是使用哈希表实现的,当你向 map 里插入元素时,元素会依据哈希函数的计算结果被放置到不同的桶(bucket)中。在遍历 map 时,Go 语言并不会按照元素插入的顺序或者键的顺序来访问,而是随机选择一个起始位置,然后按照内部的哈希表结构依次访问各个桶中的元素。并且,每次遍历 map 时,起始位置都是随机的,这就使得每次遍历的顺序都可能不同

slice扩容机制

array:指向底层数组的指针

len:切片的长度,表示切片当前包含的元素个数

cap:切片的容量,表示切片底层数组的长度

扩容的触发条件:

当向切片中添加元素时,如果切片的长度 len 等于其容量 cap,此时再添加元素就会触发扩容操作。常见的触发场景是使用 append 函数向切片中追加元素

go1.18之前:

  • 如果新的元素数量(即原切片长度加上要追加的元素数量)大于原切片容量的 2 倍,则直接将容量扩容为新元素数量。
  • 如果原切片容量小于 1024,则将容量翻倍。
  • 如果原切片容量大于等于 1024,则每次增加原容量的 1/4,直到容量足够为止

go1.18之后:

  • 如果新的元素数量大于原切片容量的 2 倍,则直接将容量扩容为新元素数量。
  • 否则,若原切片长度小于 256,则将容量翻倍。
  • 若原切片长度大于等于 256,则按照公式 newcap = oldcap + (oldcap + 3*256) / 4 进行扩容,直到容量足够为止

方法值类型接收者和指针类型接收者的区别

方法值类型接收者传递对象副本,方法内修改不影响原对象,调用时值和指针类型对象通用,适合无需修改接收者或对象较小场景;指针类型接收者传递对象地址,方法内修改影响原对象,虽值和指针类型对象通常都能调用但语义上指针更直接,适用于需修改接收者或对象较大场景

go的互斥锁抢锁的两种模式(sync.Mutex)

正常模式

  • 竞争规则:多个 goroutine 竞争锁时,新请求锁的 goroutine 与被唤醒等待锁的 goroutine 一起竞争。由于新请求锁的 goroutine 正在 CPU 上运行,在竞争中更具优势,能优先获得锁。
  • 性能特点:吞吐量较高,因为减少了 goroutine 进入和退出等待队列的上下文切换开销,但在高并发场景下,可能导致部分等待中的 goroutine 长时间获取不到锁,出现 "饥饿" 现象。

饥饿模式

  • 触发条件:当一个 goroutine 等待锁的时间超过 1 毫秒,互斥锁会从正常模式切换到饥饿模式。
  • 竞争规则:锁的所有权直接交给等待队列中最早等待的 goroutine,新请求锁的 goroutine 不会尝试获取锁,而是直接加入等待队列尾部。
  • 性能特点:保证了每个等待锁的 goroutine 最终都能获取到锁,避免了 "饥饿" 问题,但会降低系统整体吞吐量,因为新请求锁的 goroutine 无法立即获取锁,增加了上下文切换次数。

当等待队列中没有等待的 goroutine 或者最后一个等待的 goroutine 等待时间小于 1 毫秒时,互斥锁会从饥饿模式切换回正常模式

goroutine和线程的区别

1. 内存占用

  • 线程:栈空间初始固定且大,一般几兆,易造成内存浪费。
  • goroutine:栈空间动态分配,初始小(约 2KB),按需伸缩,节省内存。

2. 创建销毁开销

  • 线程:涉及内核操作,开销大。
  • goroutine:由 Go 运行时管理,开销极小。

3. 调度

  • 线程:由操作系统内核抢占式调度,上下文切换开销大。
  • goroutine:Go 运行时调度,用户态切换,开销小。

4. 通信

  • 线程:多靠共享内存,需同步机制,易有安全问题。
  • goroutine:用通道通信,避免数据竞争,更安全简单。

5. 并发规模

  • 线程:数量受限,几百到几千。
  • goroutine:可轻松创建数万个甚至更多

goroutine泄露

原因

  • 无限循环:Goroutine 里无退出条件的循环,持续运行不停止。
  • 通道阻塞:发送数据到已满通道且无接收操作,或从空且未关闭通道接收数据。
  • 锁未释放:加锁后没解锁,使其他等锁的 Goroutine 阻塞。

危害

  • 耗尽内存和 CPU 资源。
  • 增加调度负担,降低程序性能。
  • 严重时导致程序崩溃。

解决办法

  • 给循环设退出条件,可用通道控制。
  • 合理处理通道收发,避免阻塞。
  • 确保锁正确加锁和解锁

GMP调度原理

GMP 是 Go 语言运行时(runtime)实现的一种调度模型,用于高效地管理和调度 goroutine,其中 G 代表 goroutine,M 代表操作系统线程,P 代表处理器(逻辑处理器)。以下详细介绍 GMP 调度原理。

基本概念

  • G(Goroutine):Go 语言中轻量级的执行单元,由 Go 运行时管理,初始栈空间小且可动态伸缩,创建和销毁开销低。
  • M(Machine):对应一个操作系统线程,是执行计算的实际载体,负责执行 G。
  • P(Processor):处理器,它提供了执行 G 所需的上下文环境,每个 P 都有一个本地的运行队列,用于存放待执行的 G。

调度流程

1. 启动阶段
  • 程序启动时,Go 运行时会创建一定数量的 M 和 P,M 的数量通常由操作系统的线程数和 Go 程序的配置决定,P 的数量默认等于 CPU 的核心数,可以通过 GOMAXPROCS 环境变量或 runtime.GOMAXPROCS 函数进行调整。
2. 创建 Goroutine
  • 当使用 go 关键字创建一个新的 goroutine 时,这个新的 G 会被放入当前 P 的本地运行队列中。如果本地运行队列已满,G 会被放入全局运行队列中。
3. 调度执行
  • 本地队列调度:M 会从关联的 P 的本地运行队列中取出一个 G 并执行。当一个 M 执行完一个 G 后,会继续从本地队列中获取下一个 G 执行。
  • 全局队列调度:如果 P 的本地运行队列为空,M 会尝试从全局运行队列中获取 G。为了保证公平性,全局队列中的 G 会被均摊到各个 P 的本地队列中。
  • 偷取调度:如果全局运行队列也为空,M 会随机选择一个其他的 P,并从其本地队列中 "偷取" 一半的 G 到自己关联的 P 的本地队列中执行,这种机制保证了各个 P 的负载均衡。
4. 阻塞和唤醒
  • 阻塞:当一个 G 发生阻塞操作(如进行 I/O 操作、加锁等)时,M 会将当前 G 挂起,然后从本地队列或全局队列中选择另一个 G 继续执行。如果本地队列和全局队列都为空,M 会进入休眠状态。
  • 唤醒:当阻塞的 G 操作完成后,它会被唤醒,并被放入某个 P 的本地队列中等待再次执行。
5. 系统调用
  • 当 G 进行系统调用时,M 会和 P 分离,带着进行系统调用的 G 一起进入系统调用状态。如果此时还有其他可运行的 G 在 P 的本地队列中,会有新的 M 被调度来和这个 P 绑定,继续执行队列中的 G。当系统调用完成后,进行系统调用的 G 会尝试重新找一个空闲的 P 继续执行,如果没有空闲的 P,这个 G 会被放入全局队列中。

调度优势

  • 高效并发:通过轻量级的 goroutine 和 M:N 的调度模型,避免了频繁的内核态和用户态切换,减少了上下文切换的开销,提高了并发性能。
  • 负载均衡:偷取调度机制保证了各个 P 的负载均衡,充分利用了多核 CPU 的计算能力。
  • 灵活调度:能够根据 goroutine 的状态和系统资源的使用情况,动态地调整调度策略,提高了系统的整体性能和资源利用率

什么是通道,它的底层是什么

通道是什么

通道(channel)是 Go 语言用于 goroutine 间通信和同步的工具。就像一个传送带,一端的 goroutine 可以把数据放上去(发送数据),另一端的 goroutine 能从上面取走数据(接收数据),保障数据安全有序传递,避免数据竞争问题。

通道底层结构

通道底层是个结构体,关键部分及作用如下:

  • 缓冲区:类似传送带的承载区域,有大小限制。无缓冲通道就像没有承载区域的传送带,数据得立刻被取走;有缓冲通道则能暂存一定数量的数据。
  • 队列:有两个队列,一个存等待发送数据的 goroutine,另一个存等待接收数据的 goroutine。当通道条件不满足(如满了或空了),goroutine 会进入对应队列等待。
  • :好比传送带的安全开关,保证同一时间只有一个 goroutine 能操作通道,避免混乱。
  • 状态标记:记录通道是否关闭等状态信息

生产者和消费者怎么关闭管道

基本规则

Go 里通常由生产者关闭管道,因为它知道何时不再产生数据,消费者能通过管道关闭信号得知数据传输结束。

不同场景关闭方式

  1. 单生产者单消费者 :生产者完成数据发送后,用 close(chan) 关闭管道。消费者用 for...range 接收数据,管道关闭且数据取完,循环自动结束。例如面包师傅做完面包就关店门,顾客看到店门关且没面包就不再等。
  2. 需外部控制时 :创建信号通道,生产者用 select 监听。收到关闭信号就关闭数据管道。如面包师傅收到停电通知就停止做面包并关店。
  3. 多生产者单 / 多消费者 :用 sync.WaitGroup 跟踪生产者。所有生产者完成后,通过一个 goroutine 关闭管道。像多个送报小组都送完报,统一锁报箱

如何检测通道关闭

在 Go 里检测通道关闭有三种常用办法:

  1. for...range 循环 :用 for num := range ch 接收数据,通道关闭且数据收完,循环自动结束,很适合持续从通道取数据的场景。
  2. 多值接收 :使用 v, ok := <-choktrue 表示正常收到数据,为 false 说明通道已关且无数据。
  3. select 语句 :在 select 里结合多值接收,case v, ok := <-ch ,依据 ok 值判断通道是否关闭

通道和锁的区别

设计理念

  • 通道:以通信方式共享内存,通过传递数据协调并发。
  • 锁:共享内存来通信,用加解锁控制对共享资源的访问。

使用场景

  • 通道:适合数据传递、多协程协作和同步场景。
  • 锁:用于保护共享资源,避免多协程同时读写引发冲突。

并发控制

  • 通道:自动处理阻塞和同步,协程通过通道收发数据。
  • 锁:需手动控制加锁和解锁,易出现死锁问题。

代码复杂度

  • 通道:代码简洁,逻辑清晰,提高可读性。
  • 锁:复杂场景下需精细管理,增加代码维护难度。

性能表现

  • 通道:高并发时避免锁竞争,但有一定通信开销。
  • 锁:操作轻量,高并发竞争激烈时性能下降

数组和切片的区别

定义和长度特性

  • 数组:定义时必须明确指定长度,且该长度固定,不可更改。数组长度属于其类型的一部分,不同长度的数组属于不同类型。
  • 切片:是对数组的抽象,长度可在运行时动态改变。它本身不存储数据,而是指向一个底层数组。

内存分配

  • 数组:定义时会分配连续的内存空间来存储所有元素,内存大小在编译时就已确定。作为函数参数传递时,会复制整个数组,可能造成较大内存开销。
  • 切片:是轻量级数据结构,由指向底层数组的指针、切片长度和容量构成。内存分配主要取决于底层数组。作为函数参数传递时,传递的是切片结构体本身,不复制底层数组,开销小。

长度和容量

  • 数组:只有长度概念,即元素数量,没有容量概念,因为长度固定。
  • 切片:有长度和容量两个属性。长度指当前元素数量,容量是底层数组从切片起始位置开始的最大元素数量。向切片追加元素,长度超容量时会自动扩容,通常重新分配更大底层数组并复制原元素。

使用灵活性

  • 数组:因长度固定,使用不够灵活,若要存储不同数量元素,可能需重新定义数组。
  • 切片:动态长度特性使其使用更灵活,可方便进行元素追加、删除和修改操作。
相关推荐
清北_1 小时前
万科000002
golang
今天也想MK代码1 小时前
写好简历的三个关键认知
面试·职场和发展
兮动人2 小时前
Golang 执行流程分析
开发语言·后端·golang·golang 执行流程分析
好记性+烂笔头2 小时前
4 Hadoop 面试真题
大数据·hadoop·面试
zhuyasen3 小时前
多维度详细比较 kratos、go-zero、goframe、sponge 框架
后端·http·微服务·rpc·golang
特立独行的猫a6 小时前
Golang 应用的 Docker 部署方式介绍及使用详解
开发语言·docker·golang
鲨鱼 Fish7 小时前
JVM运行时数据区域-附面试题
java·开发语言·jvm·面试
慕璃嫣7 小时前
Clojure语言的系统运维
开发语言·后端·golang