【Go语言基础面试题】

devtools/2024/9/19 0:43:49/ 标签: golang, 开发语言, 面试, 基础知识

1.基础语法篇

1.1 =和:=的区别

  • =是赋值
  • :=是声明+赋值

1.2. 指针的作用?

指针用来保存变量的地址

  • 解引用运算符*用于访问地址中的值
  • 地址运算符&用于返回变量的地址

1.3. Go允许多个返回值吗?

允许,返回两个string。func swap(x, y string) (string, string)

1.4. Go有异常类型吗?

没有异常类型,只有错误类型Error

1.5. 什么是协程(Goroutine)?

Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。

1.6. 如何高效地拼接字符串

利用strings.Builder

1.7. 什么是rune类型

rune是int32类型的别称,是Unicode编码的别称
Go语言中字符串的底层表示是byte(8 bit)序列,而不是rune(32 bit)序列
正常情况下中文会使用utf-8编码,每个中文字符需要3byte

fmt.Println(len("Go语言")) // 8
fmt.Println(len([]rune("Go语言"))) // 4

将字符串转换为[]rune可以进行准确的子串截取

1.8. 如何判断map中是否包含某个key?

if val, ok := dict["foo"]; ok {//do something here}

ok为true代表存在包含key的value

1.9. Go支持默认参数或可选参数吗?

  • Go不支持默认参数和可选参数
  • 如何实现默认参数和可变参数?
    • 创建一个结构体类型来封装相关的参数,并在函数中接受指向该结构体的指针。这样可以在结构体定义中为字段提供默认值,调用者可以选择性地初始化部分或全部字段。
    • 使用变长参数,虽然变长参数本身并不直接提供默认值,但可以结合函数内部逻辑来实现类似功能。通过检查传入的参数数量,可以决定是否使用预设的默认值。

1.10. defer的执行顺序

  • 多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。
  • defer在return语句之后执行,return中存在表达式时会先计算表达式的值之后赋值给返回变量,之后开始执行defer

1.11. 如何交换2个变量的值?

a, b := "A", "B"
a, b = b, a

1.12. Go语言tag的用处?

tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。

1.13. 如何判断2个字符串切片(slice)是否相等

  • 可以通过reflect.DeepEqual(a,b)判断a、b两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
  • 通常采用的方式如下,遍历比较切片中的每一个元素

1.14. 字符串打印时,%v 和 %+v 的区别

%v 和 %+v 都可以用来打印 struct 的值,区别在于 %v 仅打印各个字段的值,%+v 还会打印各个字段的名称。

1.15. Go 语言中如何表示枚举值(enums)

使用const和iota来进行

type StuType int32
const (Type1 StuType = iotaType2Type3Type4
)

1.16. 空struct{} 的用途

  • 使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
  • 比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
  • 再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。
  • 再比如,声明只包含方法的结构体

2. 实现原理篇

2.1 init() 函数是什么时候执行的?

init() 函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。

  • 每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。
  • 同一个包,甚至是同一个源文件可以有多个 init() 函数。
  • init() 函数没有入参和返回值,不能被其他函数调用。
  • 同一个包内多个 init() 函数的执行顺序不作保证。

一句话总结: import –> const –> var –> init() –> main()

2.2 Go 语言的局部变量分配在栈上还是堆上?

Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。

2.3 2 个 interface 可以比较吗?

可以。Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 == 或 != 比较。
2 个 interface 相等有以下 2 种情况

  • 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
  • 类型 T 相同,且对应的值 V 相等。

2.4 两个 nil 可能不相等吗?

可能。
接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

  • 两个接口值比较时,会先比较 T,再比较 V。
  • 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() {var p *int = nilvar i interface{} = pfmt.Println(i == p) // truefmt.Println(p == nil) // truefmt.Println(i == nil) // false
}

上面这个例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil)

  • i 与 p 作比较时,将 p 转换为接口后再比较,因此 i 等于 p
  • p 与 nil 比较,直接比较值,所以 p 等于 nil。
  • 但是当 i 与 nil 比较时,会将 nil 转换为接口 (T=nil, V=nil),与i (T=*int, V=nil) 不相等,因此 i != nil。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。

2.5 简述 Go 语言GC(垃圾回收)的工作原理

最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),

2.5.1 标记清除算法

Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。

标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

  • 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象
  • 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表

标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。

三色标记算法将程序中的对象分成白色、黑色和灰色三类。

白色:不确定对象。
灰色:存活对象,子对象待处理。
黑色:存活对象。

标记开始时,所有对象加入白色集合(这一步需 STW )。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止,标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。

三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。

三色标记法存在的另外一个问题。即在GC过程中,对象指针发生了改变。比如:

A (黑) -> B (灰) -> C (白) -> D (白)

正常情况下,D对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了 C 对 D 的引用,而 A 获得了 D 的引用。标记继续进行,D 就没有机会被标记为黑色了(A 已经处理过,这一轮不会再被处理)。

A (黑) -> B (灰) -> C (白) ↓
D (白)

