互联网的就业环境越来越差了,应小伙伴们的强烈建议,你们要的Go面试题汇总他来了。
我们平常在工作中除了撸好代码,跑通项目之外,还要注意内外兼修。内功和招式都得练👌,才能应对突如其来的变故,顺利的拿到新的offer。
值类型和引用类型
值类型有哪些?
基本数据类型都是值类型,包括:int系列、float系列、bool、字符串、数组、结构体struct。
引用类型有哪些?
指针、切片slice、接口interface、管道channel以及map
值类型和引用类型的区别?
- 值类型在内存中存储的是值本身,而引用类型在内存中存储的是值的内存地址。
- 值类型内存通常在栈中分配,引用类型内存通常在堆中分配。
垃圾回收
引用类型的内存在堆中分配,当没有任何变量引用堆中的内存地址时,该内存地址对应的数据存储空间就变成了垃圾,就会被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==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := 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的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的是类型本身(类型本身就是引用类型(指针类型));
- 而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和线程的区别?
- 内存占用:Goroutine初始栈约2KB且可动态扩展,线程栈通常固定2MB
- 调度方式:Goroutine由Go运行时调度,线程由OS内核调度
- 切换成本:Goroutine切换只需120ns,线程切换需1-2μs
- 通信机制:Goroutine通过channel通信,线程通过共享内存需要同步原语
Channel的缓冲机制?
ch := make(chan int) // 无缓冲通道(同步通道)
bufCh := make(chan int, 3) // 缓冲容量为3的通道
- 无缓冲通道:发送接收操作会阻塞直到配对操作就绪
- 有缓冲通道:缓冲区满时发送阻塞,缓冲区空时接收阻塞
sync.Map的实现原理?
- 空间换时间:通过两个map(read和dirty)实现读写分离
- 原子操作:使用atomic.Value保证read map的原子访问
- 延迟删除:删除操作先标记再物理删除
- 适用场景:多读少写、键值对变化少的场景
接口与反射
空接口与类型断言
var i interface{} = "hello"
s := i.(string) // 直接断言
s, ok := i.(string) // 安全断言
switch v := i.(type) { // 类型switch
case string:
fmt.Println(v)
}
接口的底层实现?
Go接口采用双指针结构:
- 类型指针:指向接口动态类型信息
- 数据指针:指向实际数据值
错误处理
panic/recover机制要点
- recover必须在defer函数中调用才有效
- panic会逆序执行当前goroutine的defer链
- 未捕获的panic会导致程序崩溃
典型应用模式:
func SafeRun() { defer func() { if err := recover(); err != nil { log.Println("捕获到panic:", err) } }() // 可能引发panic的代码 }
高级特性
Context包的核心作用
- 链路控制:传递请求上下文信息(如traceID)
超时控制:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel()
- 取消传播:通过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,备注:sf面试群。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。