文章目录
- 目录
-
- [一、Go 核心特性](#一、Go 核心特性)
- [二、Go 基础语法](#二、Go 基础语法)
-
- [1. 变量声明(3种方式)](#1. 变量声明(3种方式))
- [2. 函数(多返回值+匿名函数)](#2. 函数(多返回值+匿名函数))
- [3. 接口(隐式实现)](#3. 接口(隐式实现))
- [4. 并发(Goroutine + Channel)](#4. 并发(Goroutine + Channel))
- [5. defer 延迟执行](#5. defer 延迟执行)
- [三、Go 高频面试题及答案(含代码+解析)](#三、Go 高频面试题及答案(含代码+解析))
-
- [面试题 1:Goroutine 和 OS 线程的区别?](#面试题 1:Goroutine 和 OS 线程的区别?)
- [面试题 2:defer 的执行顺序是什么?return 和 defer 的执行逻辑?](#面试题 2:defer 的执行顺序是什么?return 和 defer 的执行逻辑?)
- [面试题 3:slice 和 array 的区别?slice 扩容机制是什么?](#面试题 3:slice 和 array 的区别?slice 扩容机制是什么?)
- [面试题 4:nil 接口和 nil 结构体的区别?](#面试题 4:nil 接口和 nil 结构体的区别?)
- [面试题 5:channel 无缓冲和有缓冲的区别?关闭 channel 后读写会发生什么?](#面试题 5:channel 无缓冲和有缓冲的区别?关闭 channel 后读写会发生什么?)
- 四、面试复习总结
目录
若对您有帮助的话,请点赞收藏加关注哦,您的关注是我持续创作的动力!有问题请私信或联系邮箱:funian.gm@gmail.com
Go(Golang)是 Google 设计的静态强类型语言,主打简洁、高效、并发友好,广泛用于微服务、云原生、中间件开发。

一、Go 核心特性
| 特性名称 | 核心说明 | 面试高频考点 |
|---|---|---|
| 静态强类型+类型推断 | 编译时校验类型,支持 var a = 10(自动推断为 int),无需显式声明类型 |
1. 静态类型 vs 动态类型区别;2. var 和 := 的差异 |
| 简洁语法 | 无分号(自动补全)、无类继承、少关键字(仅 25 个),避免冗余语法 | 1. 为什么 Go 不支持类继承?2. 语法简洁带来的开发优势 |
| 原生并发模型(Goroutine) | 轻量级线程(栈初始 2KB,可动态扩缩),由 Go 运行时(GOMAXPROCS)调度,而非 OS 内核 | 1. Goroutine 与 OS 线程的区别;2. GOMAXPROCS 作用;3. Goroutine 调度原理 |
| 通信机制(Channel) | 用于 Goroutine 间安全通信("不要通过共享内存通信,而通过通信共享内存") | 1. Channel 无缓冲 vs 有缓冲的区别;2. Channel 关闭后读取/写入的行为;3. 如何用 Channel 实现同步 |
| 接口(隐式实现) | 无需 implements 关键字,只要结构体实现接口所有方法即自动适配 |
1. 隐式接口的优势;2. nil 接口为何不等于 nil;3. 空接口(interface{})的用途 |
| 垃圾回收(GC) | 并发三色标记+写屏障,低延迟(Go 1.19+ 支持并发标记和清理) | 1. Go GC 的核心原理;2. 如何优化 GC 性能;3. 逃逸分析与 GC 的关系 |
| 值类型 vs 引用类型 | 值类型(int、struct、array)拷贝值,引用类型(slice、map、channel)拷贝指针 | 1. 常见值类型/引用类型分类;2. 函数传参时值拷贝的坑;3. slice 扩容机制 |
| 延迟执行(defer) | 函数退出前执行,用于资源释放(文件、锁),支持多个 defer 按"后进先出"执行 | 1. defer 的执行顺序;2. defer 与 return 的执行逻辑;3. defer 踩坑场景 |
| 编译速度快 | 静态链接、无依赖解析、编译优化(仅编译被引用代码) | 1. Go 编译快的原因;2. 静态链接的优势与缺点 |
二、Go 基础语法
1. 变量声明(3种方式)
Go 推荐"短变量声明"(:=),但仅用于函数内;全局变量需用 var。
go
package main
import "fmt"
// 1. 全局变量(函数外只能用 var)
var globalVar int = 100
var globalVar2 = 200 // 类型推断
func main() {
// 2. 函数内:短变量声明(:=),自动推断类型
localVar := "hello"
fmt.Println(localVar) // 输出:hello
// 3. 显式声明类型
var explicitVar string = "world"
fmt.Println(explicitVar) // 输出:world
// 多变量声明
a, b := 1, 2
fmt.Println(a, b) // 输出:1 2
}
2. 函数(多返回值+匿名函数)
Go 函数支持多返回值(常用于返回结果+错误),且支持匿名函数和闭包。
go
package main
import "errors"
import "fmt"
// 多返回值函数(结果+错误)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
func main() {
// 调用多返回值函数
res, err := divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", res) // 输出:结果:5
// 匿名函数(立即执行)
func(msg string) {
fmt.Println("匿名函数:", msg) // 输出:匿名函数:hello go
}("hello go")
// 闭包(捕获外部变量)
counter := func() func() int {
i := 0
return func() int {
i++
return i
}
}()
fmt.Println(counter()) // 输出:1
fmt.Println(counter()) // 输出:2
}
3. 接口(隐式实现)
Go 接口是"行为契约",无需显式绑定,结构体实现接口所有方法即自动适配,灵活性极高。
go
package main
import "fmt"
// 定义接口(仅声明方法,无实现)
type Animal interface {
Speak() string
}
// 结构体 Dog 实现 Animal 接口(无需 implements 关键字)
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return fmt.Sprintf("汪!我是%s", d.Name)
}
// 结构体 Cat 实现 Animal 接口
type Cat struct {
Age int
}
func (c Cat) Speak() string {
return fmt.Sprintf("喵!我%d岁了", c.Age)
}
// 接收 Animal 接口的函数(多态)
func LetItSpeak(a Animal) {
fmt.Println(a.Speak())
}
func main() {
dog := Dog{Name: "旺财"}
cat := Cat{Age: 3}
LetItSpeak(dog) // 输出:汪!我是旺财
LetItSpeak(cat) // 输出:喵!我3岁了
}
4. 并发(Goroutine + Channel)
Go 原生支持并发,通过 go 关键字启动 Goroutine,channel 实现 Goroutine 间通信。
go
package main
import "fmt"
import "time"
// Goroutine 间通过 channel 通信
func producer(ch chan<- int) { // 只写 channel
for i := 1; i <= 5; i++ {
ch <- i // 向 channel 写入数据
time.Sleep(100 * time.Millisecond)
}
close(ch) // 关闭 channel(告知接收方无数据)
}
func consumer(ch <-chan int) { // 只读 channel
for num := range ch { // 循环读取 channel,直到关闭
fmt.Println("收到数据:", num)
}
}
func main() {
// 创建无缓冲 channel(同步通信)
ch := make(chan int)
// 启动 2 个 Goroutine
go producer(ch)
go consumer(ch)
// 主 Goroutine 等待 1 秒(避免提前退出)
time.Sleep(1 * time.Second)
// 输出:
// 收到数据: 1
// 收到数据: 2
// 收到数据: 3
// 收到数据: 4
// 收到数据: 5
}
5. defer 延迟执行
defer 用于函数退出前执行(如关闭文件、释放锁),多个 defer 按"后进先出(LIFO)"顺序执行。
go
package main
import "fmt"
func main() {
defer fmt.Println("defer 1") // 最后执行
defer fmt.Println("defer 2") // 中间执行
fmt.Println("主逻辑") // 先执行
// 输出顺序:
// 主逻辑
// defer 2
// defer 1
// 进阶:defer 中修改返回值(仅当返回值有命名时生效)
fmt.Println(add(1, 2)) // 输出:4
}
func add(a, b int) (res int) { // 命名返回值 res
defer func() {
res += 1 // 延迟修改返回值
}()
return a + b // 实际返回:3 + 1 = 4
}
三、Go 高频面试题及答案(含代码+解析)
面试题 1:Goroutine 和 OS 线程的区别?
题目分析:
考察对 Go 并发模型的核心理解,是面试必问考点。
答案对比表:
| 对比维度 | Goroutine | OS 线程(Thread) |
|---|---|---|
| 调度者 | Go 运行时(GOMAXPROCS) | 操作系统内核 |
| 栈大小 | 初始 2KB,可动态扩缩(最大 1GB) | 固定大小(通常 1MB),不可动态调整 |
| 上下文切换开销 | 极低(用户态切换,无需内核参与) | 较高(内核态切换,需保存寄存器/内存映射) |
| 并发数量 | 支持百万级并发 | 仅支持数千级并发(受内存限制) |
| 依赖 | 基于 M:N 调度(M 个 Goroutine 映射到 N 个 OS 线程) | 1:1 调度(一个线程对应一个内核线程) |
代码验证:
go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 设置最大 OS 线程数(默认等于 CPU 核心数)
runtime.GOMAXPROCS(2)
// 启动 10000 个 Goroutine(无压力)
for i := 0; i < 10000; i++ {
go func(n int) {
time.Sleep(1 * time.Second)
fmt.Println("Goroutine", n)
}(i)
}
time.Sleep(2 * time.Second)
}
面试题 2:defer 的执行顺序是什么?return 和 defer 的执行逻辑?
题目分析:
考察 defer 底层机制,容易踩坑,高频考点。
核心结论:
- 多个 defer 按「后进先出(LIFO)」执行;
- return 执行逻辑:先计算返回值 → 执行 defer → 真正返回;
- 若返回值是命名变量,defer 可修改返回值;若为匿名返回值,修改无效。
代码示例:
go
package main
import "fmt"
// 1. 多个 defer 执行顺序(LIFO)
func deferOrder() {
defer fmt.Println("a")
defer fmt.Println("b")
defer fmt.Println("c")
// 输出:c → b → a
}
// 2. 命名返回值:defer 可修改
func namedReturn() (res int) {
defer func() {
res = 100 // 修改返回值
}()
return 50 // 先赋值 res=50,再执行 defer 改为 100,最终返回 100
}
// 3. 匿名返回值:defer 无法修改
func anonymousReturn() int {
var res int = 50
defer func() {
res = 100 // 仅修改局部变量 res,返回值已拷贝为 50
}()
return res
}
func main() {
deferOrder()
fmt.Println(namedReturn()) // 输出:100
fmt.Println(anonymousReturn()) // 输出:50
}
面试题 3:slice 和 array 的区别?slice 扩容机制是什么?
题目分析:
slice 是 Go 中最常用的数据结构,扩容机制是高频考点。
核心结论:
- array :固定长度(
var arr [5]int),值类型,拷贝时复制整个数组; - slice :动态长度(
var s []int),引用类型(底层是「数组指针+长度+容量」),拷贝时复制指针; - 扩容机制 (Go 1.18+ 规则):
- 当切片长度 < 256 时,扩容后容量 = 原容量 × 2;
- 当切片长度 ≥ 256 时,扩容后容量 = 原容量 × 1.25(向上取整);
- 若扩容后容量仍小于所需长度,则直接扩容到所需长度。
代码示例:
go
package main
import "fmt"
func main() {
// 1. array vs slice 声明
var arr [3]int = [3]int{1, 2, 3} // 固定长度 3
var s []int = arr[0:2] // slice 引用 arr 的前 2 个元素(长度 2,容量 3)
fmt.Println("arr:", arr) // 输出:arr: [1 2 3]
fmt.Println("s:", s) // 输出:s: [1 2]
// 2. slice 修改会影响原 array(引用类型)
s[0] = 100
fmt.Println("arr:", arr) // 输出:arr: [100 2 3]
fmt.Println("s:", s) // 输出:s: [100 2]
// 3. slice 扩容验证
s = append(s, 4, 5) // 原容量 3,长度 2 → 追加 2 个元素,长度 4,触发扩容
fmt.Println("s 扩容后:", s) // 输出:[100 2 4 5]
fmt.Println("s 容量:", cap(s)) // 输出:6(原容量 3 < 256 → 3×2=6)
}
面试题 4:nil 接口和 nil 结构体的区别?
题目分析:
考察接口的底层结构,是 Go 面试的"坑题"之一。
核心结论:
- 接口(
interface{})的底层结构包含两部分:type(类型)和value(值); - 只有当
type和value都为 nil 时,接口才等于 nil; - 当把
nil *int赋值给接口时,接口的type是*int,value是 nil → 因此接口不等于 nil。
代码示例:
go
package main
import "fmt"
func main() {
// 1. 接口的 type 和 value 都为 nil → 等于 nil
var i1 interface{} = nil
fmt.Println(i1 == nil) // 输出:true
// 2. 接口的 type 是 *int,value 是 nil → 不等于 nil
var p *int = nil
var i2 interface{} = p
fmt.Println(i2 == nil) // 输出:false
// 验证接口的底层结构(通过反射)
import "reflect"
fmt.Printf("i2 类型:%v,值:%v\n", reflect.TypeOf(i2), reflect.ValueOf(i2))
// 输出:i2 类型:*int,值:<nil>
}
面试题 5:channel 无缓冲和有缓冲的区别?关闭 channel 后读写会发生什么?
题目分析:
考察 channel 的核心特性,是并发模块的高频考点。
核心结论:
| channel 类型 | 核心特性(通信方式) | 写入阻塞条件 | 读取阻塞条件 |
|---|---|---|---|
| 无缓冲 | 同步通信("手递手") | 无接收方时阻塞 | 无发送方时阻塞 |
| 有缓冲 | 异步通信("队列") | 缓冲区满时阻塞 | 缓冲区空时阻塞 |
关闭 channel 后的行为:
- 读取:已写入的数据可正常读取,读完后返回该类型的零值(如 int 返回 0),可通过
val, ok := <-ch判断 channel 是否关闭(ok 为 false 表示关闭); - 写入:会触发
panic(关闭后的 channel 不能写入); - 多次关闭:会触发
panic(channel 只能关闭一次)。
代码示例:
go
package main
import "fmt"
func main() {
// 1. 无缓冲 channel(同步)
ch1 := make(chan int)
go func() {
ch1 <- 100 // 无接收方则阻塞,直到主 Goroutine 读取
}()
fmt.Println(<-ch1) // 无发送方则阻塞,直到子 Goroutine 写入 → 输出:100
// 2. 有缓冲 channel(异步)
ch2 := make(chan int, 2) // 缓冲区大小 2
ch2 <- 200
ch2 <- 300
// ch2 <- 400 // 缓冲区满,阻塞
fmt.Println(<-ch2) // 输出:200
fmt.Println(<-ch2) // 输出:300
// 3. 关闭 channel 后读取
close(ch2)
val1, ok1 := <-ch2 // 缓冲区空,返回零值和 false → val1=0,ok1=false
fmt.Println(val1, ok1) // 输出:0 false
// 4. 关闭 channel 后写入(触发 panic)
// ch2 <- 500 // 报错:panic: send on closed channel
// 5. 遍历读取 channel(直到关闭)
ch3 := make(chan int, 3)
ch3 <- 1
ch3 <- 2
ch3 <- 3
close(ch3)
for num := range ch3 {
fmt.Println(num) // 输出:1、2、3(读完后退出循环)
}
}
四、面试复习总结
Go 面试的核心考点集中在:
- 并发模型:Goroutine 调度、Channel 通信、GOMAXPROCS;
- 语法特性:defer 执行逻辑、接口隐式实现、值类型 vs 引用类型;
- 数据结构:slice 扩容、map 底层实现(哈希表)、channel 缓冲机制;
- 工程实践 :错误处理(
error接口)、GC 优化、逃逸分析。