为了解决这个问题,Go 使用了内存屏障技术,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。

总结:
一次完整的GC分为四个阶段:
1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
2)使用三色标记法标记(Marking, 并发)
3)标记结束(Mark Termination,需 STW),关闭写屏障。
4)清理(Sweeping, 并发)

2.5.2 引用计数(Reference Count)

这个在之后的博客里面补吧

2.6 函数返回局部变量的指针是否安全?

这在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。

2.7 非接口非接口的任意类型 T() 都能够调用 *T 的方法吗?反过来呢?

  • 一个T类型的值可以调用为*T类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下。
  • 反过来,一个T类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型 T 声明的方法,编译器都会为类型T自动隐式声明一个同名和同签名的方法。

不可寻址的值的

  • 字符串中的字节
  • map 对象中的元素(slice 对象中的元素是可寻址的,slice的底层是数组)
  • 常量
  • 包级别的函数等

3. 并发编程篇

3.1 无缓冲的 channel 和 有缓冲的 channel 的区别?

对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。

对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。

3.2 什么是协程泄露(Goroutine Leak)?

协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。
协程泄露的常见原因:

  • 缺少接收器,导致发送阻塞
  • 缺少发送器,导致接收阻塞
  • 死锁(dead lock)
  • 无限循环(infinite loops)

3.3 Go 可以限制运行时操作系统线程的数量吗?

可以使用环境变量 GOMAXPROCS 或 runtime.GOMAXPROCS(num int) 设置。
GOMAXPROCS 限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。GOMAXPROCS 的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。
因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。
对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。

4. 代码输出篇

4.1 常量与变量

4.1.1 case1

下列代码的输出是:

func main() {const (a, b = "golang", 100d, ef bool = trueg)fmt.Println(d, e, g)
}

输出:golang 100 true
在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。

4.1.2 case2

下列代码的输出是:

func main() {const N = 100var x int = Nconst M int32 = 100var y int = Mfmt.Println(x, y)
}

编译失败:cannot use M (type int32) as type int in assignment
Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N。但是对于有类型的常量 const M int32 = 100,赋值给其他变量时,需要类型匹配才能成功,所以需要显示地类型转换

4.1.3 case3

下列代码的输出是:

func main() {var a int8 = -1var b int8 = -128 / afmt.Println(b)
}

输出:-128
int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。

4.1.4 case4

下列代码的输出是:

func main() {const a int8 = -1var b int8 = -128 / afmt.Println(b)
}

编译失败:constant 128 overflows int8
-128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败。

4.2 作用域

func main() {var err errorif err == nil {err := fmt.Errorf("err")fmt.Println(1, err)}if err != nil {fmt.Println(2, err)}
}

输出:1 err

4.3 defer延迟调用

4.3.1 case1

type T struct{}func (t T) f(n int) T {fmt.Print(n)return t
}func main() {var t Tdefer t.f(1).f(2)fmt.Print(3)
}

输出:132
defer 延迟调用时,需要提前保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1) 直接执行,然后执行 fmt.Print(3),最后函数返回时再执行 .f(2),因此输出是 132。

4.3.2 case2

func f(n int) {defer fmt.Println(n)n += 100
}func main() {f(1)
}

输出:1
打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。

4.3.3 case3

func main() {n := 1defer func() {fmt.Println(n)}()n += 100
}

输出:101
匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。

4.3.4 case4

func main() {n := 1if n == 1 {defer fmt.Println(n)n += 100}fmt.Println(n)
}

输出:

101
1

先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。


http://www.ppmy.cn/devtools/4297.html

相关文章

边缘计算网关有哪些用途及使用方法?-天拓四方

在数字化日益深入的今天,边缘计算网关作为一种重要的设备,正在越来越多地被应用于各种场景中。它不仅能够提升数据处理的速度和效率,还能在降低网络延迟的同时确保数据的安全性。本文将详细介绍边缘计算网关的用途及其使用方法,帮…

软考128-上午题-【软件工程】-白盒测试

一、白盒测试(结构测试) 白盒测试也称为结构测试,根据程序的内部结构和逻辑来设计测试用例,对程序的路径和过程进行测试,检查是否满足设计的需要。 白盒测试常用的技术是:逻辑覆盖、循环覆盖和基本路径测…

Labview2024安装包(亲测可用)

目录 一、软件简介 二、软件下载 一、软件简介 LabVIEW是一种由美国国家仪器(NI)公司开发的程序开发环境,它显著区别于其他计算机语言,如C和BASIC。传统的计算机语言是基于文本的语言来产生代码,而LabVIEW则采用图形化…

vscode i18n Ally插件配置项

