Skip to content

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 的区别

特性ArraySlice
长度编译期确定,类型的一部分 [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 扩容

两种扩容条件:

  1. 负载因子超过 6.5:翻倍扩容,桶数量翻倍,重新分配所有 key
  2. 溢出桶太多:等量扩容(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 dereference

6.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 channel

8.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.Add

10.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/As

12.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=. -benchmem

14.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.mod

15.2 init 函数

go
// 在包被导入时自动执行,顺序:
// 1. 被导入包的 init 先执行
// 2. 同一包内多个 init 按声明顺序执行
// 3. 一个文件内多个 init 按出现顺序执行

// 用途:注册(数据库驱动)、初始化配置
// 缺点:隐式逻辑,难以测试,尽量避免滥用

十六、Go 常见关键字

16.1 new vs make

特性newmake
适用类型任意类型slice、map、channel
返回值*T(指针)T(已初始化的值)
初始化零值已初始化(非零值)

16.2 defer 执行时机

  1. 函数 return 时
  2. panic 后 recover 前
  3. 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 压力