写点什么

GO 必知必会面试题汇总

作者:王中阳Go
  • 2025-02-14
    湖南
  • 本文字数:3520 字

    阅读完需:约 12 分钟

GO必知必会面试题汇总

互联网的就业环境越来越差了,应小伙伴们的强烈建议,你们要的 Go 面试题汇总他来了。


我们平常在工作中除了撸好代码,跑通项目之外,还要注意内外兼修。内功和招式都得练👌,才能应对突如其来的变故,顺利的拿到新的 offer。


值类型和引用类型

值类型有哪些?

基本数据类型都是值类型,包括:int 系列、float 系列、bool、字符串、数组、结构体 struct。

引用类型有哪些?

指针、切片 slice、接口 interface、管道 channel 以及 map

值类型和引用类型的区别?

  1. 值类型在内存中存储的是值本身,而引用类型在内存中存储的是值的内存地址。

  2. 值类型内存通常在栈中分配,引用类型内存通常在堆中分配。

垃圾回收

引用类型的内存在堆中分配,当没有任何变量引用堆中的内存地址时,该内存地址对应的数据存储空间就变成了垃圾,就会被 GO 语言的 GC 回收。

一图胜千言


堆和栈

在 Go 中,栈的内存是由编译器自动进行分配和释放,栈区往往存储着函数参数、局部变量和调用函数帧,它们随着函数的创建而分配,函数的退出而销毁。


一个 goroutine 对应一个栈,栈是调用栈(call stack)的简称。一个栈通常又包含了许多栈帧(stack frame),它描述的是函数之间的调用关系,每一帧对应一次尚未返回的函数调用,它本身也是以栈形式存放数据。

与栈不同的是,应用程序在运行时只会存在一个堆。狭隘地说,内存管理只是针对堆内存而言的。程序在运行期间可以主动从堆上申请内存,这些内存通过 Go 的内存分配器分配,并由垃圾收集器回收。

切片

比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。


切片唯一合法的比较操作是和 nil 比较。

比较的详解

要检查切片是否为空,应该使用


len(s) == 0
复制代码


来判断,而不应该使用


s == nil
复制代码


来判断。


原因是:一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是 0。但是我们不能说一个长度和容量都是 0 的切片一定是 nil。


我们通过下面的示例就很好理解了:


var s1 []int            //len(s1)=0;cap(s1)=0;s1==nils2 := []int{}           //len(s2)=0;cap(s2)=0;s2!=nils3 := make([]int, 0)    //len(s3)=0;cap(s3)=0;s3!=nil
复制代码


所以要判断一个切片是否是空的,要是用 len(s) == 0 来判断,不应该使用 s == nil 来判断。


其根本原因在于后面两种初始化方式已经给切片分配了空间,所以就算切片为空,也不等于 nil。但是 len(s) == 0 成立,则切片一定为空。


注意:在 go 中 var 是声明关键字,不会开辟内存空间;使用 := 或者 make 关键字进行初始化时才会开辟内存空间。

深拷贝和浅拷贝

操作对象

深拷贝和浅拷贝操作的对象都是 Go 语言中的引用类型

区别如下:

引用类型的特点是在内存中存储的是其他值的内存地址;而值类型在内存中存储的是真实的值。


我们在 go 语言中通过 := 赋值引用类型就是 浅拷贝,即拷贝的是内存地址,两个变量对应的是同一个内存地址对应的同一个值。


a := []string{1,2,3} b := a
复制代码


如果我们通过 copy()函数进行赋值,就是深拷贝,赋值的是真实的值,而非内存地址,会在内存中开启新的内存空间。


举例如下:


a := []string{1,2,3} b := make([]string,len(a),cap(a)) copy(b,a)
复制代码

new 和 make

new

new 是 GO 语言一个内置的函数,它的函数签名如下:


func new(Type) *Type
复制代码

特点

  • Type 表示类型,new 函数只接受一个参数,这个参数是一个类型

  • *Type 表示类型指针,new 函数返回一个指向该类型内存地址的指针。


new 函数不太常用,使用 new 函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。

举个例子:

