Appearance
Go 基础知识 - 面试复习
配合高级篇 20260401-高级Go面试复习 使用
一、数据类型
1.1 基本类型
布尔: bool
整型: int8, int16, int32, int64, int, uint8, uint16, uint32, uint64, uint
- int/uint 在 64 位系统上是 64 位,32 位系统上是 32 位
浮点: float32, float64(Go 没有 float)
复数: complex64, complex128
字节: byte = uint8 的别名
字符: rune = int32 的别名,表示 Unicode 码点
字符串: string(不可变的字节序列)1.2 类型转换
Go 没有隐式转换,必须显式转换:
go
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// 注意:int 和 int64 是不同类型
var a int = 1
var b int64 = int64(a) // 必须显式转换1.3 零值
Go 中变量声明不赋值时有零值:
go
int -> 0
float -> 0.0
bool -> false
string -> ""
指针 -> nil
slice -> nil(不是空 slice)
map -> nil
channel -> nil
interface -> nil
func -> nil面试注意:
nil slice和空 slice不同。nil slice的 len/cap 都是 0,但 JSON 序列化时nil slice输出null,空 slice 输出[]。
go
var s1 []int // nil slice
s2 := []int{} // 空 slice
s3 := make([]int, 0) // 空 slice二、Slice 深入
2.1 底层结构
go
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 当前长度
cap int // 容量
}2.2 扩容机制(Go 1.18+)
如果 newCap > 2*oldCap:
newCap = newCap(按需分配)
否则如果 oldCap < 256:
newCap = 2 * oldCap(小 slice 激进扩容)
否则:
循环 newCap += newCap/4 直到 newCap >= newCap_old
即每次增长 25%(大 slice 保守扩容)总结:小 slice 翻倍扩容,大 slice 1.25 倍扩容
2.3 常见坑
go
// 坑1:slice 作为函数参数是引用传递(传的是 slice header 的拷贝)
func appendItem(s []int) {
s = append(s, 4) // 修改的是副本的 header,外部不受影响!
}
// 坑2:两个 slice 共享底层数组
a := make([]int, 3, 5)
b := a[1:3]
b[0] = 99 // a[1] 也变成 99!
// 坑3:range 遍历时修改元素
for _, v := range slice {
v = 10 // 不会修改 slice 中的元素
}
// 正确做法
for i := range slice {
slice[i] = 10
}
// 坑4:range 遍历时删除元素(会跳过元素)
for i, v := range slice {
if v == target {
slice = append(slice[:i], slice[i+1:]...) // 错!会导致索引错乱
}
}
// 正确:倒序删除
for i := len(slice) - 1; i >= 0; i-- {
if slice[i] == target {
slice = append(slice[:i], slice[i+1:]...)
}
}2.4 slice 和 array 的区别
| 特性 | Array | Slice |
|---|---|---|
| 长度 | 编译期确定,类型的一部分 [3]int | 运行时动态 |
| 传参 | 值拷贝(拷贝整个数组) | 引用语义(拷贝 header,共享底层数组) |
| 比较 | 可以用 == 比较 | 不能用 ==,只能和 nil 比较 |
三、Map 深入
3.1 底层结构
hmap:
- count: 元素个数
- B: 桶数量 = 2^B
- hash0: hash 种子(随机化防攻击)
- buckets: 指向桶数组
- oldbuckets: 扩容时的旧桶
每个 bucket:
- 8 个 key(连续存储)
- 8 个 value(连续存储)
- 1 个 top hash 数组(每个 key 的高 8 位,快速比较)
- overflow 指针(指向溢出桶)3.2 扩容
两种扩容条件:
- 负载因子超过 6.5:翻倍扩容,桶数量翻倍,重新分配所有 key
- 溢出桶太多:等量扩容(sameSizeGrow),桶数量不变,重新排列减少溢出链
扩容是渐进式的(不是一次性完成),每次访问/写入时最多迁移 2 个桶
3.3 map 的注意点
go
// 1. map 不是并发安全的
// 2. map 不能比较(只能和 nil 比较)
// 3. map 的 key 必须是可比较的类型(不能是 slice、map、func)
// 4. map 的遍历顺序是随机的(Go 故意为之,防止依赖顺序)
// 5. map 取不存在的 key 返回 value 类型的零值
v, ok := m["key"] // ok 为 false 表示 key 不存在3.4 map 的 delete
go
delete(m, "key") // 删除 key,如果 key 不存在不会 panic四、String
4.1 底层结构
go
type string struct {
data unsafe.Pointer // 指向底层字节数组
len int // 字节长度
}字符串是不可变的字节序列。任何修改都会创建新字符串。
4.2 string 和 []byte 的转换
go
s := "hello"
b := []byte(s) // 拷贝底层数组
s2 := string(b) // 同样拷贝
// 零拷贝转换(不安全,但高效)
// 通过 unsafe.Pointer 实现
// 适用于临时使用、确认不修改的场景4.3 字符串拼接
go
// 低效:每次 + 都创建新字符串,O(n^2)
s := "a" + "b" + "c"
// 高效:strings.Builder 内部用 []byte 缓冲
var sb strings.Builder
sb.WriteString("a")
sb.WriteString("b")
sb.WriteString("c")
s := sb.String()
// 或者 fmt.Sprintf(内部也是 Builder)4.4 rune 和 byte
go
s := "hello 世界"
// byte = uint8,代表单字节
// rune = int32,代表 Unicode 码点
fmt.Println(len(s)) // 12(字节数,中文占 3 字节 UTF-8)
fmt.Println(len([]rune(s))) // 7(字符数)
// range 遍历 string 时,每次迭代得到的是 rune
for i, r := range s {
fmt.Printf("%d: %c\n", i, r)
}
// 0: h, 1: e, 2: l, 3: l, 4: o, 5: , 6: 世, 9: 界
// 注意索引 6 到 9 跨了 3 字节五、函数 & 闭包 & defer
5.1 函数是一等公民
go
// 函数可以赋值给变量
var add = func(a, b int) int { return a + b }
// 函数可以作为参数
func apply(f func(int) int, x int) int { return f(x) }
// 函数可以作为返回值
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}5.2 闭包
闭包 = 函数 + 它引用的外部变量
go
func counter() (func() int, func() int) {
n := 0
increment := func() int { n++; return n }
decrement := func() int { n--; return n }
return increment, decrement
}
inc, dec := counter()
inc() // 1
inc() // 2
dec() // 1注意:循环中的闭包陷阱
go
// 错误:所有 goroutine 共享同一个 i(i 最终是 5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 可能都输出 5
}()
}
// 正确:通过参数捕获
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}5.3 defer 执行顺序
go
// defer 按 LIFO(后进先出)执行
func f() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
// 输出: 3, 2, 1
}
// defer 的参数在声明时求值(不是执行时)
func f() {
i := 0
defer fmt.Println(i) // 输出 0,不是 1
i = 1
}
// defer 与 return 的执行顺序
// return 先执行:1. 保存返回值到局部变量 2. 执行 defer 3. 返回
func f() (result int) {
defer func() {
result++ // 可以修改命名返回值
}()
return 0 // result = 0,然后 defer 执行 result++,最终返回 1
}5.4 可变参数
go
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
nums := []int{1, 2, 3}
sum(nums...) // 6,展开 slice六、接口
6.1 接口是隐式实现
go
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "Woof" }
// Dog 自动实现了 Speaker,不需要 implements 关键字
var s Speaker = Dog{}6.2 空接口
go
// any = interface{} 的别名
var a any = 42
var b any = "hello"
var c any = []int{1, 2, 3}
// 类型断言
if v, ok := a.(int); ok {
fmt.Println("int:", v)
}
// 类型选择
switch v := a.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("unknown:", v)
}6.3 接口组合
go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer // 嵌入,组合了 Reader 和 Writer 的方法集
}6.4 接口的零值是 nil
go
var s Speaker // nil
s.Speak() // panic: nil pointer dereference6.5 nil 接口 vs 非 nil 接口
go
type MyErr struct{}
func (e *MyErr) Error() string { return "err" }
func getErr() error {
var p *MyErr = nil
return p // 返回的是非 nil 接口!(接口内部有类型信息)
}
var err error = getErr()
fmt.Println(err == nil) // false!
// 原因:接口 = (type, data),这里 type=*MyErr, data=nil
// 接口只有在 type 和 data 都为 nil 时才等于 nil七、Struct & 方法
7.1 结构体
go
type User struct {
Name string
Age int
}
// 匿名结构体
u := struct {
Name string
}{Name: "Alice"}
// 结构体比较:所有字段可比较时,结构体可以用 == 比较7.2 匿名字段(嵌入/组合)
go
type Animal struct {
Name string
}
type Dog struct {
Animal // 匿名字段(嵌入)
Breed string
}
d := Dog{Animal: Animal{Name: "Buddy"}, Breed: "Lab"}
d.Name // 直接访问嵌入字段(Go 的"继承")Go 没有继承,只有组合(embedding)。嵌入字段的方法会被"提升"到外层。
7.3 值接收者 vs 指针接收者
go
type User struct {
Name string
}
// 值接收者:不会修改原始值
func (u User) GetName() string { return u.Name }
// 指针接收者:可以修改原始值
func (u *User) SetName(name string) { u.Name = name }
// 规则:
// 1. 如果需要修改接收者,必须用指针
// 2. 如果接收者是大结构体,用指针避免拷贝
// 3. 保持一致性:一个类型的所有方法要么都是值接收者,要么都是指针接收者
// 4. 值类型也可以调用指针接收者的方法(Go 自动取地址)八、指针
8.1 基本概念
go
a := 42
p := &a // p 是 *int 类型,指向 a
*p = 99 // 修改 a 的值
// Go 没有指针运算
// p++ // 编译错误!8.2 new 和 make
go
// new: 分配零值内存,返回指针
p := new(int) // *int, 值为 0
p2 := new(User) // *User, Name="" Age=0
// make: 只用于 slice、map、channel,返回初始化后的值(不是指针)
s := make([]int, 5) // []int, [0,0,0,0,0]
m := make(map[string]int) // map[string]int{}
ch := make(chan int, 10) // buffered channel8.3 函数传参:值传递
Go 中所有参数都是值传递:
go
func modify(s []int) {
s[0] = 99 // 可以修改底层数组元素(因为 slice header 中有数组指针)
s = append(s, 4) // 不影响外部!因为修改的是 slice header 的副本
}
func modifyMap(m map[string]int) {
m["a"] = 99 // 可以修改,因为 map 本身就是指针
}九、Goroutine & Channel 基础
9.1 Goroutine
go
go func() {
fmt.Println("hello")
}()
// 启动即忘:不需要等待结果
// 如果需要等待:用 WaitGroup
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()9.2 Channel 基础
go
// 无缓冲:发送和接收必须同时准备好(同步)
ch := make(chan int)
ch <- 1 // 阻塞,直到有人接收
v := <-ch // 阻塞,直到有人发送
// 有缓冲:缓冲区满时发送阻塞,空时接收阻塞
ch := make(chan int, 10)
ch <- 1 // 不阻塞(缓冲区未满)
v := <-ch // 不阻塞(缓冲区有数据)
// 关闭
close(ch) // 关闭后不能再发送(panic),但可以继续接收已发送的值
// 从已关闭的 channel 接收:
v, ok := <-ch // ok=false 表示 channel 已关闭且没有数据
// 遍历
for v := range ch { // channel 关闭后自动退出
fmt.Println(v)
}9.3 Channel 的方向
go
// 只发送
func sender(ch chan<- int) {
ch <- 1
// v := <-ch // 编译错误
}
// 只接收
func receiver(ch <-chan int) {
v := <-ch
// ch <- 1 // 编译错误
}
// 双向
func both(ch chan int) {}9.4 select
go
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
case ch3 <- 1:
fmt.Println("sent to ch3")
default:
fmt.Println("no channel ready") // 非阻塞
}
// select 超时模式
select {
case res := <-resultCh:
return res
case <-time.After(3 * time.Second):
return errors.New("timeout")
}9.5 goroutine 泄漏
go
// 常见原因:
// 1. 向无缓冲 channel 发送,但没有接收者
// 2. 从 channel 接收,但没有发送者
// 3. 没有使用 context 控制生命周期
// 预防:始终用 context 或超时
go func(ctx context.Context) {
select {
case <-ch:
// 处理
case <-ctx.Done():
return // context 取消时退出
}
}(ctx)十、并发原语
10.1 sync.Mutex & sync.RWMutex
go
var mu sync.RWMutex
var data map[string]string
func write(key, value string) {
mu.Lock() // 写锁:独占
defer mu.Unlock()
data[key] = value
}
func read(key string) string {
mu.RLock() // 读锁:共享(多个读者可并发)
defer mu.RUnlock()
return data[key]
}| 锁 | 读读 | 读写 | 写写 |
|---|---|---|---|
| Mutex | 互斥 | 互斥 | 互斥 |
| RWMutex | 并发 | 互斥 | 互斥 |
10.2 sync.WaitGroup
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
wg.Wait() // 阻塞直到所有 goroutine 完成
// 注意:Add 要在 goroutine 启动之前调用
// 不要在 goroutine 内部调用 wg.Add10.3 sync.Once
go
var once sync.Once
once.Do(func() {
// 只执行一次,即使多个 goroutine 并发调用
initDB()
})10.4 sync.Pool
go
pool := sync.Pool{
New: func() any {
return &bytes.Buffer{}
},
}
buf := pool.Get().(*bytes.Buffer)
buf.Reset()
// 使用 buf...
pool.Put(buf)
// 用途:减少内存分配(减少 GC 压力)
// 场景:频繁创建和销毁的临时对象(bytes.Buffer、json Encoder 等)
// 注意:GC 时 Pool 中的对象会被清除,不是持久缓存10.5 sync/atomic
go
var counter int64
// 原子操作
atomic.AddInt64(&counter, 1)
val := atomic.LoadInt64(&counter)
atomic.CompareAndSwapInt64(&counter, old, new)
// Go 1.19+ 推荐 atomic.Int64 等类型(更安全)
var counter atomic.Int64
counter.Add(1)
val := counter.Load()十一、Context 详解
11.1 四种 Context
go
// 1. Background:根 context
ctx := context.Background()
// 2. TODO:不确定用哪种时先用 TODO
ctx := context.TODO()
// 3. WithCancel:手动取消
ctx, cancel := context.WithCancel(parentCtx)
cancel() // 取消
// 4. WithTimeout:超时自动取消
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel() // 必须 defer cancel()
// 5. WithDeadline:截止时间自动取消
ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second))
defer cancel()
// 6. WithValue:传递值
ctx := context.WithValue(parentCtx, "key", "value")11.2 Context 的传播规则
- 取消信号会向下传播(子 context 取消 -> 所有子孙 context 取消)
- 取消信号不会向上传播(子 context 取消不影响父 context)
- Value 会向下传播,子可以读父的值,反之不行
11.3 正确使用姿势
go
// 1. 作为函数第一个参数
func HandleRequest(ctx context.Context, req *Request) error {}
// 2. 不要把 context 存在结构体中
// 错误:
type Handler struct {
ctx context.Context // 不要这样做
}
// 正确:每次调用时传递
// 3. 始终 defer cancel()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 4. context.Value 只传请求元数据,不传业务数据
ctx = context.WithValue(ctx, "traceID", traceID) // OK
ctx = context.WithValue(ctx, "user", fullUserObj) // 避免!对象太大十二、错误处理
12.1 error 接口
go
type error interface {
Error() string
}
// 创建错误
err := errors.New("something went wrong")
err := fmt.Errorf("query failed: %w", err) // 包装,支持 Is/As12.2 errors.Is / errors.As
go
// Is:判断错误链中是否包含某个错误
if errors.Is(err, sql.ErrNoRows) {}
// As:判断错误链中是否包含某个类型的错误
var netErr *net.OpError
if errors.As(err, &netErr) {
// 处理网络错误
}12.3 panic & recover
go
// panic:不可恢复的错误(编程错误)
// 不要用 panic 处理业务错误!
// recover:只在 defer 中有效
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil // b=0 时 panic,但被 recover 捕获
}十三、包管理
13.1 go.mod 关键字
module -> 模块名
go -> Go 版本
require -> 依赖
replace -> 替换依赖(本地开发常用)
exclude -> 排除依赖13.2 常用命令
bash
go mod init example.com/mypkg # 初始化模块
go mod tidy # 整理依赖
go mod vendor # 将依赖打包到 vendor 目录
go get github.com/pkg@v1.2.3 # 添加/更新依赖
go build ./... # 编译
go test ./... # 运行测试
go vet ./... # 静态分析13.3 internal 包
internal 目录下的包只能被同一模块(或父模块)导入,外部模块不能导入。
十四、测试
14.1 单元测试
go
// 文件名:xxx_test.go
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1,2) = %d, want %d", got, want)
}
}
// 表格驱动测试
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}14.2 Benchmark
go
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// 运行
go test -bench=. -benchmem14.3 Mock
go
// 接口 + mock(手动或 go:generate + mockgen)
type UserRepository interface {
GetByID(id int) (*User, error)
}
// mock 实现
type MockUserRepo struct {
GetByIDFunc func(id int) (*User, error)
}
func (m *MockUserRepo) GetByID(id int) (*User, error) {
return m.GetByIDFunc(id)
}十五、Go Modules & 项目结构
15.1 推荐项目结构
myproject/
cmd/ # 程序入口
server/main.go
internal/ # 私有代码
user/
order/
pkg/ # 可被外部导入的公共库
api/ # API 定义(proto、OpenAPI)
configs/ # 配置文件
scripts/ # 脚本
go.mod15.2 init 函数
go
// 在包被导入时自动执行,顺序:
// 1. 被导入包的 init 先执行
// 2. 同一包内多个 init 按声明顺序执行
// 3. 一个文件内多个 init 按出现顺序执行
// 用途:注册(数据库驱动)、初始化配置
// 缺点:隐式逻辑,难以测试,尽量避免滥用十六、Go 常见关键字
16.1 new vs make
| 特性 | new | make |
|---|---|---|
| 适用类型 | 任意类型 | slice、map、channel |
| 返回值 | *T(指针) | T(已初始化的值) |
| 初始化 | 零值 | 已初始化(非零值) |
16.2 defer 执行时机
- 函数 return 时
- panic 后 recover 前
- goroutine 退出前
16.3 range 行为
go
// range map:遍历顺序随机
// range slice:按索引顺序
// range channel:依次读取直到关闭
// range string:按 rune 遍历(Unicode 安全)十七、Linux 常用命令(JD 要求)
bash
# 进程
ps aux | grep app
top / htop
kill -9 PID
strace -p PID # 跟踪系统调用
lsof -i :8080 # 查看端口占用
netstat -tlnp # 查看监听端口
# 文件
tail -f app.log # 实时查看日志
grep -r "error" ./logs/
find / -name "*.log"
chmod / chown
# 网络
curl -v http://localhost:8080/health
wget
dig / nslookup
tcpdump
# 磁盘/内存
df -h
du -sh *
free -h
# Shell 基础
变量: name="hello"
条件: if [ -f file ]; then ...; fi
循环: for i in $(ls); do ...; done
管道: cmd1 | cmd2 | cmd3
重定向: cmd > out.log 2>&1十八、快速问答速查
| 问题 | 答案要点 |
|---|---|
| Go 中 make 和 new 的区别? | new 返回指针零值;make 返回初始化后的 slice/map/channel |
| slice 和 array 的区别? | array 长度固定是值类型;slice 动态长度是引用语义 |
| map 是线程安全的吗? | 不是,需要 sync.Map 或 map + Mutex/RWMutex |
| channel 关闭后还能读吗? | 可以,返回零值 + ok=false;不能再写(panic) |
| 向 nil channel 发送/接收? | 永久阻塞 |
| goroutine 怎么退出? | channel + context 控制,Go 不会强制杀死 goroutine |
| defer 参数什么时候求值? | 声明时求值,不是执行时 |
| interface 的零值? | nil(type 和 data 都为 nil) |
| Go 有异常吗? | 没有 try/catch,用 error 返回值 + panic/recover |
| Go 的 GC 算法? | 三色标记法 + 混合写屏障 |
| Go 的调度模型? | GMP(Goroutine-Machine-Processor) |
| defer 的执行顺序? | LIFO(后进先出) |
| context 的作用? | 超时控制、取消传播、请求元数据传递 |
| sync.Pool 的用途? | 对象复用,减少 GC 压力 |
| 逃逸分析的作用? | 决定对象分配在栈还是堆,减少 GC 压力 |