.vscode文件: {"i18n-ally.localesPaths": ["src/lang"], //显示语言, 这里也可以设置显示英文为en,// 如下须要手动配置"i18n-ally.keystyle": "nested", // 翻译路径格式 (翻译后变量格式 nested&#xff1a…

Llama3本地部署实现模型对话

1. 从github下载目录文件 https://github.com/meta-llama/llama3 使用git下载或者直接从github项目地址下载压缩包文件 git clone https://github.com/meta-llama/llama3.git2.申请模型下载链接 到Meta Llama website填写表格申请,国家貌似得填写外国,组织随便填写即可 3.…

力扣110. 平衡二叉树

思路:与二叉树最大高度类似,但是这里需要返回 -1 的高度来标识不是平衡二叉树,判断左右子树的高度相差大于1则不平衡,否则就是平衡。 class Solution {public boolean isBalanced(TreeNode root) {int ans func(root);if(ans >…

docker基础

常见命令 Docker最常见的命令就是操作镜像、容器的命令,详见官方文档: Docker Docs 镜像拉取:docker pull本地镜像查询:docker images本地镜像移除:docker rmi自定义镜像:docker build镜像保存本地&#…

mongodbTemplate 修改JSON [key: ‘1‘, key2: [{id:1, name: ‘name‘}] 中 key2.name属性

问题描述 mongodbTemplate 修改JSON [key: ‘1‘, key2: [{id:1, name: ‘name‘}] 中 key2.name属性 代码 Query query Query.query(Criteria.where("key").is(1) .and("key2.id").is(1) …

clang:C++ 编程入门

简单来说,Clang是一个编译器,目前用来编译C、C、Objective-C语言。 where clang D:\Swift\Toolchains\5.10.0Asserts\usr\bin\clang.exe clang -v clang version 16.0.0 Target: x86_64-unknown-windows-msvc Thread model: posix 计算 斐波那契数列&…

开源模型应用落地-LangChain高阶-QWen1.5-外部实时数据

一、前言 通过langchain框架调用本地模型,使得用户可以直接提出问题或发送指令,而无需担心具体的步骤或流程。langchain会自动将任务分解为多个子任务,并将它们传递给适合的语言模型进行处理。 本篇将通过LangChain调用外部心知天气API,并将结果返回给QWen1.5模型进行加工处…

TDengine taosAdapter启用HTTPS

HTTPS (Hypertext Transfer Protocol Secure ),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加…

Servlet-Filter实现反爬虫

以前用DotNetCore实现过反爬虫功能。在tomcat里面可以利用Servlet的Filter类实现请求的控制来达到反爬虫功能,进而增强JRT的web安全。 实现黑名单过滤器,对在黑名单列表的IP的所有请求都跳转到警告页面,业务各种请求自行定义加入黑名单 /* …

QA测试开发工程师面试题满分问答15: 讲一讲InnoDB和MyISAM

InnoDB和MyISAM是MySQL中两种常见的存储引擎,它们在数据存储和处理方面有着显著的区别。让我们逐一来看一下它们的区别、原理以及适用场景。 区别: 事务支持:InnoDB是一个支持事务的存储引擎,而MyISAM不支持事务。事务是一种用于维…

C++实战——日期类的实现

日期类的实现 前言一、日期类概念实现运用场景 二、日期类的具体实现代码构造函数拷贝构造函数获取日期(内联函数)赋值加等减等加减小于小于等于大于大于等于相等不相等前置后置前置- -后置- -关于类里重载的比较运算符为什么要加外部const示例 Date.hDa…

Rust常用特型之TryFrom和TryInto特型

在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。 我们前面学习了了From和Into特型,今天我们来学们一下两个相似的特型TryFrom和TryInto。看名字就知道他们是试图转换的意思,那为什么有试图转换…

浏览器——Microsoft Edge

Microsoft Edge 浏览器具有诸多功能特点和使用技巧 核心知识点和实用心得摘要: 性能优化: 睡眠标签:Edge 浏览器引入了睡眠标签功能,旨在降低内存占用和CPU使用率。当标签页长时间未活动时,系统会自动将其置于睡眠状态…

18. TypeScript的配置 tsconfig.json

1.为什么需要tsconfig.json tsconfig.json文件是TypeScript项目的重要组成部分,它定义了项目的根文件和编译器选项。通过这个文件,我们可以控制TypeScript编译器的行为,以满足我们的需求。 2. tsconfig.json文件的基本结构 tsconfig.json是…

vue2中数组的变更检测

一、数组的变更检测 之前我们在学习深入data属性的知识中,提及到Vue2中通过Object.defineProperty()方法实现数据响应、数据绑定。但是Object.defineProperty()方法有一个缺点,就是Object.defineProperty()在数组内部数据变动的时候,不能监听…

golang-基础语法

make 和 new 的区别 make 和 new 都是用来分配内存 make 只能对 slice map channel 进行初始化结构体实例。new 可以对任意类型进行初始化make 用于分配数据对象的具体实例,new 用于分配数据类型的默认值,并返回该数据的指针。 new 出来的 slice 、ma…

数据结构之单链表相关刷题

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程(ಥ_ಥ)-CSDN博客 所属专栏:数据结构 数据结构之单链表的相关知识点及应用-CSDN博客 下面题目基于上面这篇文章: 下面有任何不懂的地方欢迎在评论区留言或…