func main() {    a := new(int)     b := new(bool)    fmt.Printf("%T\n", a) // *int     fmt.Printf("%T\n", b) // *bool     fmt.Println(*a) // 0     fmt.Println(*b) // false }
复制代码

使用技巧

var a *int 只是声明了一个指针变量 a 但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。


应该按照如下方式使用内置的 new 函数对 a 进行初始化之后就可以正常对其赋值了:


    func main() {    var a *int    a = new(int)    *a = 10    fmt.Println(*a)}
复制代码

make

make 也是用于内存分配的,区别于 new,它只用于 slice、map 以及 channel 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型(指针类型),所以就没有必要返回他们的指针了。

make 函数的函数签名

func make(t Type, size ...IntegerType) Type
复制代码

特点

make 函数是无可替代的,我们在使用 slice、map 以及 channel 的时候,都需要使用 make 进行初始化,然后才可以对它们进行操作。

使用技巧

var b map[string]int这段代码,只是声明变量 b 是一个 map 类型的变量,需要像下面的示例代码一样使用 make 函数进行初始化操作之后,才能对其进行键值对赋值:


func main() {     var b map[string]int     b = make(map[string]int, 10)     b["分数"] = 100     fmt.Println(b)}
复制代码

总结:new 与 make 的区别

  1. 二者都是用来做内存分配的。

  2. make 只用于 slice、map 以及 channel 的初始化,返回的是类型本身(类型本身就是引用类型(指针类型));

  3. 而 new 用于内存分配时,在内存中存储的是对应类型的型零值(比如 0,false),返回的是该类型的指针类型。

Go 的 map 如何实现排序

我们知道 Go 语言的 map 类型底层是由 hash 实现的,是无序的,不支持排序。


如果我们的数据使用 map 类型存储,如何实现排序呢?

解决思路

排序 map 的 key,再根据排序后的 key 遍历输出 map 即可。

代码实现:

package main
import ( "fmt" "math/rand" "sort" "time")
func main() { rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 30)
for i := 0; i < 30; i++ { key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串 value := rand.Intn(30) //生成0~50的随机整数 scoreMap[key] = value } //取出map中的所有key存入切片keys var keys = make([]string, 0, 30) for key := range scoreMap { keys = append(keys, key) } //对切片进行排序 sort.Strings(keys) //按照排序后的key遍历map for _, key := range keys { fmt.Println(key, scoreMap[key]) }}
复制代码

运行结果


搞定,非常顺滑!

并发编程

Goroutine 和线程的区别?

  1. 内存占用:Goroutine 初始栈约 2KB 且可动态扩展,线程栈通常固定 2MB

  2. 调度方式:Goroutine 由 Go 运行时调度,线程由 OS 内核调度

  3. 切换成本:Goroutine 切换只需 120ns,线程切换需 1-2μs

  4. 通信机制:Goroutine 通过 channel 通信,线程通过共享内存需要同步原语

Channel 的缓冲机制?

ch := make(chan int)    // 无缓冲通道(同步通道)bufCh := make(chan int, 3) // 缓冲容量为3的通道
复制代码


  • 无缓冲通道:发送接收操作会阻塞直到配对操作就绪

  • 有缓冲通道:缓冲区满时发送阻塞,缓冲区空时接收阻塞

sync.Map 的实现原理?

  1. 空间换时间:通过两个 map(read 和 dirty)实现读写分离

  2. 原子操作:使用 atomic.Value 保证 read map 的原子访问

  3. 延迟删除:删除操作先标记再物理删除

  4. 适用场景:多读少写、键值对变化少的场景

接口与反射

空接口与类型断言

var i interface{} = "hello"s := i.(string)        // 直接断言s, ok := i.(string)    // 安全断言switch v := i.(type) { // 类型switchcase string:    fmt.Println(v)}
复制代码

接口的底层实现?

Go 接口采用双指针结构


  • 类型指针:指向接口动态类型信息

  • 数据指针:指向实际数据值

错误处理

panic/recover 机制要点

  1. recover 必须在 defer 函数中调用才有效

  2. panic 会逆序执行当前 goroutine 的 defer 链

  3. 未捕获的 panic 会导致程序崩溃

  4. 典型应用模式:


func SafeRun() {    defer func() {        if err := recover(); err != nil {            log.Println("捕获到panic:", err)        }    }()    // 可能引发panic的代码}
复制代码

高级特性

Context 包的核心作用

  1. 链路控制:传递请求上下文信息(如 traceID)

  2. 超时控制


ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()
复制代码


  1. 取消传播:通过 Done()通道实现级联取消

defer 的执行顺序

遵循 LIFO 原则(后进先出):


func main() {    defer fmt.Println(1)    defer fmt.Println(2)    // 输出顺序:2 -> 1}
复制代码


注意:defer 后的函数参数会立即求值

性能优化

内存对齐原则

通过调整结构体字段顺序减少内存占用:


// 优化前(占用24字节)type Bad struct {    a bool    b int64    c bool}
// 优化后(占用16字节)type Good struct { a bool c bool b int64}
复制代码

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:infoq 面试群。

发布于: 2 小时前阅读数: 2
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
GO必知必会面试题汇总_Go_王中阳Go_InfoQ写作社区