wx

关注公众号

「Go语言面试题库」(150+题)


由浅入深 · 全面覆盖 · 代码可运行


📋 目录

  1. 基础语法 (20题)
  2. 数据类型 (25题)
  3. 函数与方法 (20题)
  4. 接口与反射 (20题)
  5. 并发编程 (30题)
  6. 内存与GC (15题)
  7. 标准库 (15题)
  8. 工程实践 (15题)
  9. 源码与高级主题 (20题)

🚀 如何运行代码

题库中所有代码均可在 Go Playground 运行:

  1. 访问 https://go.dev/play
  2. 将代码复制到编辑器
  3. 点击 Run 按钮执行
  4. 如需分享,点击 Share 获取链接

一、基础语法 (20题)

第1题:变量声明与零值

问题:以下代码输出什么?为什么?

package main

import "fmt"

func main() {
    var a int
    var b string
    var c bool
    var d []int
    
    fmt.Printf("a=%d, b=%q, c=%t, d==nil:%t\n", a, b, c, d == nil)
}

Playgroundhttps://go.dev/play/p/ypEhyJMcd8Q

答案

a=0, b="", c=false, d==nil:true

解析

  • Go中变量声明后会被赋予零值(zero value)
  • int的零值是0
  • string的零值是空字符串""
  • bool的零值是false
  • 引用类型(slice、map、channel、指针、interface)的零值是nil

第2题:短变量声明的作用域

问题:以下代码能否编译通过?如果不能,如何修改?

package main

import "fmt"

func main() {
    x := 10
    if x > 5 {
        x := 5  // 这里的x是新变量还是赋值?
        fmt.Println("inner:", x)
    }
    fmt.Println("outer:", x)
}

答案:能编译通过,输出:

inner: 5
outer: 10

解析

  • :=在内部作用域声明了新的局部变量x,遮蔽了外部的x
  • 这是Go语言中常见的**变量遮蔽(variable shadowing)**现象
  • 如果意图是修改外部x,应使用赋值语句x = 5

第3题:多变量赋值与交换

问题:以下代码输出什么?利用了什么特性?

package main

import "fmt"

func main() {
    a, b := 1, 2
    a, b = b, a
    fmt.Println(a, b)
}

Playgroundhttps://go.dev/play/p/5u012hcK1o0

答案:输出2 1,成功交换了两个变量的值。

解析

  • Go支持并行赋值(multiple assignment)
  • 赋值前,右边的所有表达式先求值,然后同时赋给左边
  • 不需要临时变量即可完成交换

第4题:const常量

问题:以下代码有什么问题?

package main

const (
    A = iota
    B
    C
)

func main() {
    const x = 100
    x = 200  // 错误?
    const y = len("hello")  // 能否编译?
}

Playgroundhttps://go.dev/play/p/LVHoQKmlJPf

答案

  • x = 200编译错误:常量不能重新赋值
  • const y = len("hello")可以编译(len在常量表达式中求值)

解析

  • Go常量必须在编译期确定值
  • iota是常量声明中的枚举计数器,从0开始
  • 内置函数lencaprealimagcomplexunsafe.Sizeof在常量表达式中可用

第5题:iota枚举

问题:以下代码输出什么?

package main

import "fmt"

const (
    _ = iota
    KB = 1 << (10 * iota)
    MB
    GB
)

func main() {
    fmt.Println(KB, MB, GB)
}

答案:输出1024 1048576 1073741824

解析

  • _跳过iota=0的位置
  • iota在同一const块中每行递增1
  • <<是位左移运算符,实现单位换算
  • KB = 1 « 10 = 1024
  • MB = 1 « 20 = 1048576
  • GB = 1 « 30 = 1073741824

第6题:字符串与rune

问题:以下代码输出什么?为什么长度不同?

package main

import "fmt"

func main() {
    s := "Hello, 世界"
    fmt.Println(len(s))           // ?
    fmt.Println(len([]rune(s)))   // ?
}

Playgroundhttps://go.dev/play/p/k38lW2uUHiJ

答案:输出139

解析

  • Go字符串是UTF-8编码的字节序列
  • len(s)返回字节数,中文字符占3字节
  • []rune(s)将字符串转为rune切片,每个rune代表一个Unicode码点
  • “Hello, “占7字节,“世界"占6字节,共13字节
  • 但字符数是9个(7个ASCII + 2个中文)

第7题:for-range遍历

问题:以下代码输出什么?有什么陷阱?

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3}
    for i, v := range arr {
        if i == 0 {
            arr[1], arr[2] = 999, 999
        }
        fmt.Printf("i=%d, v=%d\n", i, v)
    }
}

Playgroundhttps://go.dev/play/p/XAFb17qhXr_v

答案

i=0, v=1
i=1, v=999
i=2, v=999

解析

  • range遍历切片时,虽然会拷贝切片header,但底层数组是共享的
  • i=0时修改了arr[1]arr[2],后续迭代取到的v已经是修改后的值
  • 遍历数组时才会拷贝整个数组,此时修改不影响遍历值

对比

// 切片(底层数组共享)
arr := []int{1, 2, 3}  // 输出: 1, 999, 999

// 数组(值拷贝)
arr := [3]int{1, 2, 3} // 输出: 1, 2, 3

第8题:range遍历的变量复用(⚠️ 版本差异题)

问题:以下代码有什么问题?如何修复?

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3}
    var out []*int
    for _, v := range arr {
        out = append(out, &v)
    }
    for _, p := range out {
        fmt.Println(*p)
    }
}

Playgroundhttps://go.dev/play/p/dySdpNSAXUs

答案

  • Go 1.21及之前:输出3 3 3
  • Go 1.22+:输出1 2 3

解析

  • Go 1.21及之前:range循环中的变量v复用的单一变量,不是每次迭代创建新变量。所有&v指向同一个内存地址,最终存储的是最后一次迭代的值3。

  • Go 1.22+:每次迭代都会创建新的变量,v不再是同一个变量,所以&v指向不同的地址,输出预期的1 2 3

  • 向后兼容修复(推荐在所有版本中使用):

    for i := range arr {
        v := arr[i]  // 明确创建局部副本
        out = append(out, &v)
    }
    // 或使用索引直接取地址
    for i := range arr {
        out = append(out, &arr[i])
    }
    

第9题:switch语句

问题:以下代码输出什么?switch在Go中有什么特点?

package main

import "fmt"

func main() {
    x := 5
    switch {
    case x > 3:
        fmt.Println(">3")
        fallthrough
    case x > 4:
        fmt.Println(">4")
    case x > 5:
        fmt.Println(">5")
    }
}

Playgroundhttps://go.dev/play/p/IXx2n2a7RaP

答案

>3
>4

解析

  • Go的switch默认不穿透(不需要break)
  • 使用fallthrough关键字可以强制执行下一个case
  • switch表达式可以省略(默认true,用于if-else链替代)
  • case支持多个值、表达式,不需要是常量

第10题:switch与类型断言

问题:以下代码输出什么?

package main

import "fmt"

func checkType(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("int: %d\n", v)
    case string:
        fmt.Printf("string: %s\n", v)
    case nil:
        fmt.Println("nil")
    default:
        fmt.Printf("unknown: %T\n", v)
    }
}

func main() {
    checkType(42)
    checkType("hello")
    checkType(3.14)
}

Playgroundhttps://go.dev/play/p/XdvKz5HO_F7

答案

int: 42
string: hello
unknown: float64

解析

  • switch v := i.(type)类型开关(type switch)
  • 可以获取interface的动态类型
  • 每个case中v的类型会自动转换为对应类型

第11题:goto语句

问题:Go中的goto有什么限制?以下代码能编译吗?

package main

import "fmt"

func main() {
    i := 0
LOOP:
    if i >= 5 {
        return
    }
    fmt.Println(i)
    i++
    goto LOOP
}

Playgroundhttps://go.dev/play/p/KbieuY6I3hD

答案:能编译,输出0到4。

解析

  • Go支持goto,但有严格限制:
    1. 不能跳转到其他函数
    2. 不能跳转到变量声明之后(跳过声明)
    3. 不能跳入代码块(会绕过某些初始化)
  • 实践中很少使用,主要用于错误处理或跳出深层循环

第12题:标签与break/continue

问题:以下代码输出什么?标签有什么用?

package main

import "fmt"

func main() {
OUTER:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break OUTER
            }
            fmt.Printf("(%d,%d)", i, j)
        }
    }
}

答案:输出(0,0)(0,1)(0,2)(1,0)

解析

  • break LABEL可以跳出指定标签标识的循环
  • 同理continue LABEL可以跳到指定标签的下一轮迭代
  • 避免使用flag变量或多次break跳出多层循环

第13题:指针基础

问题:以下代码输出什么?

package main

import "fmt"

func addOne(x int) {
    x++
}

func addOnePtr(x *int) {
    *x++
}

func main() {
    a := 1
    addOne(a)
    fmt.Println(a)
    
    addOnePtr(&a)
    fmt.Println(a)
}

Playgroundhttps://go.dev/play/p/2eftrWz2pBI

答案

1
2

解析

  • Go是值传递,函数参数是原变量的副本
  • 要修改原变量需要传递指针
  • &取地址,*解引用
  • 注意:Go不支持指针运算(如p++移动指针位置)

第14题:new与make的区别

问题:以下代码有什么问题?

package main

func main() {
    var s1 []int = new([]int)      // 正确吗?
    var m1 map[string]int = new(map[string]int)  // 正确吗?
    var s2 []int = make([]int, 5)  // 正确吗?
    var p *int = make(*int)        // 正确吗?
}

Playgroundhttps://go.dev/play/p/9xGW8_pL-jQ

答案

  • new([]int)返回*[]int,不能赋值给[]int
  • new(map[string]int)语法正确但返回空map指针,很少这样用
  • make([]int, 5)正确,返回已初始化的slice
  • make(*int)编译错误,make不能用于指针

解析

特性 new make
返回类型 指向类型的指针 类型本身(非指针)
适用类型 任意类型 slice、map、channel
初始化 零值 非零值(已可用)
示例 new(int)返回*int make([]int, 5)返回[]int

第15题:类型别名与类型定义

问题:以下代码输出什么?有什么区别?

package main

import "fmt"

type MyInt1 = int  // 类型别名
type MyInt2 int    // 新类型

func main() {
    var i int = 10
    var a MyInt1 = i
    var b MyInt2 = i    // 能编译吗?
    
    fmt.Printf("a type: %T, b type: %T\n", a, b)
}

Playgroundhttps://go.dev/play/p/-VT7eCEcnak

答案var b MyInt2 = i编译错误:cannot use i (type int) as type MyInt2

解析

  • type MyInt1 = int类型别名(Go 1.9+),MyInt1和int完全相同
  • type MyInt2 int类型定义,创建了一个基于int的新类型
  • 新类型和原类型是不同类型,需要显式转换
  • 类型别名主要用于代码重构时保持兼容性

第16题:类型转换(⚠️ 不推荐用法)

问题:以下代码输出什么?哪些转换会报错?

package main

import "fmt"

func main() {
    var i int = 10
    var f float64 = float64(i)
    var s string = string(i)  // 结果是什么?
    // var b []byte = []byte(i)  // 能编译吗?
    
    fmt.Println(f, s)
}

Playgroundhttps://go.dev/play/p/0q0IomU5zKe

答案:输出10和一个换行符(ASCII 10是换行符)。[]byte(i)编译错误。

解析

  • 数值类型之间可以显式转换
  • string(int)将整数转为对应的Unicode字符,不是数字字符串
    • string(65) = “A”
    • string(10)是换行符(ASCII 10)

⚠️ Go 1.15+警告

  • go vet会对string(int)发出警告:“conversion from int to string yields a string of one rune, not a string of digits”
  • 现代推荐做法
    // 将整数转为数字字符串
    s := strconv.Itoa(10)           // "10"
    s := fmt.Sprintf("%d", 10)      // "10"
    s := string(rune(65))           // "A"(明确意图)
    
  • 不同类型之间不能隐式转换,必须显式转换

第17题:可变参数

问题:以下代码输出什么?如何实现可变参数?

package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))
    fmt.Println(sum())
    
    arr := []int{4, 5, 6}
    fmt.Println(sum(arr...))
}

Playgroundhttps://go.dev/play/p/qcITJJnr8Z5

答案

6
0
15

解析

  • ...int表示可变参数,函数内部视为[]int
  • 可以传0个或多个参数
  • 可以用slice...展开slice作为参数
  • 可变参数必须是最后一个参数

第18题:init函数

问题:以下代码输出顺序是什么?

package main

import "fmt"

var _ = initVar()

func initVar() int {
    fmt.Println("initVar")
    return 0
}

func init() {
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2")
}

func main() {
    fmt.Println("main")
}

Playgroundhttps://go.dev/play/p/0Hj0aeBlCvM

答案

initVar
init 1
init 2
main

解析

  • 程序执行顺序:导入包 → 包级变量初始化 → init函数 → main函数
  • 一个包可以有多个init函数,按声明顺序执行
  • init函数在包被导入时自动执行,不能被显式调用
  • 每个包可以被多个文件包含,每个文件的init按文件名排序执行

第19题:main包的特殊性

问题:以下代码能运行吗?为什么?

package mymain

import "fmt"

func main() {
    fmt.Println("hello")
}

Playgroundhttps://go.dev/play/p/aqlpxGgsExH

答案:编译错误:

  • 如果文件名是main.go:function main is undeclared in the main package(package名不是main)
  • 或者:cannot find main function in main package(如果包名改成main但文件不对)

解析

  • 可执行程序的入口包必须是package main
  • func main()是程序入口点,无参数无返回值
  • 库包不能使用package main

第20题:代码格式化规范

问题:Go有哪些强制的代码格式规范?

答案

  1. 缩进:必须使用Tab(非空格)
  2. 括号{必须与函数声明在同一行(不能换行)
  3. 行尾:不需要分号(自动插入)
  4. 导入:未使用的导入会编译错误
  5. 变量:未使用的局部变量会编译错误(下划线_可忽略)

解析

  • gofmt是官方格式化工具,统一代码风格
  • 强制规则通过编译器检查,确保代码整洁
  • 使用go fmt命令格式化代码
  • 编辑器应配置保存时自动格式化

二、数据类型 (25题)

第21题:数组与切片的区别

问题:以下代码输出什么?数组和切片有什么区别?

package main

import "fmt"

func modifyArray(arr [3]int) {
    arr[0] = 100
}

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    arr := [3]int{1, 2, 3}
    slice := []int{1, 2, 3}
    
    modifyArray(arr)
    modifySlice(slice)
    
    fmt.Println(arr)
    fmt.Println(slice)
}

答案

[1 2 3]
[100 2 3]

解析

特性 数组 切片
长度 固定,类型的一部分 可变
传递 值传递(拷贝整个数组) 引用传递(拷贝header)
底层 直接存储数据 指向底层数组的header
比较 可比较(元素可比较时) 不可比较(只能用nil)

第22题:切片的底层结构

问题:以下代码输出什么?为什么?

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4}
    s1 := s[1:3]
    
    fmt.Println(len(s1), cap(s1))
    
    s1 = append(s1, 100)
    fmt.Println(s)
    fmt.Println(s1)
}

Playgroundhttps://go.dev/play/p/p4Bj2I9iGU-

答案

2 4
[0 1 2 100 4]
[1 2 100]

解析

  • 切片header包含:指针、长度(len)、容量(cap)
  • s[1:3]创建的切片:len=3-1=2,cap=5-1=4
  • append在容量足够时直接修改底层数组,会影响原切片
  • 这是一个常见的副作用bug

第23题:append导致重新分配

问题:以下代码输出什么?什么时候会重新分配?

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4}
    s1 := s[1:3:3]  // 注意第三个参数
    
    fmt.Println(len(s1), cap(s1))
    
    s1 = append(s1, 100)
    fmt.Println(s)
    fmt.Println(s1)
}

Playgroundhttps://go.dev/play/p/HoiDSIAKUNY

答案

2 2
[0 1 2 3 4]
[1 2 100]

解析

  • s[low:high:max]完整切片表达式,max限制容量
  • cap = max - low = 3 - 1 = 2
  • append时发现容量不够(cap=2, len=2),会分配新数组
  • 新数组与原数组脱离关系,修改不影响原切片

第24题:nil切片与空切片

问题:以下代码输出什么?nil切片和空切片有什么区别?

package main

import "fmt"

func main() {
    var s1 []int        // nil切片
    s2 := []int{}       // 空切片
    s3 := make([]int, 0) // 空切片
    
    fmt.Printf("s1 == nil: %t, len=%d, cap=%d\n", s1 == nil, len(s1), cap(s1))
    fmt.Printf("s2 == nil: %t, len=%d, cap=%d\n", s2 == nil, len(s2), cap(s2))
    fmt.Printf("s3 == nil: %t, len=%d, cap=%d\n", s3 == nil, len(s3), cap(s3))
}

Playgroundhttps://go.dev/play/p/oX2jD0M4WTW

答案

s1 == nil: true, len=0, cap=0
s2 == nil: false, len=0, cap=0
s3 == nil: false, len=0, cap=0

解析

  • nil切片:未初始化,指针为nil,== nil为true
  • 空切片:已初始化但长度为0,== nil为false
  • JSON序列化时:nil切片为null,空切片为[]
  • 通常建议返回空切片而非nil(避免前端null判断)

第25题:切片扩容机制

问题:切片的扩容策略是什么?以下代码输出什么?

package main

import "fmt"

func main() {
    s := make([]int, 0, 2)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
    
    s = append(s, 1)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
    
    s = append(s, 2)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
    
    s = append(s, 3)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
}

Playgroundhttps://go.dev/play/p/b2wxc0IO8UC

答案

len=0, cap=2
len=1, cap=2
len=2, cap=2
len=3, cap=4

解析

  • 扩容策略(Go 1.18+):
    • 原容量 < 256:新容量 = 原容量 × 2
    • 原容量 ≥ 256:新容量 = 原容量 + (原容量 + 3×256) / 4
  • 新数组分配后,旧数据会拷贝到新数组
  • 频繁扩容性能差,应预分配容量
  • 注意:Go 1.23+对切片扩容策略有进一步微调,但对于大多数应用场景行为一致

第26题:copy函数

问题:以下代码输出什么?copy有什么特点?

package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, 3)
    
    n := copy(dst, src)
    fmt.Printf("n=%d, dst=%v\n", n, dst)
    
    // 切片复制自身
    copy(src[1:], src[2:])
    fmt.Println(src)
}

Playgroundhttps://go.dev/play/p/uC_pSA4nfXI

答案

n=3, dst=[1 2 3]
[1 3 4 5 5]

解析

  • copy(dst, src)返回实际复制的元素个数(min(len(dst), len(src)))
  • 支持同一切片内的复制(如删除元素)
  • copy是按字节复制的,即使是重叠区域也能正确处理
  • 第一个输出是3,因为dst长度为3
  • 第二个是删除索引1元素的效果

第27题:map基础

问题:以下代码有什么问题?

package main

import "fmt"

func main() {
    var m map[string]int
    m["key"] = 1  // 有什么问题?
    
    m2 := make(map[string]int)
    m2["key"] = 2
    
    fmt.Println(m, m2)
}

Playgroundhttps://go.dev/play/p/5dJcD25GmZp

答案m["key"] = 1运行时panic:assignment to entry in nil map

解析

  • 未初始化的map是nil,不能写入
  • 必须用make初始化或用字面量创建
  • 但可以读取nil map,返回零值:v := m["key"]不会panic
  • 检查key是否存在:v, ok := m["key"]

第28题:map的并发安全

问题:以下代码有什么问题?如何修复?

package main

import "sync"

func main() {
    m := make(map[int]int)
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m[n] = n  // 并发写入
        }(i)
    }
    
    wg.Wait()
}

答案:运行时fatal error:concurrent map writes

解析

  • Go map不是并发安全
  • 并发读写会导致数据竞争和panic
  • 解决方案
    1. 使用sync.RWMutex加锁
    2. 使用sync.Map(Go 1.9+,特定场景)
    3. 使用channel串行化访问

第29题:map遍历顺序

问题:以下代码有什么特点?输出顺序是固定的吗?

package main

import "fmt"

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
    for k, v := range m {
        fmt.Printf("%s:%d ", k, v)
    }
}

答案:输出顺序不固定,每次运行可能不同。

解析

  • Go故意随机化map遍历顺序,防止开发者依赖特定顺序
  • 这是有意的设计决策,鼓励编写顺序无关的代码
  • 如果需要固定顺序,必须手动排序:
    keys := make([]string, 0, len(m))
    for k := range m { keys = append(keys, k) }
    sort.Strings(keys)
    

Playgroundhttps://go.dev/play/p/JuVHWJUX8En


第30题:map的key类型

问题:以下代码哪些能编译?

package main

func main() {
    // 哪些可以作为map的key?
    _ = make(map[[]int]int)     // 1
    _ = make(map[[3]int]int)    // 2
    _ = make(map[map[int]int]int) // 3
    _ = make(map[struct{ X int }]int) // 4
    _ = make(map[interface{}]int) // 5
}

答案:2、4、5能编译;1、3不能。

解析

  • map的key必须是可比较类型(支持==操作)
  • 可比较类型:bool、数字、string、指针、channel、interface、array(元素可比较)
  • 不可比较类型:slice、map、function
  • struct如果所有字段都可比较,则可比较

第31题:struct标签

问题:以下代码输出什么?struct标签有什么用?

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name" db:"user_name"`
    Age  int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Tom", Age: 0}
    
    b, _ := json.Marshal(u)
    fmt.Println(string(b))
    
    t := reflect.TypeOf(u)
    field, _ := t.FieldByName("Name")
    fmt.Println(field.Tag.Get("json"))
}

Playgroundhttps://go.dev/play/p/HzQDPSkVSUU

答案

{"name":"Tom"}
name

解析

  • struct标签是反射可读取的元数据字符串
  • json:"name"指定JSON序列化的字段名
  • omitempty表示零值时忽略该字段(Age=0被省略)
  • 通过reflect包可以读取标签值

第32题:struct嵌入

问题:以下代码输出什么?什么是嵌入字段?

package main

import "fmt"

type Person struct {
    Name string
}

func (p Person) SayHi() {
    fmt.Println("Hi, I'm", p.Name)
}

type Student struct {
    Person  // 嵌入字段
    Grade int
}

func main() {
    s := Student{Person: Person{Name: "Tom"}, Grade: 90}
    fmt.Println(s.Name)      // 能访问吗?
    s.SayHi()                // 能调用吗?
}

答案:都能访问,输出:

Tom
Hi, I'm Tom

解析

  • 嵌入字段(匿名字段)让外层struct自动获得内层的方法
  • 这是一种组合而非继承,但语法上像继承
  • 可以s.Name直接访问,等价于s.Person.Name
  • 方法也会被提升,可以直接调用

第33题:嵌入与同名字段

问题:以下代码输出什么?如何区分?

package main

import "fmt"

type A struct { Name string }
type B struct { Name string }

type C struct {
    A
    B
}

func main() {
    c := C{A{Name: "A_name"}, B{Name: "B_name"}}
    fmt.Println(c.Name)  // 编译错误?
}

Playgroundhttps://go.dev/play/p/12Zitezk21L

答案:编译错误:ambiguous selector c.Name

解析

  • 当多个嵌入字段有同名成员时,必须通过完整路径访问
  • 正确写法:c.A.Namec.B.Name
  • 这是一种设计保护,避免歧义

第34题:struct比较

问题:以下代码能否编译?输出什么?

package main

import "fmt"

type S1 struct {
    A int
    B string
}

type S2 struct {
    A int
    B string
}

type S3 struct {
    A []int
}

func main() {
    a := S1{A: 1, B: "x"}
    b := S2{A: 1, B: "x"}
    c := S3{A: []int{1}}
    d := S3{A: []int{1}}
    
    fmt.Println(a == S1(b))  // 能比较吗?
    fmt.Println(c == d)      // 能比较吗?
}

Playgroundhttps://go.dev/play/p/kwjhlDqDLpF

答案

  • a == S1(b)可以,输出true(类型转换后比较)
  • c == d编译错误:invalid operation: c == d (struct containing []int cannot be compared)

解析

  • struct可比较的条件:所有字段都可比较
  • 不同类型的struct不能直接比较,需显式转换
  • 包含slice、map、function的struct不可比较

第35题:空struct

问题:以下代码输出什么?空struct有什么特点?

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s struct{}
    fmt.Println("size:", unsafe.Sizeof(s))
    
    // 常用于信号传递
    ch := make(chan struct{})
    go func() {
        fmt.Println("working")
        ch <- struct{}{}
    }()
    <-ch
    fmt.Println("done")
}

Playgroundhttps://go.dev/play/p/lTiFDI7WEUn

答案

size: 0
working
done

解析

  • 空structstruct{}不占用内存(size为0)
  • 常用于:
    1. 信号channel(只关心事件,不关心数据)
    2. 实现Set:map[string]struct{}
    3. context取消信号
  • 所有空struct变量共享同一个内存地址

第36题:数组作为函数参数

问题:以下代码输出什么?

package main

import "fmt"

func modify(arr [3]int) {
    arr[0] = 999
    fmt.Println("in modify:", arr)
}

func modifyPtr(arr *[3]int) {
    arr[0] = 888
}

func main() {
    a := [3]int{1, 2, 3}
    modify(a)
    fmt.Println("after modify:", a)
    
    modifyPtr(&a)
    fmt.Println("after modifyPtr:", a)
}

答案

in modify: [999 2 3]
after modify: [1 2 3]
after modifyPtr: [888 2 3]

解析

  • 数组是值类型,作为参数会复制整个数组
  • 大数组传递应考虑使用指针或切片
  • [3]int[5]int是不同的类型

第37题:切片作为map的value

问题:以下代码能否编译?有什么问题?

package main

import "fmt"

func main() {
    m := make(map[string][]int)
    
    // 追加元素
    m["key"] = append(m["key"], 1)
    m["key"] = append(m["key"], 2)
    
    // 直接修改
    s := m["key"]
    s[0] = 999
    
    fmt.Println(m["key"])
}

答案:能编译,输出[999 2]

解析

  • slice是引用类型,即使从map取值出来也是指向同一切片header
  • s[0] = 999修改会影响map中的值
  • 注意append可能重新分配数组,此时s和map中的切片会分离

第38题:delete函数

问题:以下代码输出什么?delete有什么特点?

package main

import "fmt"

func main() {
    m := map[string]int{"a": 1, "b": 2}
    
    delete(m, "a")
    delete(m, "c")  // 删除不存在的key
    delete(m, "b")
    delete(m, "a")  // 再次删除
    
    fmt.Println(m)
    fmt.Println(len(m))
}

Playgroundhttps://go.dev/play/p/k9PZ_P-VCwT

答案

map[]
0

解析

  • delete(map, key)删除map中的键值对
  • 删除不存在的key不会报错,是安全的空操作
  • 重复删除同一个key也是安全的
  • map不是线程安全的,并发delete需要加锁

第39题:map值类型的方法调用

问题:以下代码能否编译?

package main

import "fmt"

type Counter struct {
    count int
}

func (c *Counter) Inc() {
    c.count++
}

func main() {
    m := map[string]Counter{
        "a": {count: 0},
    }
    
    m["a"].Inc()  // 能编译吗?
}

Playgroundhttps://go.dev/play/p/AS1hsBC1XfK

答案:编译错误:cannot call pointer method on m[“a”] or cannot take address of m[“a”]

解析

  • map的值是不可寻址的(not addressable)
  • m["a"]返回的是值的副本,无法获取地址调用指针方法
  • 解决方法
    1. map定义为map[string]*Counter
    2. 先取值,修改后再赋值回去

第40题:二维切片

问题:以下代码有什么问题?如何正确初始化?

package main

import "fmt"

func main() {
    // 尝试创建 3x3 的二维切片
    m := make([][]int, 3)
    for i := range m {
        m[i] = make([]int, 3)
    }
    
    m[1][1] = 999
    
    for i := range m {
        fmt.Println(m[i])
    }
}

Playgroundhttps://go.dev/play/p/z3BQnel_F7t

答案:代码正确,输出:

[0 0 0]
[0 999 0]
[0 0 0]

解析

  • 二维切片是"切片的切片”,需要逐层初始化
  • make([][]int, 3)只创建了外层切片(3个nil)
  • 每个内层切片需要单独make
  • 常见错误是只make外层,访问m[0][0]时panic

第41题:切片的截取与共享

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4}
    
    // 截取
    s1 := s[1:4]
    
    // append导致重新分配
    s2 := append(s1, 100)
    
    fmt.Println("s:", s)
    fmt.Println("s1:", s1)
    fmt.Println("s2:", s2)
    
    // 再append
    s3 := append(s2, 200)
    fmt.Println("s3:", s3)
    fmt.Println("s:", s)
}

Playgroundhttps://go.dev/play/p/KUw7s2qPfZS

答案

s: [0 1 2 3 100]
s1: [1 2 3]
s2: [1 2 3 100]
s3: [1 2 3 100 200]
s: [0 1 2 3 100]

解析

  • s1的cap=4(从索引1到末尾),还有1个容量
  • 第一次append填充了s[4]的位置,影响原切片
  • 第二次append时容量不足,重新分配,s3指向新数组

第42题:字符串不可变性

问题:以下代码能否编译?

package main

func main() {
    s := "hello"
    s[0] = 'H'  // 能编译吗?
}

答案:编译错误:cannot assign to s[0]

解析

  • Go字符串是不可变的(immutable)
  • 修改字符串必须转换为[]byte[]rune,修改后再转回
  • 字符串可以重新赋值(s = “Hello”),但不能修改内容
  • 不可变性使得字符串可以安全共享

第43题:字符串与byte切片转换

问题:以下代码有什么问题?

package main

import "fmt"

func main() {
    s := "Hello, 世界"
    
    b := []byte(s)
    r := []rune(s)
    
    fmt.Println(len(b), len(r))
    
    // 修改后再转回
    b[0] = 'h'
    s2 := string(b)
    
    r[7] = '中'
    s3 := string(r)
    
    fmt.Println(s2)
    fmt.Println(s3)
}

Playgroundhttps://go.dev/play/p/fr5FOspXdPw

答案:输出:

13 9
hello, 世界
Hello, 中国

解析

  • []byte(s)按字节转换,包含所有UTF-8字节
  • []rune(s)按字符转换,每个rune代表一个Unicode码点
  • 转换会复制数据,修改不影响原字符串
  • 选择[]byte还是[]rune取决于按字节还是按字符处理

第44题:数组初始化

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    // 各种初始化方式
    a1 := [5]int{1, 2, 3}           // 索引0,1,2有值
    a2 := [...]int{1, 2, 3, 4}      // 长度自动推断
    a3 := [5]int{2: 100, 4: 200}    // 按索引赋值
    a4 := [5]int{1: 10, 3: 30}      // 其他为0
    
    fmt.Println(a1)
    fmt.Println(a2)
    fmt.Println(a3)
    fmt.Println(a4)
}

Playgroundhttps://go.dev/play/p/rudzheEZFNn

答案

[1 2 3 0 0]
[1 2 3 4]
[0 0 100 0 200]
[0 10 0 30 0]

解析

  • [n]int{...}指定长度的数组初始化
  • [...]int{...}让编译器根据初始值数量推断长度
  • {index: value}语法可以按索引位置赋值
  • 未指定的元素为对应类型的零值

第45题:指针数组与数组指针

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    // 数组指针:指向数组的指针
    arr := [3]int{1, 2, 3}
    p := &arr
    
    // 指针数组:元素为指针的数组
    a, b, c := 1, 2, 3
    arr2 := [3]*int{&a, &b, &c}
    
    fmt.Println((*p)[0])  // 解引用后访问
    fmt.Println(*arr2[0]) // 访问指针元素
    
    // 修改
    (*p)[0] = 100
    *arr2[0] = 10
    
    fmt.Println(arr)
    fmt.Println(a)
}

Playgroundhttps://go.dev/play/p/S0nBMzjfD_K

答案

1
1
[100 2 3]
10

解析

  • *[3]int是指向数组的指针,解引用后可索引
  • [3]*int是元素为指针的数组
  • 注意运算符优先级:*p[0]*(p[0]),应该写(*p)[0]

三、函数与方法 (20题)

第46题:defer执行顺序

问题:以下代码输出什么?defer的执行顺序是什么?

package main

import "fmt"

func main() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    fmt.Println("main body")
}

Playgroundhttps://go.dev/play/p/v6piDHYjIrb

答案

main body
defer 3
defer 2
defer 1

解析

  • defer遵循**后进先出(LIFO)**的栈顺序
  • defer在函数返回前执行,可用于资源清理
  • 多个defer按声明的逆序执行
  • defer的参数在声明时求值,不是执行时

第47题:defer参数求值时机

问题:以下代码输出什么?为什么?

package main

import "fmt"

func main() {
    i := 0
    defer fmt.Println("defer:", i)  // 输出什么?
    i++
    fmt.Println("main:", i)
}

Playgroundhttps://go.dev/play/p/Gj-U2nIyBaw

答案

main: 1
defer: 0

解析

  • defer的参数在defer语句处立即求值,不是延迟执行时
  • defer fmt.Println(i)中的i在声明时被计算为0
  • 即使后面i变成1,defer输出仍是0
  • 要获取最终值,可用闭包:defer func() { fmt.Println(i) }()

第48题:defer闭包陷阱(⚠️ 版本差异题)

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}

Playgroundhttps://go.dev/play/p/m_HuJCx7mP6

答案

  • Go 1.21及之前:输出3 3 3
  • Go 1.22+:输出2 1 0(逆序,因为defer是LIFO)

解析

  • Go 1.21及之前:defer中的闭包捕获的是变量i的引用,不是值。函数返回时i已经是3(循环结束后的值),所以输出3 3 3

  • Go 1.22+:每次迭代创建新的i变量,defer捕获的是各自的值。由于defer是LIFO(后进先出),所以输出2 1 0

  • 向后兼容修复(推荐):

    for i := 0; i < 3; i++ {
        defer func(n int) {
            fmt.Println(n)
        }(i)
    }
    

第49题:defer与return

问题:以下代码输出什么?defer能否修改返回值?

package main

import "fmt"

func f() (result int) {
    defer func() {
        result++  // 修改命名返回值
    }()
    return 0  // 实际执行:result = 0; return
}

func g() int {
    result := 0
    defer func() {
        result++
    }()
    return result
}

func main() {
    fmt.Println("f():", f())
    fmt.Println("g():", g())
}

答案

f(): 1
g(): 0

解析

  • return xxx实际上分两步执行:先给返回值赋值,再执行defer,最后真正返回
  • 对于命名返回值,defer可以修改result,影响最终返回值
  • 对于非命名返回值,defer修改的是局部变量,不影响返回值

第50题:defer与panic

问题:以下代码输出什么?defer在panic时会执行吗?

package main

import "fmt"

func main() {
    defer fmt.Println("defer 1")
    defer func() {
        fmt.Println("defer 2")
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    defer fmt.Println("defer 3")
    
    panic("something wrong")
    fmt.Println("after panic")  // 不会执行
}

Playgroundhttps://go.dev/play/p/3VL5G2CSvr_7

答案

defer 3
defer 2
recovered: something wrong
defer 1

解析

  • panic会立即停止当前函数执行,但已注册的defer仍会执行
  • defer按LIFO顺序执行
  • recover()必须在defer中调用,可捕获panic并恢复
  • recover后程序从panic点恢复继续执行

第51题:panic与recover规则

问题:以下代码能否捕获panic?输出什么?

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    
    go func() {
        panic("goroutine panic")  // 能被捕获吗?
    }()
    
    select {}  // 阻塞
}

Playgroundhttps://go.dev/play/p/TP4rKKzcp1S

答案:不能捕获,程序会崩溃。

解析

  • recover只能捕获当前goroutine的panic
  • 其他goroutine的panic无法被当前goroutine捕获
  • 每个goroutine需要自己的recover处理
  • 未捕获的panic会导致整个程序退出

第52题:闭包与变量捕获(⚠️ 版本差异题)

问题:以下代码输出什么?

package main

import "fmt"

func makeFuncs() []func() {
    funcs := make([]func(), 3)
    for i := 0; i < 3; i++ {
        funcs[i] = func() {
            fmt.Println(i)
        }
    }
    return funcs
}

func main() {
    funcs := makeFuncs()
    for _, f := range funcs {
        f()
    }
}

Playgroundhttps://go.dev/play/p/E1_ivuky9I8

答案

  • Go 1.21及之前:输出3 3 3
  • Go 1.22+:输出0 1 2

解析

  • Go 1.21及之前:for循环中的变量i在整个循环中是同一个变量,闭包捕获的是变量的引用。所有函数都引用同一个i,调用时i已经是3。

  • Go 1.22+:每次迭代都会创建新的i变量,闭包捕获的是各自迭代的值,输出0 1 2

  • 向后兼容修复(推荐):

    for i := 0; i < 3; i++ {
        i := i  // 创建局部副本
        funcs[i] = func() {
            fmt.Println(i)
        }
    }
    // 或传递参数
    for i := 0; i < 3; i++ {
        funcs[i] = func(n int) func() {
            return func() {
                fmt.Println(n)
            }
        }(i)
    }
    

第53题:方法接收者

问题:以下代码输出什么?值接收者和指针接收者有什么区别?

package main

import "fmt"

type Counter struct {
    count int
}

func (c Counter) Value() int {
    return c.count
}

func (c *Counter) Inc() {
    c.count++
}

func main() {
    var c Counter
    c.Inc()
    fmt.Println(c.Value())
    
    // 以下都能编译吗?
    p := &Counter{}
    p.Inc()
    fmt.Println(p.Value())
}

答案:输出11。都能编译。

解析

  • 值接收者:方法内是值的副本,修改不影响原值
  • 指针接收者:可以修改原值
  • Go自动处理取地址和解引用:c.Inc()等价于(&c).Inc()p.Value()等价于(*p).Value()
  • 通常:需要修改用指针接收者,只读取用值接收者

第54题:方法集

问题:以下哪些调用是合法的?

package main

type T struct{}

func (T) M1() {}     // 值接收者
func (*T) M2() {}    // 指针接收者

func main() {
    var t T
    var p = &t
    
    t.M1()  // ?
    t.M2()  // ?
    p.M1()  // ?
    p.M2()  // ?
}

Playgroundhttps://go.dev/play/p/6Wfg2p5D1HY

答案:全部合法!

解析

调用 合法性 实际调用
t.M1() T.M1(t)
t.M2() (*T).M2(&t)(自动取地址)
p.M1() T.M1(*p)(自动解引用)
p.M2() (*T).M2(p)
  • 方法调用时,Go自动进行取地址(&)或解引用(*)转换
  • 但interface的方法集有区别(见下一题)

第55题:接口的方法集

问题:以下代码能否编译?

package main

type MyInterface interface {
    M1()
    M2()
}

type T struct{}

func (T) M1() {}
func (*T) M2() {}

func main() {
    var t T
    var i MyInterface = t  // 能编译吗?
    _ = i
}

Playgroundhttps://go.dev/play/p/h912-IdG0-y

答案:编译错误:T does not implement MyInterface (M2 method has pointer receiver)

解析

  • 值类型T的方法集:只有值接收者的方法(M1)
  • 指针类型*T的方法集:值接收者和指针接收者的方法(M1+M2)
  • interface赋值时:
    • i = t要求T实现所有方法(不包括仅指针接收者的方法)
    • i = &t要求*T实现所有方法

第56题:nil接收者

问题:以下代码会panic吗?输出什么?

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func (n *Node) Sum() int {
    if n == nil {
        return 0
    }
    return n.Value + n.Next.Sum()
}

func main() {
    var n *Node
    fmt.Println(n.Sum())  // 会panic吗?
}

Playgroundhttps://go.dev/play/p/2GdXhY7MKjw

答案:输出0,不会panic。

解析

  • 方法调用n.Sum()本质上是Node.Sum(n)
  • n可以为nil,在方法内部检查即可
  • 这是一种Go惯用法(如error接口的Error()方法)
  • 但调用n.Value如果n为nil会panic

第57题:函数是一等公民

问题:以下代码输出什么?

package main

import "fmt"

type Handler func(int) int

func (h Handler) Process(n int) int {
    return h(n) * 2
}

func addOne(x int) int {
    return x + 1
}

func main() {
    h := Handler(addOne)
    fmt.Println(h.Process(5))
}

Playgroundhttps://go.dev/play/p/cpAvjKQ9b9C

答案:输出12((5+1)*2)

解析

  • 函数类型可以定义方法
  • 可以实现接口(使函数具有面向对象能力)
  • 常用于http.Handler等场景

第58题:匿名函数与递归

问题:以下代码输出什么?如何写递归匿名函数?

package main

import "fmt"

func main() {
    // 递归匿名函数
    var fib func(int) int
    fib = func(n int) int {
        if n < 2 {
            return n
        }
        return fib(n-1) + fib(n-2)
    }
    
    fmt.Println(fib(10))
}

Playgroundhttps://go.dev/play/p/u3bMOPrLbHm

答案:输出55(第10个斐波那契数)

解析

  • 匿名函数要递归,必须先声明变量var fib func(int) int
  • 然后再赋值,才能在函数体内引用自己
  • 不能直接在:=定义的匿名函数内调用自己

第59题:函数作为参数和返回值

问题:以下代码实现了什么设计模式?输出什么?

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 3; i++ {
        fmt.Println(pos(i), neg(-2*i))
    }
}

Playgroundhttps://go.dev/play/p/WdTiyfDzFJM

答案

0 0
1 -2
3 -6

解析

  • 实现了闭包(Closure)工厂模式
  • 每个adder()返回的函数有自己的sum变量(词法作用域)
  • pos和neg是两个独立的闭包,互不影响
  • 闭包可以记住并访问外部函数的变量

第60题:高阶函数

问题:以下代码实现了什么功能?

package main

import "fmt"

func Map(slice []int, fn func(int) int) []int {
    result := make([]int, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter(slice []int, fn func(int) bool) []int {
    result := []int{}
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    
    doubled := Map(nums, func(x int) int { return x * 2 })
    evens := Filter(nums, func(x int) bool { return x%2 == 0 })
    
    fmt.Println(doubled)
    fmt.Println(evens)
}

Playgroundhttps://go.dev/play/p/jB03ivVAbKJ

答案

[2 4 6 8 10]
[2 4]

解析

  • 实现了函数式编程的Map和Filter操作
  • 高阶函数:接收函数作为参数或返回函数的函数
  • Go不是纯函数式语言,但可以借鉴函数式风格

第61题:方法表达式

问题:以下代码输出什么?方法表达式有什么用?

package main

import "fmt"

type Person struct {
    Name string
}

func (p Person) Greet() string {
    return "Hello, " + p.Name
}

func main() {
    p := Person{Name: "Tom"}
    
    // 方法值
    greetFunc := p.Greet
    fmt.Println(greetFunc())
    
    // 方法表达式
    greetExpr := Person.Greet
    fmt.Println(greetExpr(p))
}

Playgroundhttps://go.dev/play/p/WSDeic4Q02-

答案:都输出Hello, Tom

解析

  • 方法值p.Greet,接收者已绑定,调用时无需传接收者
  • 方法表达式Person.Greet,接收者未绑定,调用时需传接收者
  • 方法表达式在需要延迟绑定接收者时有用

第62题:defer与循环

问题:以下代码有什么问题?输出什么?

package main

import (
    "fmt"
    "os"
)

func processFiles(files []string) error {
    for _, file := range files {
        f, err := os.Open(file)
        if err != nil {
            return err
        }
        defer f.Close()  // 有问题吗?
        
        // 处理文件...
        fmt.Println("processing", file)
    }
    return nil
}

func main() {
    _ = processFiles([]string{"/etc/passwd"})
}

答案:能运行,但有资源泄漏风险

解析

  • defer在函数返回时才执行,不是在循环结束时
  • 如果files有100个文件,会同时打开100个文件句柄
  • 修复:将文件处理逻辑抽成函数
    func processFile(file string) error {
        f, err := os.Open(file)
        if err != nil {
            return err
        }
        defer f.Close()
        // 处理...
        return nil
    }
    

Playgroundhttps://go.dev/play/p/UJkG-186P4F


第63题:函数返回值命名

问题:以下代码有什么问题?

package main

import "fmt"

func divide(a, b int) (result int, err error) {
    if b == 0 {
        err = fmt.Errorf("cannot divide by zero")
        return  // 裸return
    }
    result = a / b
    return  // 裸return
}

func main() {
    r, e := divide(10, 0)
    fmt.Println(r, e)
}

Playgroundhttps://go.dev/play/p/uCoT52KwFEN

答案:输出0 cannot divide by zero

解析

  • 命名返回值可以作为函数内的局部变量使用
  • return(裸return)自动返回命名返回值
  • 优点:文档化返回值含义,方便修改
  • 缺点:裸return可能降低可读性,长函数慎用

第65题:init函数的执行顺序

问题:假设有两个文件a.go和b.go,init执行顺序是什么?

// a.go
package main

import "fmt"

func init() {
    fmt.Println("a.go init")
}

// b.go  
package main

import "fmt"

func init() {
    fmt.Println("b.go init")
}

// main.go
package main

import "fmt"

func init() {
    fmt.Println("main.go init")
}

func main() {
    fmt.Println("main")
}

Playgroundhttps://go.dev/play/p/vSs1-Z6I6O_i

答案:按文件名字母顺序执行init,然后是main:

a.go init
b.go init
main.go init
main

解析

  • 同包内多个文件的init按文件名排序执行
  • 不同包之间:先执行导入包的init(递归),再执行当前包
  • 一个文件可以有多个init函数,按出现顺序执行

Playground:需要多文件,略


第66题:context.WithTimeout

问题:以下代码会输出什么?

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("work done")
    case <-ctx.Done():
        fmt.Println("timeout:", ctx.Err())
    }
}

Playgroundhttps://go.dev/play/p/32DeShyJLau

答案:输出timeout: context deadline exceeded

解析

  • context超时后,ctx.Done() channel会被关闭
  • ctx.Err()返回context取消的原因
  • defer cancel()即使超时也会执行,释放资源
  • 超时设置1秒,工作需2秒,触发超时分支


四、接口与反射 (20题)

第67题:接口基础

问题:以下代码输出什么?

package main

import "fmt"

type Animal interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow!"
}

func MakeSound(a Animal) {
    fmt.Println(a.Speak())
}

func main() {
    MakeSound(Dog{})
    MakeSound(Cat{})
}

答案

Woof!
Meow!

解析

  • Go接口是隐式实现,不需要显式声明implements
  • 只要类型实现了接口的所有方法,就自动实现该接口
  • 接口值由两部分组成:类型指针和值指针(动态类型+动态值)

第68题:接口的nil判断

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    var p *int = nil
    var i interface{} = p
    
    fmt.Println(p == nil)  // ?
    fmt.Println(i == nil)  // ?
}

Playgroundhttps://go.dev/play/p/vqDbxpGbnat

答案

true
false

解析

  • 接口值nil需要类型和值都为nil
  • i的类型是*int,值是nil,所以接口值不是nil
  • 这是Go接口的一个常见陷阱
  • 判断接口内部值是否为nil:先用类型断言取出再判断

第69题:类型断言

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    var i interface{} = "hello"
    
    s := i.(string)  // 直接断言
    fmt.Println(s)
    
    n, ok := i.(int)  // 安全断言
    fmt.Println(n, ok)
    
    // f := i.(float64)  // 会怎样?
}

Playgroundhttps://go.dev/play/p/JYjuOWRessb

答案

hello
0 false

解析

  • 类型断言i.(T)获取接口的动态值
  • 直接断言失败会panic
  • 安全断言v, ok := i.(T)失败时ok=false,v为零值
  • i.(float64)会panic(运行时panic)

第70题:空接口interface

问题:以下代码有什么问题?空接口有什么用?

package main

import "fmt"

func PrintAny(v interface{}) {
    fmt.Printf("value: %v, type: %T\n", v, v)
}

func main() {
    PrintAny(42)
    PrintAny("hello")
    PrintAny([]int{1, 2, 3})
    PrintAny(nil)
}

Playgroundhttps://go.dev/play/p/jqhV8TSO63M

答案:无问题,都能正常输出。

解析

  • interface{}空接口,可以保存任意类型的值
  • 类似于C的void*或Java的Object
  • 常用于:
    • fmt.Println的参数
    • 泛型编程(Go 1.18前)
    • JSON序列化/反序列化
  • 使用类型断言或反射获取具体类型

第71题:接口嵌套

问题:以下代码展示了什么特性?

package main

import "fmt"

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
}

type MyIO struct{}

func (m MyIO) Read(p []byte) (int, error) {
    return 0, nil
}

func (m MyIO) Write(p []byte) (int, error) {
    return len(p), nil
}

func main() {
    var rw ReadWriter = MyIO{}
    fmt.Printf("%T implements ReadWriter\n", rw)
}

Playgroundhttps://go.dev/play/p/uwYGv1KGIQo

答案:输出main.MyIO implements ReadWriter

解析

  • 接口可以嵌套其他接口
  • ReadWriter自动拥有ReaderWriter的所有方法
  • 实现了Reader和Writer的类型自动实现ReadWriter
  • Go标准库io包大量使用了这种模式

第72题:反射获取类型信息

问题:以下代码输出什么?

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Tom", Age: 30}
    
    t := reflect.TypeOf(p)
    v := reflect.ValueOf(p)
    
    fmt.Println("Type:", t)
    fmt.Println("Kind:", t.Kind())
    fmt.Println("Value:", v)
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        fmt.Printf("%s: %v\n", field.Name, value)
    }
}

Playgroundhttps://go.dev/play/p/hxksj0uJvW3

答案

Type: main.Person
Kind: struct
Value: {Tom 30}
Name: Tom
Age: 30

解析

  • reflect.TypeOf()获取类型信息
  • reflect.ValueOf()获取值信息
  • Kind()返回基本类型(struct、int、slice等)
  • 可遍历结构体字段,获取标签等信息

第73题:反射修改值

问题:以下代码能否成功修改?

package main

import (
    "fmt"
    "reflect"
)

func main() {
    x := 10
    v := reflect.ValueOf(x)
    // v.SetInt(20)  // 会怎样?
    
    v2 := reflect.ValueOf(&x).Elem()
    v2.SetInt(20)
    fmt.Println(x)
}

Playgroundhttps://go.dev/play/p/Rar3_AAhPBf

答案v.SetInt(20)会panic,v2.SetInt(20)成功,x变为20

解析

  • reflect.ValueOf(x)返回的是值的副本,不可修改
  • reflect.ValueOf(&x).Elem()获取指针指向的元素,可修改
  • 修改前需要检查CanSet()
  • 只有可寻址的值才能修改

第74题:反射调用方法

问题:以下代码输出什么?

package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int {
    return a + b
}

func (c Calculator) Subtract(a, b int) int {
    return a - b
}

func main() {
    c := Calculator{}
    v := reflect.ValueOf(c)
    
    method := v.MethodByName("Add")
    args := []reflect.Value{
        reflect.ValueOf(10),
        reflect.ValueOf(5),
    }
    result := method.Call(args)
    
    fmt.Println(result[0].Int())
}

Playgroundhttps://go.dev/play/p/eata3f-j-F-

答案:输出15

解析

  • MethodByName获取方法(值接收者方法)
  • Call调用方法,传入[]reflect.Value参数
  • 返回[]reflect.Value结果
  • 注意:值接收者和指针接收者方法的获取方式略有不同

第75题:反射与JSON标签

问题:以下代码展示了什么用法?

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type User struct {
    Name  string `json:"name" db:"user_name"`
    Email string `json:"email,omitempty"`
    Age   int    `json:"-"`
}

func main() {
    u := User{Name: "Tom", Email: "tom@example.com", Age: 30}
    
    t := reflect.TypeOf(u)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s, JSON: %s\n", field.Name, field.Tag.Get("json"))
    }
    
    b, _ := json.Marshal(u)
    fmt.Println(string(b))
}

Playgroundhttps://go.dev/play/p/vT-lVb13MN8

答案

Field: Name, JSON: name
Field: Email, JSON: email,omitempty
Field: Age, JSON: -
{"name":"Tom","email":"tom@example.com"}

解析

  • `json:"name"`是结构体标签,反射可读取
  • json:"-"表示该字段不参与JSON序列化
  • omitempty表示零值时省略
  • 反射广泛用于ORM、序列化库等

第76题:接口比较

问题:以下代码能否编译?输出什么?

package main

import "fmt"

func main() {
    var a interface{} = []int{1, 2, 3}
    var b interface{} = []int{1, 2, 3}
    
    fmt.Println(a == b)  // ?
}

Playgroundhttps://go.dev/play/p/Z1ehsXDV9Nb

答案:运行时panic

解析

  • 接口值可比较,但内部值必须是可比较类型
  • slice是不可比较类型(不能用==)
  • 运行时会panic:runtime error: comparing uncomparable type []int
  • 比较前先判断类型,或使用反射

第77题:类型switch

问题:以下代码输出什么?

package main

import "fmt"

func printType(v interface{}) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %s\n", x)
    case []int:
        fmt.Printf("[]int with len=%d\n", len(x))
    default:
        fmt.Printf("unknown: %T\n", x)
    }
}

func main() {
    printType(42)
    printType("hello")
    printType([]int{1, 2, 3})
    printType(3.14)
}

Playgroundhttps://go.dev/play/p/VBPgahU_Zbk

答案

int: 42
string: hello
[]int with len=3
unknown: float64

解析

  • 类型switch可以同时判断类型并获取值
  • x在每个case中具有对应类型
  • 比多次类型断言更简洁
  • 可以用case nil:判断nil值

第78题:反射创建实例

问题:以下代码输出什么?

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int
}

func main() {
    t := reflect.TypeOf(User{})
    
    // 创建实例
    v := reflect.New(t).Elem()
    v.FieldByName("Name").SetString("Alice")
    v.FieldByName("Age").SetInt(25)
    
    user := v.Interface().(User)
    fmt.Println(user)
}

Playgroundhttps://go.dev/play/p/hxRUqPYgy0e

答案:输出{Alice 25}

解析

  • reflect.New(t)创建指向类型的指针
  • Elem()获取指针指向的元素
  • 设置字段值后,用Interface()转回interface{}
  • 最后类型断言获取具体类型

第79题:接口的底层结构

问题:接口值在内存中是如何表示的?

答案

interface {
    type  uintptr   // 指向类型信息的指针
    data  uintptr   // 指向值的指针
}

或详细结构:

type iface struct {
    tab  *itab      // 类型信息+方法表
    data unsafe.Pointer
}

type eface struct {  // 空接口
    _type *_type
    data  unsafe.Pointer
}

Playgroundhttps://go.dev/play/p/SHpwUfEOCnN

解析

  • 接口值由两部分组成:类型描述符数据指针
  • 非空接口(interface{Method()})使用iface
  • 空接口(interface{})使用eface
  • 理解这个结构有助于理解接口nil判断等问题

第80题:断言链式调用

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    var i interface{} = map[string]interface{}{
        "user": map[string]interface{}{
            "name": "Tom",
            "age":  30,
        },
    }
    
    if m, ok := i.(map[string]interface{}); ok {
        if user, ok := m["user"].(map[string]interface{}); ok {
            if name, ok := user["name"].(string); ok {
                fmt.Println("Name:", name)
            }
        }
    }
}

Playgroundhttps://go.dev/play/p/frHYYF_W434

答案:输出Name: Tom

解析

  • JSON反序列化后常是map[string]interface{}结构
  • 需要层层类型断言才能访问深层字段
  • 代码冗长且易出错,实际项目中建议使用结构体或第三方库
  • Go 1.18+泛型可以部分改善这种情况

第81题:反射的局限性

问题:反射有哪些缺点?在什么情况下应避免使用?

答案

  1. 性能损耗:反射比直接调用慢10-100倍
  2. 类型安全:编译时无法检查,运行时可能panic
  3. 代码可读性差:难以理解维护
  4. 无法获取私有字段(包外):未导出字段无法访问

应避免场景

  • 性能敏感的代码路径
  • 可以用类型参数(泛型)替代时
  • 简单类型转换场景

适合场景

  • 序列化/反序列化(JSON、XML等)
  • ORM框架
  • 依赖注入框架
  • 需要处理多种类型的通用工具函数

第82题:接口赋值给接口

问题:以下代码能否编译?

package main

import "fmt"

type Stringer interface {
    String() string
}

type Printer interface {
    String() string
    Print()
}

type MyType struct{}

func (m MyType) String() string { return "mytype" }
func (m MyType) Print()         { fmt.Println(m.String()) }

func main() {
    var p Printer = MyType{}
    var s Stringer = p  // 能赋值吗?
    fmt.Println(s.String())
}

Playgroundhttps://go.dev/play/p/p9vA78921kZ

答案:能编译,输出mytype

解析

  • Printer实现了Stringer的所有方法,所以Printer可以赋值给Stringer
  • 接口可以向上转型(小接口赋值给大接口)
  • 但大接口不能赋值给小接口(方法可能不全)

第83题:reflect.DeepEqual

问题:以下代码输出什么?DeepEqual有什么作用?

package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := []int{1, 2, 3}
    b := []int{1, 2, 3}
    
    fmt.Println(a == b)  // 能编译吗?
    fmt.Println(reflect.DeepEqual(a, b))
    
    m1 := map[string]int{"a": 1}
    m2 := map[string]int{"a": 1}
    fmt.Println(reflect.DeepEqual(m1, m2))
}

Playgroundhttps://go.dev/play/p/5f2KXrd4oSo

答案

  • a == b编译错误
  • reflect.DeepEqual(a, b)输出true
  • reflect.DeepEqual(m1, m2)输出true(顺序可能不同)

解析

  • reflect.DeepEqual可以比较不可比较类型(slice、map、struct含slice等)
  • 递归比较,支持嵌套结构
  • 注意:对于float的NaN处理有特殊情况

第84题:接口的类型擦除

问题:以下代码能否获取原始类型?

package main

import (
    "fmt"
    "reflect"
)

type MyInt int

func main() {
    var i interface{} = MyInt(10)
    
    fmt.Printf("%T\n", i)  // 输出什么?
    
    t := reflect.TypeOf(i)
    fmt.Println(t.Name())  // 输出什么?
    fmt.Println(t.Kind())  // 输出什么?
}

Playgroundhttps://go.dev/play/p/ROE4Gkq0CXc

答案

main.MyInt
MyInt
int

解析

  • %Treflect.TypeOf可以获取原始类型信息
  • Name()返回自定义类型名
  • Kind()返回底层基础类型(int、struct等)
  • 接口保留了完整的类型信息,不是类型擦除

第85题:接口与指针接收者

问题:以下代码有什么问题?

package main

import "fmt"

type Sayer interface {
    Say()
}

type Cat struct {
    name string
}

func (c *Cat) Say() {
    fmt.Println(c.name, "says meow")
}

func main() {
    var c Cat = Cat{name: "Kitty"}
    var s Sayer = c  // 能编译吗?
    s.Say()
}

Playgroundhttps://go.dev/play/p/NkkNC4U4zWt

答案:编译错误:Cat does not implement Sayer (Say method has pointer receiver)

解析

  • Say()方法有指针接收者,只有*Cat实现了Sayer
  • Cat(值类型)没有实现Sayer
  • 修复:var s Sayer = &c

第86题:反射与函数

问题:以下代码输出什么?

package main

import (
    "fmt"
    "reflect"
)

func Add(a, b int) int {
    return a + b
}

func main() {
    f := reflect.ValueOf(Add)
    
    fmt.Println("Kind:", f.Kind())
    fmt.Println("Type:", f.Type())
    
    args := []reflect.Value{
        reflect.ValueOf(10),
        reflect.ValueOf(20),
    }
    result := f.Call(args)
    fmt.Println("Result:", result[0])
}

Playgroundhttps://go.dev/play/p/krDxL4L9c9h

答案

Kind: func
Type: func(int, int) int
Result: 30

解析

  • 函数也可以通过反射调用
  • Kind()返回func
  • Type()返回完整的函数签名
  • 参数和返回值都是[]reflect.Value

五、并发编程 (30题)

第87题:Goroutine基础

问题:以下代码输出什么?main函数会等待goroutine吗?

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Hello from goroutine")
}

func main() {
    go sayHello()
    fmt.Println("Hello from main")
    // 没有等待
}

Playgroundhttps://go.dev/play/p/3Paq6heGhAk

答案:可能只输出Hello from main,goroutine来不及执行。

解析

  • main函数结束会立即终止程序,不管goroutine是否完成
  • 需要使用sync.WaitGroup或channel等待goroutine
  • goroutine是Go协程,由Go运行时调度,非OS线程

第88题:WaitGroup等待

问题:修正上一题的代码,确保goroutine完成。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Hello from goroutine")
    }()
    
    fmt.Println("Hello from main")
    wg.Wait()  // 等待
}

Playgroundhttps://go.dev/play/p/BNYrE-SFXqQ

答案:两个消息都输出。

解析

  • wg.Add(n)添加n个等待计数
  • wg.Done()完成一个,减1
  • wg.Wait()阻塞直到计数为0
  • defer wg.Done()确保即使panic也会调用

第89题:Channel基础

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    go func() {
        ch <- 42
    }()
    
    value := <-ch
    fmt.Println(value)
}

答案:输出42

解析

  • channel是goroutine间通信的管道
  • ch <- 42发送数据到channel
  • <-ch从channel接收数据
  • 无缓冲channel的发送和接收会阻塞,直到配对

第90题:有缓冲Channel

问题:以下代码会死锁吗?

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)  // 缓冲区大小2
    
    ch <- 1
    ch <- 2
    // ch <- 3  // 会怎样?
    
    go func() {
        time.Sleep(time.Second)
        fmt.Println(<-ch)
    }()
    
    fmt.Println(<-ch)
    time.Sleep(2 * time.Second)
}

Playgroundhttps://go.dev/play/p/El6VYZE7Usf

答案:不会死锁。如果发送第3个值会阻塞。

解析

  • 有缓冲channel只在缓冲区满时阻塞发送
  • 只在缓冲区空时阻塞接收
  • ch <- 3会阻塞,因为没有接收者
  • 容量(capacity)和长度(length)概念类似slice

第91题:Channel关闭与range

问题:以下代码输出什么?

package main

import "fmt"

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    
    for v := range ch {
        fmt.Println(v)
    }
    fmt.Println("done")
}

Playgroundhttps://go.dev/play/p/QY_CBtf3JP3

答案:输出0到4,然后done

解析

  • range ch从channel接收值直到channel关闭
  • 关闭channel后,range结束循环
  • 接收关闭的channel会返回零值,可用v, ok := <-ch判断是否关闭
  • 只能关闭一次,关闭后不能再发送

第92题:Select多路复用

问题:以下代码输出什么?

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()
    
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()
    
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }
}

答案

from ch1
from ch2

解析

  • select同时等待多个channel操作
  • 哪个channel就绪就执行哪个case
  • 多个就绪时随机选择一个(避免饥饿)
  • 没有default且都阻塞时会等待

第93题:Select默认case

问题:以下代码输出什么?

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    select {
    case v := <-ch:
        fmt.Println("received:", v)
    default:
        fmt.Println("no data")
    }
}

答案:输出no data

解析

  • default case使select变为非阻塞
  • 如果没有channel就绪,执行default
  • 可用于非阻塞发送:select { case ch <- v: default: }
  • 常用于超时处理、心跳检测等

第94题:Select超时

问题:以下代码实现了什么功能?

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    
    select {
    case v := <-ch:
        fmt.Println("received:", v)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("timeout")
    }
}

答案:输出timeout(500ms后)

解析

  • time.After返回一个超时channel
  • select等待接收或超时,哪个先到执行哪个
  • 常用模式:防止goroutine永久阻塞
  • 注意:time.After会创建新goroutine,大量使用时考虑context.WithTimeout

第95题:Mutex互斥锁

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "sync"
)

func main() {
    var counter int
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Println(counter)
}

答案:输出可能小于1000,有数据竞争

解析

  • counter++不是原子操作(读取-增加-写入)
  • 多个goroutine同时操作会产生竞争
  • 修复:使用sync.Mutexsync/atomic

第96题:Mutex正确使用

问题:修正上一题的代码。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var counter int
    var mu sync.Mutex
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }
    
    wg.Wait()
    fmt.Println(counter)
}

答案:输出1000

解析

  • mu.Lock()获取锁,mu.Unlock()释放锁
  • 确保同时只有一个goroutine访问临界区
  • 建议用defer mu.Unlock()避免遗漏
  • 锁粒度应尽可能小,减少竞争

第97题:RWMutex读写锁

问题:以下代码展示了什么用法?

package main

import (
    "fmt"
    "sync"
    "time"
)

type Cache struct {
    mu    sync.RWMutex
    data  map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func main() {
    c := &Cache{data: make(map[string]string)}
    c.Set("a", "1")
    fmt.Println(c.Get("a"))
}

答案:输出1

解析

  • RWMutex区分读锁和写锁
  • RLock/RUnlock:多个读可以并行
  • Lock/Unlock:写独占,读写互斥
  • 读多写少场景性能优于Mutex

第98题:死锁检测

问题:以下代码有什么问题?

package main

import "sync"

func main() {
    var mu sync.Mutex
    
    mu.Lock()
    mu.Lock()  // 会怎样?
    
    mu.Unlock()
    mu.Unlock()
}

答案:第二次Lock会永久阻塞,造成死锁

解析

  • 标准Mutex不支持重入(递归锁)
  • 同一线程重复Lock会导致死锁
  • 需要重入的场景可以:
    • 重新设计避免重入
    • 使用channel替代
    • 记录goroutine ID实现可重入锁(不推荐)

第99题:Once单例

问题:以下代码输出什么?Once有什么特点?

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    
    for i := 0; i < 10; i++ {
        go func(n int) {
            once.Do(func() {
                fmt.Println("Executed by goroutine", n)
            })
        }(i)
    }
    
    // 等待所有goroutine
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func(n int) {
            defer wg.Done()
            once.Do(func() {
                fmt.Println("Should not print")
            })
        }(i)
    }
    wg.Wait()
}

答案Executed by goroutine只输出一次。

解析

  • sync.Once确保函数只执行一次
  • 常用于单例模式、初始化操作
  • 即使函数panic,Once也认为已执行,不会重试
  • 并发安全,无需额外锁

第100题:Pool对象池

问题:以下代码展示了什么用法?

package main

import (
    "bytes"
    "fmt"
    "sync"
)

var pool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func main() {
    // 获取对象
    buf := pool.Get().(*bytes.Buffer)
    buf.WriteString("hello")
    fmt.Println(buf.String())
    
    // 归还对象
    buf.Reset()
    pool.Put(buf)
    
    // 再次获取(可能是同一个)
    buf2 := pool.Get().(*bytes.Buffer)
    fmt.Println(buf2.String())
}

答案:可能输出hello和空字符串(取决于是否获取到同一个对象)

解析

  • sync.Pool用于缓存临时对象,减少GC压力
  • Get获取对象,Put归还对象
  • Pool中的对象可能被GC随时回收,不保证持久
  • 适合用于缓冲区、临时对象等

第101题:Cond条件变量

问题:以下代码输出什么?

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    ready := false
    
    go func() {
        time.Sleep(100 * time.Millisecond)
        mu.Lock()
        ready = true
        cond.Broadcast()  // 通知所有等待者
        mu.Unlock()
    }()
    
    mu.Lock()
    for !ready {
        cond.Wait()  // 等待条件
    }
    fmt.Println("ready!")
    mu.Unlock()
}

答案:输出ready!

解析

  • sync.Cond用于复杂的等待/通知场景
  • Wait()会释放锁并阻塞,被唤醒后重新获取锁
  • Signal()唤醒一个等待者,Broadcast()唤醒所有
  • 必须用for检查条件(防止虚假唤醒)
  • 在Go中,更多场景可用channel替代Cond

第102题:Context取消

问题:以下代码展示了什么模式?

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d stopped\n", id)
            return
        default:
            fmt.Printf("worker %d working\n", id)
            time.Sleep(100 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    for i := 0; i < 3; i++ {
        go worker(ctx, i)
    }
    
    time.Sleep(250 * time.Millisecond)
    cancel()  // 取消所有
    time.Sleep(100 * time.Millisecond)
}

答案:主协程取消后,所有worker停止。

解析

  • context.WithCancel创建可取消的上下文
  • cancel()关闭ctx.Done()channel,通知所有监听者
  • 用于级联取消多个goroutine
  • context应该作为函数第一个参数

第103题:Context超时与值

问题:以下代码输出什么?

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.WithValue(context.Background(), "user", "Alice")
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()
    
    // 获取值
    if user := ctx.Value("user"); user != nil {
        fmt.Println("user:", user)
    }
    
    select {
    case <-time.After(200 * time.Millisecond):
        fmt.Println("work done")
    case <-ctx.Done():
        fmt.Println("timeout:", ctx.Err())
    }
}

答案

user: Alice
timeout: context deadline exceeded

解析

  • context.WithValue传递请求相关的元数据
  • context可以链式组合(超时+值)
  • ctx.Value获取值,键可以是任何可比较类型
  • 值传递应该只用于请求元数据,不要传业务参数

第104题:原子操作

问题:以下代码输出什么?原子操作有什么优势?

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }
    
    wg.Wait()
    fmt.Println(counter)
    fmt.Println(atomic.LoadInt64(&counter))
}

答案:输出1000两次。

解析

  • sync/atomic提供原子操作,比锁更高效
  • AddInt64LoadInt64StoreInt64CompareAndSwapInt64
  • 适用于简单计数器、标志位等场景
  • Go 1.19+增加了atomic.Int64等类型,更易用

第105题:channel关闭原则

问题:channel关闭的最佳实践是什么?

答案

  1. 谁发送谁关闭:发送方负责关闭channel
  2. 不要重复关闭:关闭已关闭的channel会panic
  3. 不要向关闭的channel发送:会panic
  4. 接收方检查关闭状态v, ok := <-ch,ok为false表示关闭

示例

// 生产者关闭
func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

// 消费者检测
func consumer(ch <-chan int) {
    for v := range ch {
        fmt.Println(v)
    }
}

第106题:扇出Fan-out

问题:以下代码展示了什么并发模式?

package main

import (
    "fmt"
    "sync"
    "time"
)

func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {
        time.Sleep(100 * time.Millisecond) // 模拟工作
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    go producer(jobs)
    
    var wg sync.WaitGroup
    for w := 0; w < 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }
    
    go func() {
        wg.Wait()
        close(results)
    }()
    
    for r := range results {
        fmt.Println(r)
    }
}

答案:展示了**扇出(Fan-out)**模式,多个worker并发处理任务。

解析

  • 一个生产者,多个消费者(worker)
  • 实现并行处理,提高吞吐量
  • 使用WaitGroup等待所有worker完成
  • 适用于CPU密集型或IO密集型任务并行化

第107题:扇入Fan-in

问题:以下代码展示了什么并发模式?

package main

import (
    "fmt"
    "sync"
)

func producer(name string, ch chan<- int) {
    for i := 0; i < 3; i++ {
        ch <- i
    }
}

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    
    wg.Add(len(channels))
    for _, ch := range channels {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    
    go func() {
        producer("A", ch1)
        close(ch1)
    }()
    
    go func() {
        producer("B", ch2)
        close(ch2)
    }()
    
    for v := range fanIn(ch1, ch2) {
        fmt.Println(v)
    }
}

答案:展示了**扇入(Fan-in)**模式,多个channel合并为一个。

解析

  • 将多个输入channel合并到单个输出channel
  • 无需关心数据来自哪个源
  • 常用于聚合多个服务/数据源的结果
  • 与扇出配合使用,形成Pipeline

第108题:Pipeline管道

问题:以下代码展示了什么并发模式?

package main

import "fmt"

func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // pipeline: generator -> square
    for n := range square(generator(2, 3, 4)) {
        fmt.Println(n)
    }
}

答案:展示了Pipeline模式,数据流经多个处理阶段。

解析

  • 每个阶段:读取输入channel,处理,发送到输出channel
  • 阶段可串联形成管道
  • 天然支持并发,每个阶段可独立goroutine运行
  • 函数式编程风格,代码清晰易测试

第109题:优雅退出

问题:如何实现程序的优雅退出,确保资源清理?

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    
    done := make(chan bool, 1)
    
    go func() {
        sig := <-sigs
        fmt.Println("\nReceived signal:", sig)
        
        // 清理工作
        fmt.Println("Cleaning up...")
        time.Sleep(500 * time.Millisecond)
        
        done <- true
    }()
    
    fmt.Println("Running... Press Ctrl+C to exit")
    <-done
    fmt.Println("Exiting gracefully")
}

答案:捕获系统信号,执行清理后退出。

解析

  • signal.Notify注册感兴趣的信号
  • 在goroutine中等待信号
  • 执行资源清理(关闭连接、保存状态等)
  • 用于服务器、长时间运行程序

第110题:Or-Done模式

问题:以下代码实现了什么功能?

package main

import (
    "fmt"
    "time"
)

func or(channels ...<-chan interface{}) <-chan interface{} {
    switch len(channels) {
    case 0:
        return nil
    case 1:
        return channels[0]
    }
    
    orDone := make(chan interface{})
    go func() {
        defer close(orDone)
        
        switch len(channels) {
        case 2:
            select {
            case <-channels[0]:
            case <-channels[1]:
            }
        default:
            select {
            case <-channels[0]:
            case <-channels[1]:
            case <-channels[2]:
            case <-or(append(channels[3:], orDone)...):
            }
        }
    }()
    
    return orDone
}

func main() {
    sig := func(after time.Duration) <-chan interface{} {
        c := make(chan interface{})
        go func() {
            defer close(c)
            time.Sleep(after)
        }()
        return c
    }
    
    start := time.Now()
    <-or(sig(2*time.Second), sig(1*time.Second), sig(3*time.Second))
    fmt.Printf("done after %v\n", time.Since(start))
}

答案:输出done after ~1s,实现了Or-Channel模式。

解析

  • 多个channel,任意一个关闭则返回
  • 类似于Unix的select系统调用
  • 用于组合多个取消信号或超时

第111题:Tee模式

问题:以下代码实现了什么功能?

package main

import "fmt"

func tee(in <-chan int) (<-chan int, <-chan int) {
    out1 := make(chan int)
    out2 := make(chan int)
    
    go func() {
        defer close(out1)
        defer close(out2)
        
        for v := range in {
            out1, out2 := out1, out2  // 局部变量
            out1 <- v
            out2 <- v
        }
    }()
    
    return out1, out2
}

func main() {
    in := make(chan int)
    go func() {
        for i := 0; i < 3; i++ {
            in <- i
        }
        close(in)
    }()
    
    out1, out2 := tee(in)
    
    for v := range out1 {
        fmt.Println("out1:", v)
    }
    for v := range out2 {
        fmt.Println("out2:", v)
    }
}

答案:将一个channel的数据同时发送到两个channel(类似于Unix的tee命令)。

解析

  • 实现数据多路复制
  • 两个消费者独立接收相同数据
  • 注意:如果某个消费者不接收,会阻塞

第112题:Goroutine泄漏检测

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "time"
)

func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // 永远等待
        fmt.Println(val)
    }()
    // ch未关闭,goroutine永远不会退出
}

func main() {
    for i := 0; i < 1000; i++ {
        leak()
    }
    time.Sleep(time.Second)
    fmt.Println("done")
}

答案Goroutine泄漏,创建了1000个永远不会退出的goroutine。

解析

  • goroutine等待永远不会到来的channel数据
  • 应确保每个创建的goroutine都能退出
  • 使用context取消、关闭channel、超时等手段
  • 生产环境可用runtime.NumGoroutine()监控

第113题:竞态检测器

问题:如何检测数据竞争?

答案:使用go run -racego test -race

go run -race main.go
go test -race ./...

注意

  • Race detector有性能开销,不要用于生产
  • 只能在编译时使用,不是运行时保护
  • 可以检测到大部分数据竞争,但不是全部

示例代码

package main

import "fmt"

func main() {
    var counter int
    for i := 0; i < 1000; i++ {
        go func() {
            counter++  // 数据竞争
        }()
    }
    fmt.Println(counter)
}

运行go run -race main.go会报告:WARNING: DATA RACE


第114题:Ticker与Timer

问题:Ticker和Timer有什么区别?

答案

特性 Timer Ticker
次数 触发一次 周期性触发
停止 Stop()停止 Stop()停止,不回收需Drain
用途 延迟执行、超时 定时任务、心跳
内存 自动回收 需手动Stop,否则泄漏

示例

// Timer - 延迟执行
timer := time.NewTimer(2 * time.Second)
<-timer.C  // 阻塞2秒

// Ticker - 周期性
ticker := time.NewTicker(1 * time.Second)
for t := range ticker.C {
    fmt.Println("Tick at", t)
}
ticker.Stop()  // 必须停止,否则goroutine泄漏

第115题:并发安全的Map

问题:如何实现并发安全的Map?

答案:三种方式:

  1. sync.RWMutex + map
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}
  1. sync.Map(Go 1.9+):
var m sync.Map
m.Store("key", value)
value, ok := m.Load("key")
  1. 分片锁(减少锁竞争):
type ShardedMap struct {
    shards [32]struct {
        sync.RWMutex
        m map[string]interface{}
    }
}

sync.Map适用场景

  • 只追加不删除
  • 读多写少
  • 多个goroutine访问不相交的key集合

第116题:Worker Pool

问题:实现一个固定大小的worker pool。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Task func()

type Pool struct {
    tasks   chan Task
    wg      sync.WaitGroup
}

func NewPool(size int) *Pool {
    p := &Pool{
        tasks: make(chan Task),
    }
    
    p.wg.Add(size)
    for i := 0; i < size; i++ {
        go p.worker()
    }
    
    return p
}

func (p *Pool) worker() {
    defer p.wg.Done()
    for task := range p.tasks {
        task()
    }
}

func (p *Pool) Submit(task Task) {
    p.tasks <- task
}

func (p *Pool) Shutdown() {
    close(p.tasks)
    p.wg.Wait()
}

func main() {
    pool := NewPool(3)
    
    for i := 0; i < 10; i++ {
        n := i
        pool.Submit(func() {
            fmt.Printf("Task %d executed\n", n)
            time.Sleep(100 * time.Millisecond)
        })
    }
    
    pool.Shutdown()
}

答案:创建了3个worker的pool,处理10个任务。

解析

  • 限制并发数,防止资源耗尽
  • 任务队列解耦生产和消费
  • 优雅关闭:关闭channel,等待所有worker完成


六、内存与GC (15题)

第117题:逃逸分析

问题:什么是逃逸分析?以下变量会分配在堆还是栈?

package main

func foo() *int {
    x := 42
    return &x  // 会逃逸吗?
}

func bar() int {
    x := 42
    return x   // 会逃逸吗?
}

func main() {
    _ = foo()
    _ = bar()
}

答案

  • foo中的x逃逸到堆:返回了指针,函数结束后仍需访问
  • bar中的x分配在:只是返回值,可以复制

解析

  • 逃逸分析:编译器分析变量的作用域,决定分配在栈还是堆
  • 逃逸到堆的场景
    • 返回局部变量的指针
    • 闭包捕获局部变量
    • 发送到channel的指针
    • 接口类型的值(需要动态分发)
    • 大对象(>32KB)
  • 栈分配更快,函数结束自动回收
  • 查看逃逸分析:go build -gcflags="-m"

第118题:垃圾回收算法

问题:Go使用什么垃圾回收算法?有什么特点?

答案: Go使用**并发三色标记-清除(Concurrent Tri-color Mark-Sweep)**算法。

特点

  1. 并发:GC与程序并发运行,减少STW(Stop The World)
  2. 三色标记
    • 白色:未访问,可能回收
    • 灰色:已访问,引用未扫描
    • 黑色:已访问,引用已扫描
  3. 写屏障(Write Barrier):标记期间修改的指针处理
  4. 混合写屏障:Go 1.8+引入,进一步减少STW

STW时间

  • Go 1.5: 几十到几百毫秒
  • Go 1.8+: 通常<100微秒

第119题:GC触发时机

问题:GC什么时候触发?如何调优?

答案

触发时机

  1. 内存分配量达到阈值:默认当堆内存达到上次GC后的2倍
  2. 手动触发runtime.GC()
  3. 定时触发:2分钟未GC强制触发

调优参数

// 设置GC目标百分比(默认100,即2倍)
// 100表示堆内存增长到2倍时触发GC
debug.SetGCPercent(100)

// 设置内存限制(Go 1.19+)
debug.SetMemoryLimit(10 << 30)  // 10GB

最佳实践

  • 不要频繁调用runtime.GC()
  • 减少堆上对象分配(使用sync.Pool)
  • 避免不必要的指针(值类型替代指针)

第120题:内存分配器

问题:Go的内存分配器有什么特点?

答案: Go使用**TCMalloc(Thread-Caching Malloc)**启发式内存分配器。

核心概念

  1. mspan:内存管理基本单位,包含若干页
  2. mcache:每个P(Processor)的本地缓存,无锁分配
  3. mcentral:中心缓存,按size class组织
  4. mheap:全局堆,大对象直接分配

分配策略

  • 小对象(≤32KB):从mcache分配,无锁
  • 大对象(>32KB):直接从mheap分配

优势

  • 减少锁竞争(每个P独立cache)
  • 减少内存碎片(size class)

第121题:指针与性能

问题:指针一定比值快吗?什么时候用值类型更好?

答案

指针的优势

  • 避免大对象复制
  • 可以修改原值
  • 实现共享和引用语义

指针的劣势

  • 逃逸到堆,GC压力
  • 缓存不友好(数据分散)
  • 额外的内存访问(解引用)

值类型更好的场景

  • 小结构体(≤64字节)
  • 读多写少
  • 不需要修改原值
  • 对缓存友好要求高

示例

type Point struct { X, Y float64 }  // 16字节,用值类型

type BigStruct struct { data [1024]int }  // 大结构体,用指针

第122题:内存对齐

问题:以下结构体占用多少内存?如何优化?

package main

import (
    "fmt"
    "unsafe"
)

type BadStruct struct {
    A bool     // 1字节
    B int32    // 4字节
    C bool     // 1字节
    D int64    // 8字节
    E bool     // 1字节
}

type GoodStruct struct {
    D int64    // 8字节
    B int32    // 4字节
    A bool     // 1字节
    C bool     // 1字节
    E bool     // 1字节
}

func main() {
    fmt.Println("BadStruct:", unsafe.Sizeof(BadStruct{}))
    fmt.Println("GoodStruct:", unsafe.Sizeof(GoodStruct{}))
}

答案

BadStruct: 40
GoodStruct: 16

解析

  • Go按照字段大小对齐(int64对齐到8字节)
  • 字段顺序影响内存填充(padding)
  • 优化原则:大字段在前,小字段在后,减少填充

第123题:堆与栈的区别

问题:Go中堆和栈有什么区别?

特性
管理 编译器自动 GC管理
速度 极快(移动SP指针) 较慢(分配+GC)
大小 有限(初始2KB,可增长) 仅受限于系统内存
生命周期 函数返回即释放 GC决定
线程安全 协程私有 需同步机制
用途 局部变量 全局、逃逸变量

第124题:内存泄漏场景

问题:Go中常见的内存泄漏场景有哪些?

答案

  1. Goroutine泄漏
// 泄漏:等待永远不会到来的channel数据
ch := make(chan int)
go func() {
    <-ch  // 永远阻塞
}()
  1. Map持续增长
// 只添加不删除
m := make(map[string][]byte)
// m[key] = largeData
  1. 全局缓存无限制
var cache = make(map[string]interface{})
// 没有淘汰策略,无限增长
  1. time.After滥用
for {
    select {
    case <-time.After(time.Minute):  // 每次循环创建新Timer
    }
}
  1. 字符串拼接
// 大数据拷贝
s := ""
for _, part := range parts {
    s += part  // 每次都分配新内存
}
// 应使用strings.Builder

第125题:大对象分配

问题:Go如何处理大对象分配?

答案

  • 大对象:>32KB
  • 直接跳过mcache和mcentral
  • 从mheap分配,使用空闲列表或向OS申请
  • 可能触发GC

优化建议

  • 避免频繁分配大对象
  • 使用sync.Pool重用对象
  • 考虑使用make([]byte, 0, capacity)预分配

第126题:GC调优实践

问题:如何诊断和优化GC问题?

答案

诊断工具

# 查看GC统计
GODEBUG=gctrace=1 go run main.go

# 输出示例
gc 1 @0.015s 2%: 0.015+0.32+0.045 ms clock, 0.12+0.15/0.32/0.55+0.36 ms cpu, 4->4->0 MB, 5 MB goal, 8 P

优化策略

  1. 减少堆分配(使用go test -benchmem检测)
  2. 对象池复用(sync.Pool)
  3. 避免不必要的接口(接口导致值逃逸)
  4. 调整GOGC环境变量
  5. 设置内存限制(Go 1.19+)

Playground:略(需要环境变量)


第127题:Finalizer

问题:什么是Finalizer?有什么使用限制?

答案

runtime.SetFinalizer(obj, func(obj *Type) {
    // 对象被GC前执行
})

特点

  • 对象被GC前执行清理操作
  • 不保证执行时间(可能延迟)
  • 不保证一定执行(程序退出时不执行)

限制

  • 可能延长对象生命周期
  • 循环引用导致无法GC
  • 难以调试
  • 建议:显式Close/Stop,不要用Finalizer

第128题:内存屏障

问题:什么是写屏障?有什么作用?

答案

写屏障(Write Barrier)

  • GC标记阶段开启的特殊机制
  • 记录指针修改,确保三色标记正确

作用

  1. 防止漏标(丢失存活对象)
  2. 允许GC与mutator(程序)并发执行

类型

  • Dijkstra写屏障:保守,可能多标
  • Yuasa写屏障:精确
  • 混合写屏障:Go 1.8+使用,结合两者优点

第129题:runtime.ReadMemStats

问题:如何获取程序内存使用情况?

package main

import (
    "fmt"
    "runtime"
)

func main() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("Alloc = %v MB\n", m.Alloc/1024/1024)
    fmt.Printf("TotalAlloc = %v MB\n", m.TotalAlloc/1024/1024)
    fmt.Printf("Sys = %v MB\n", m.Sys/1024/1024)
    fmt.Printf("NumGC = %v\n", m.NumGC)
}

答案:输出内存分配、系统内存、GC次数等统计。

解析

  • Alloc:当前堆分配的对象大小
  • TotalAlloc:累计分配大小
  • Sys:从OS申请的内存
  • NumGC:GC执行次数
  • 可用于监控和调试内存问题

第130题:Go的栈管理

问题:Go的栈有什么特点?

答案

  1. 动态增长

    • 初始2KB
    • 需要时自动增长(分段栈,Go 1.3后改为连续栈)
  2. 协程私有

    • 每个goroutine独立栈
    • 栈切换简单高效
  3. 栈拷贝

    • 栈增长时,旧栈数据复制到新栈
    • 更新所有指向栈的指针
  4. 无溢出风险

    • 不像C有固定栈大小
    • 栈空间理论无限(受限于内存)

第131题:对象生命周期

问题:以下对象的生命周期如何?

package main

func create() []int {
    data := make([]int, 1000)  // 逃逸到堆
    return data
}

func process() {
    buf := make([]byte, 1024)  // 可能分配在栈
    _ = buf
}

func main() {
    _ = create()
    process()
}

答案

  • data:分配在堆,直到GC回收
  • buf:可能分配在栈,函数返回即释放

解析

  • 逃逸分析决定对象位置
  • go build -gcflags="-m -l"查看逃逸情况
  • 尽量让对象在栈上分配,减少GC压力

七、标准库 (15题)

第132题:strings.Builder

问题:strings.Builder相比直接拼接有什么优势?

package main

import (
    "fmt"
    "strings"
)

func main() {
    var b strings.Builder
    b.Grow(100)  // 预分配
    
    for i := 0; i < 10; i++ {
        b.WriteString("hello ")
    }
    
    fmt.Println(b.String())
}

答案:更高效,减少内存分配。

解析

  • 直接拼接+:每次创建新字符串,O(n²)
  • strings.Builder:内部使用切片,动态扩容
  • Grow预分配可避免多次扩容
  • bytes.Buffer更轻量(不实现Writer以外的接口)

第133题:bytes.Buffer

问题:bytes.Buffer和strings.Builder有什么区别?

特性 bytes.Buffer strings.Builder
目的 通用字节缓冲 高效字符串拼接
实现接口 io.Reader/Writer等 io.Writer/StringWriter
线程安全
Reset重用
使用场景 IO操作 字符串构建

建议:字符串拼接用strings.Builder,其他用bytes.Buffer


第134题:json序列化

问题:以下代码输出什么?

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age,omitempty"`
    Email string `json:"-"`  // 忽略
}

func main() {
    u := User{Name: "Tom", Age: 0}
    b, _ := json.Marshal(u)
    fmt.Println(string(b))
}

答案{"name":"Tom"}(Age为0被omitempty忽略)

解析

  • 结构体标签控制序列化行为
  • omitempty:零值时忽略
  • -:完全忽略该字段
  • 自定义序列化:实现json.Marshaler接口

第135题:json反序列化

问题:以下代码有什么问题?

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    data := `{"name":"Tom","age":30}`
    
    var m map[string]interface{}
    json.Unmarshal([]byte(data), &m)
    
    age := m["age"].(int)  // 能成功吗?
    fmt.Println(age)
}

答案:运行时panic:interface {} is float64, not int

解析

  • json.Unmarshal将数字转为float64(为了兼容JSON number)
  • 不能直接断言为int
  • 解决方案
    • 使用具体结构体:var user struct{ Age int }
    • 使用json.Decoder.UseNumber()
    • 类型断言为float64再转int

第136题:time包

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "time"
)

func main() {
    t1 := time.Now()
    time.Sleep(100 * time.Millisecond)
    t2 := time.Now()
    
    if t2.After(t1) {
        fmt.Println("t2 is later")
    }
    
    // 时间比较
    fmt.Println(t1 == t2)  // 能比较吗?
}

答案:可以比较,time.Time可比较。

解析

  • time.Time内部是结构体,包含wall time和monotonic reading
  • 可以用==BeforeAfterEqual比较
  • Equal考虑时区,比==更推荐
  • 建议使用time.Now().Sub(t1)计算耗时

第137题:文件操作

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("test.txt")
    if err != nil {
        panic(err)
    }
    // 忘记关闭
    
    buf := make([]byte, 1024)
    n, _ := f.Read(buf)
    fmt.Println(string(buf[:n]))
}

答案:文件句柄泄漏。

解析

  • 必须使用defer f.Close()关闭文件
  • 即使发生错误也要关闭
  • 标准做法:
f, err := os.Open("test.txt")
if err != nil {
    return err
}
defer f.Close()

第138题:filepath与path

问题filepathpath包有什么区别?

答案

用途 示例
path URL路径,正斜杠 path.Join("a", "b") → “a/b”
filepath 文件系统路径,跨平台 filepath.Join("a", "b") → “a\b”(Windows)

建议

  • 处理URL用path
  • 处理文件用filepath

第139题:正则表达式

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "regexp"
)

func main() {
    for i := 0; i < 1000; i++ {
        matched, _ := regexp.MatchString(`\d+`, "12345")
        fmt.Println(matched)
    }
}

答案:每次循环编译正则,性能差。

解析

  • regexp.MatchString每次编译正则表达式
  • 应预编译正则:
var digitRegexp = regexp.MustCompile(`\d+`)

func main() {
    for i := 0; i < 1000; i++ {
        matched := digitRegexp.MatchString("12345")
        fmt.Println(matched)
    }
}

第140题:sort排序

问题:以下代码如何实现自定义排序?

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }
    
    sort.Sort(ByAge(people))
    fmt.Println(people)
}

答案:实现sort.Interface接口(Len、Swap、Less)。

解析

  • 实现三个方法即可自定义排序
  • Go 1.8+可以用sort.Slice简化:
sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age
})

第141题:HTTP客户端

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    resp, err := http.Get("https://example.com")
    if err != nil {
        panic(err)
    }
    // defer resp.Body.Close()  // 忘记关闭?
    
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(len(body))
}

答案:必须关闭resp.Body,否则连接泄漏。

解析

  • 即使不读取也要关闭Body
  • 标准做法:defer resp.Body.Close()
  • 长连接会复用,关闭是归还连接池

第142题:HTTP服务端

问题:以下代码有什么问题?

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

答案:没有错误处理,无法优雅关闭。

解析

  • 应使用http.Server提供关闭能力:
srv := &http.Server{Addr: ":8080"}
http.HandleFunc("/", handler)

// 优雅关闭
go func() {
    if err := srv.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}()

// 等待信号
<-sig
srv.Shutdown(ctx)

第143题:context使用

问题:context的值传递应该传什么?

答案

适合传递

  • 请求ID(追踪)
  • 用户身份信息(认证后)
  • 截止时间/超时信息

不适合传递

  • 数据库连接
  • 业务参数
  • 大对象

最佳实践

// 在middleware中设置
ctx = context.WithValue(ctx, "request_id", generateID())

// 在handler中获取
requestID, ok := ctx.Value("request_id").(string)

第144题:strconv

问题:strconv和fmt.Sprint有什么区别?

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // 性能测试
    n := 12345
    
    // 方式1:fmt
    s1 := fmt.Sprintf("%d", n)
    
    // 方式2:strconv
    s2 := strconv.Itoa(n)
    
    fmt.Println(s1, s2)
}

答案strconv更快,专门用于字符串转换。

解析

函数 用途
strconv.Itoa int → string
strconv.Atoi string → int
strconv.FormatInt int64 → string(可指定进制)
strconv.ParseFloat string → float64

建议:转换用strconv,格式化用fmt


第145题:log包

问题:如何定制log输出格式?

package main

import (
    "log"
    "os"
)

func main() {
    // 设置前缀和格式
    logger := log.New(os.Stdout, "[APP] ", log.LstdFlags|log.Lshortfile)
    
    logger.Println("info message")
    logger.Printf("user %s logged in", "alice")
}

答案:使用log.New创建自定义logger。

解析

  • LstdFlags:标准时间格式
  • Lshortfile:文件名和行号
  • Lmicroseconds:微秒精度
  • 生产环境建议使用log/slog(Go 1.21+)或第三方库

第146题:flag解析

问题:如何解析命令行参数?

package main

import (
    "flag"
    "fmt"
)

func main() {
    var (
        host = flag.String("host", "localhost", "server host")
        port = flag.Int("port", 8080, "server port")
        debug = flag.Bool("debug", false, "enable debug")
    )
    
    flag.Parse()
    
    fmt.Printf("Host: %s, Port: %d, Debug: %t\n", *host, *port, *debug)
}

答案:使用flag包。

解析

  • flag.String返回指针,需要解引用
  • 调用flag.Parse()解析参数
  • 可以使用flag.Var自定义类型
  • Go 1.22+可用flag.Func处理复杂场景

八、工程实践 (15题)

第147题:错误处理

问题:Go如何处理错误?与异常有什么区别?

答案

Go使用显式错误返回,不用异常:

result, err := doSomething()
if err != nil {
    return err
}

与异常的区别

特性 Go错误 异常(try-catch)
显式性 必须处理 可忽略
流程控制 正常返回 跳转栈
性能 无开销 有开销
代码可读性 错误就在调用处 可能在远处捕获

panic/recover:仅用于不可恢复的错误。


第148题:错误包装

问题:Go 1.13+的错误包装有什么好处?

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findUser(id int) error {
    // 模拟查找失败
    return fmt.Errorf("find user %d: %w", id, ErrNotFound)
}

func main() {
    err := findUser(42)
    
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }
    
    fmt.Println(err)
}

答案:可以保留原始错误信息,用errors.Is判断错误类型。

解析

  • %w包装错误
  • errors.Is:检查错误链中是否包含某错误
  • errors.As:将错误转为特定类型
  • err.Error()字符串比较更可靠

第149题:单元测试

问题:如何编写和运行单元测试?

// math.go
package math

func Add(a, b int) int {
    return a + b
}

// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf("Add(1, 2) = %d, want 3", result)
    }
}

func TestAddTable(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    
    for _, tt := range tests {
        t.Run(fmt.Sprintf("%d+%d", tt.a, tt.b), func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

运行go test -v

Playground:略(需要多文件)


第150题:基准测试

问题:如何编写基准测试?

package math

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

func BenchmarkAddParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(1, 2)
        }
    })
}

运行

go test -bench=. -benchmem
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof

第151题:表驱动测试

问题:什么是表驱动测试?有什么好处?

答案

func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        want     int
        wantErr  bool
    }{
        {"normal", 10, 2, 5, false},
        {"negative", -10, 2, -5, false},
        {"zero_divisor", 10, 0, 0, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

好处

  • 易于添加新测试用例
  • 测试逻辑统一,减少重复
  • 输出清晰(t.Run子测试)

第152题:项目结构

问题:Go项目推荐的项目结构是什么?

答案(标准布局):

myproject/
├── cmd/                    # 可执行文件
│   ├── server/
│   │   └── main.go
│   └── cli/
│       └── main.go
├── internal/               # 私有代码
│   ├── service/
│   └── repository/
├── pkg/                    # 公共库(可选)
├── api/                    # API定义
├── configs/                # 配置文件
├── deployments/            # 部署脚本
├── scripts/                # 脚本
├── go.mod
└── README.md

说明

  • cmd:每个子目录一个可执行文件
  • internal:不允许外部导入
  • pkg:可以被外部导入的库

第153题:模块管理

问题:Go Modules如何解决依赖管理?常用命令有哪些?

答案

常用命令

go mod init myproject          # 初始化
go get github.com/pkg/name     # 添加依赖
go get -u ./...                # 更新所有依赖
go mod tidy                    # 清理未使用依赖
go mod vendor                  # 创建vendor目录
go mod graph                   # 查看依赖图

go.mod文件

module myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

require (
    github.com/davecgh/go-spew v1.1.1 // indirect
)

第154题:接口设计原则

问题:Go接口设计有什么原则?

答案

  1. 小接口原则:接口越小越好

    // 好
    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    
  2. 组合优于继承

    type ReadWriter interface {
        Reader
        Writer
    }
    
  3. 接口在消费者端定义

    // 不要预先定义,让使用者定义需要的接口
    type Stringer interface {
        String() string
    }
    
  4. 接受接口,返回具体类型


第155题:性能优化原则

问题:Go性能优化的一般步骤是什么?

答案

  1. 先写正确的代码:不要过早优化
  2. 用benchmark测量:找到瓶颈
  3. 用pprof分析
    import _ "net/http/pprof"
    
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
  4. 常见优化点
    • 减少堆分配(避免逃逸)
    • 使用sync.Pool
    • 预分配slice/map容量
    • 避免不必要的字符串转换
    • 使用strconv代替fmt

第156题:优雅关闭

问题:如何实现HTTP服务的优雅关闭?

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    srv := &http.Server{Addr: ":8080"}
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second)
        w.Write([]byte("Hello"))
    })
    
    // 启动服务
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()
    
    // 等待中断信号
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    
    // 优雅关闭
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal(err)
    }
    log.Println("Server stopped")
}

答案:使用http.Server.Shutdown,给正在处理的请求完成时间。


第157题:配置管理

问题:Go项目如何管理配置?

答案

  1. 环境变量

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
  2. 配置文件(推荐Viper):

    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.ReadInConfig()
    port := viper.GetInt("server.port")
    
  3. 命令行参数flag

  4. 12-Factor原则:配置存储在环境中


第158题:日志规范

问题:Go项目日志有什么最佳实践?

答案

  1. 结构化日志(log/slog):

    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger.Info("request processed",
        "method", r.Method,
        "path", r.URL.Path,
        "duration", time.Since(start),
    )
    
  2. 日志级别:Debug、Info、Warn、Error、Fatal

  3. 包含上下文:时间、请求ID、用户、堆栈等

  4. 不要

    • 打印敏感信息
    • 在循环中大量打印
    • 使用+拼接字符串(用slog的键值对)

第159题:数据库连接

问题:如何管理数据库连接池?

db, err := sql.Open("mysql", dsn)
if err != nil {
    log.Fatal(err)
}
defer db.Close()

// 连接池配置
db.SetMaxOpenConns(25)      // 最大连接数
db.SetMaxIdleConns(5)       // 最大空闲连接
db.SetConnMaxLifetime(5 * time.Minute)  // 连接最大生命周期

// 验证连接
if err := db.Ping(); err != nil {
    log.Fatal(err)
}

答案:合理配置连接池参数,避免连接耗尽或过多。


第160题:API设计

问题:Go REST API设计有什么建议?

答案

  1. 路由清晰

    GET    /users          # 列表
    GET    /users/:id      # 详情
    POST   /users          # 创建
    PUT    /users/:id      # 更新
    DELETE /users/:id      # 删除
    
  2. 统一响应格式

    {
      "code": 0,
      "message": "success",
      "data": {...}
    }
    
  3. 错误处理:HTTP状态码 + 错误信息

  4. 版本控制/v1/users

  5. 使用中间件:日志、认证、恢复


第161题:代码质量

问题:如何保证Go代码质量?

答案

工具

# 格式化
go fmt ./...

# 代码检查
go vet ./...
golint ./...
staticcheck ./...

# 测试
go test -race -cover ./...

# CI/CD集成

实践

  • Code Review
  • 单元测试覆盖率>80%
  • 使用gofmt统一格式
  • 静态分析发现潜在问题
  • 文档注释(godoc)

九、源码与高级主题 (20题)

第162题:Goroutine调度器

问题:Goroutine是如何调度的?

答案

Go使用GMP模型

  • G(Goroutine):轻量级协程,初始2KB栈
  • M(Machine):OS线程,执行G的载体
  • P(Processor):逻辑处理器,管理G队列

调度过程

  1. 新建G放入P的本地队列
  2. M绑定P,从P获取G执行
  3. G阻塞时(系统调用/channel),M与P分离
  4. P从全局队列或其他P偷取G(Work Stealing)

优势

  • 用户态调度,切换快(~200ns)
  • 少量M支撑大量G(M:N模型)
  • 利用多核并行

第163题:调度策略

问题:什么是Work Stealing和Handoff?

答案

Work Stealing(工作窃取):

  • 当P的本地队列为空时
  • 从全局队列或其他P的队列偷取G
  • 保持负载均衡

Handoff(交接):

  • G发生系统调用阻塞时
  • M释放P,P可以绑定其他M继续执行
  • 阻塞的M等待系统调用返回
  • 避免阻塞一个G就浪费一个线程

第164题:channel底层实现

问题:channel是如何实现的?

答案

type hchan struct {
    qcount   uint           // 队列中元素个数
    dataqsiz uint           // 环形队列大小
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 元素大小
    closed   uint32         // 是否关闭
    elemtype *_type         // 元素类型
    sendx    uint           // 发送索引
    recvx    uint           // 接收索引
    recvq    waitq          // 等待接收的goroutine队列
    sendq    waitq          // 等待发送的goroutine队列
    lock     mutex          // 互斥锁
}

关键

  • 有缓冲:环形队列存储数据
  • 无缓冲:直接交换,需要配对
  • 阻塞时goroutine进入waitq等待

第165题:select底层实现

问题:select是如何实现的?

答案

编译器转换

// 源代码
select {
case c1 <- v1:
case v2 = <-c2:
case v3, ok = <-c3:
default:
}

执行步骤

  1. 打乱case顺序(随机选择)
  2. 尝试所有case,有可执行的立即执行
  3. 无可执行的:
    • 有default:执行default
    • 无default:当前G加入所有channel的等待队列,阻塞
  4. 被唤醒后,从等待队列移除,继续执行

第166题:map底层实现

问题:map是如何实现的?扩容机制是什么?

答案

结构

type hmap struct {
    count     int
    flags     uint8
    B         uint8      // 桶数 = 2^B
    noverflow uint16
    hash0     uint32     // 哈希种子
    buckets    unsafe.Pointer  // 桶数组
    oldbuckets unsafe.Pointer  // 扩容时的旧桶
    nevacuate  uintptr     // 扩容进度
}

实现

  • 哈希表 + 拉链法
  • 每个桶存储8个key-value对
  • 超过8个用overflow桶

扩容

  • 翻倍扩容:负载因子>6.5时
  • 等量扩容:overflow桶太多时(整理碎片)
  • 渐进式扩容,每次操作迁移部分数据

第167题:interface底层

问题:interface的底层结构是什么?

答案

非空接口

type iface struct {
    tab  *itab          // 类型+方法表
    data unsafe.Pointer // 数据指针
}

空接口

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

itab

  • 接口类型
  • 动态类型
  • 方法表(接口方法到具体类型方法的映射)

第168题:reflect性能

问题:反射为什么慢?如何优化?

答案

慢的原因

  1. 绕过编译器优化
  2. 类型检查(interface{}转具体类型)
  3. 内存分配(装箱/拆箱)
  4. 间接调用(通过函数指针)

优化方法

  1. 减少反射使用(用codegen)
  2. 缓存Type/Value(避免重复反射)
  3. 使用map[string]any替代struct反射
  4. 使用unsafe(谨慎)

第169题:unsafe包

问题:unsafe包有什么使用场景?有什么风险?

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := "hello"
    
    // 获取string底层指针
    ptr := (*[5]byte)(unsafe.Pointer(
        (*[2]uintptr)(unsafe.Pointer(&s))[0]))
    
    fmt.Printf("%c\n", ptr[0])
}

答案:用于底层内存操作,如性能优化、与C交互。

风险

  • 破坏类型安全
  • 可能panic
  • 不保证跨版本兼容
  • 可能被GC误回收

使用原则

  • 只在必要时使用
  • 充分测试
  • 添加详细注释

第170题:cgo使用

问题:如何在Go中调用C代码?

package main

/*
#include <stdio.h>

void hello() {
    printf("Hello from C!\n");
}
*/
import "C"
import "fmt"

func main() {
    C.hello()
    fmt.Println("Hello from Go!")
}

答案:使用import "C"和注释中的C代码。

注意

  • 有C调用开销
  • 破坏跨平台编译
  • CGO_ENABLED=0禁用
  • 复杂项目用swig

第171题:内存模型

问题:Go的内存模型是什么?happens-before关系有哪些?

答案

Happens-Before

  • 如果A happens-before B,则A的修改对B可见

保证

  1. ** Goroutine创建**:go语句happens-before goroutine执行
  2. Channel
    • 发送happens-before接收完成
    • 关闭happens-before接收零值
  3. Mutex:Unlock happens-before 后续Lock
  4. Once:Do返回happens-before 后续Do返回
  5. WaitGroup:Wait返回happens-before 所有Done

建议:不要依赖复杂同步,用channel和sync包。


第172题:逃逸分析原理

问题:编译器如何判断变量是否逃逸?

答案

逃逸场景

  1. 返回局部变量指针
  2. 发送到channel的指针
  3. 闭包捕获局部变量
  4. 接口类型的值(需要动态分发)
  5. 大对象(>32KB)

分析过程

  • 指针分析:追踪指针的传递
  • 如果指针可能超出函数生命周期 → 逃逸到堆

查看go build -gcflags="-m"


第173题:编译过程

问题:Go程序是如何编译的?

答案

编译步骤

  1. 词法分析:源代码 → Token
  2. 语法分析:Token → AST(抽象语法树)
  3. 类型检查:AST语义分析
  4. 中间代码:AST → SSA(静态单赋值)
  5. 优化:死代码消除、内联、逃逸分析
  6. 机器码:SSA → 目标架构机器码
  7. 链接:合并目标文件

工具链go tool compilego tool link


第174题:内联优化

问题:什么是函数内联?有什么优缺点?

答案

内联:编译器将函数调用替换为函数体,消除调用开销。

Go内联规则

  • 小函数(行数少、复杂度低)自动内联
  • //go:noinline注释不内联
  • 递归函数不内联
  • 有defer/runtime包的函数不内联(⚠️ Go 1.14+已改变,见下方

关于defer的内联(Go 1.14+)

  • Go 1.14引入"开放编码的defer(open-coded defers)”
  • Go 1.14+带有defer的函数也可以被内联(满足其他条件时)
  • open-coded defer显著降低了defer的开销
  • 但defer在循环中仍可能分配内存

优点

  • 消除调用开销
  • 允许更多优化

缺点

  • 代码膨胀
  • 编译时间增加

查看内联go build -gcflags="-m"


第175题:编译标签

问题:如何实现条件编译?

答案

文件标签

// +build linux

package main

文件名后缀

  • xxx_linux.go:Linux专用
  • xxx_windows.go:Windows专用
  • xxx_amd64.go:64位专用

使用

// 编译指定标签
go build -tags debug

第176题:init执行顺序

问题:多个包有init函数时,执行顺序是什么?

答案

顺序

  1. 先执行导入包的init(递归,深度优先)
  2. 按文件名字母顺序执行同包init
  3. 最后执行当前包init
  4. 然后执行main函数

依赖包:先执行被依赖包的init

示例

main → A → B
     → C

执行顺序:B.init → A.init → C.init → main.init → main


第177题:plugin包

问题:如何实现Go代码的动态加载?

// 编译为插件
go build -buildmode=plugin -o plugin.so plugin.go

// 加载插件
p, err := plugin.Open("plugin.so")
sym, err := p.Lookup("VarName")

答案:使用plugin包。

限制

  • 仅Linux、macOS
  • 不支持Windows
  • 复杂且不稳定
  • 生产环境建议用RPC/HTTP

第178题:go:generate

问题:如何自动化代码生成?

答案

//go:generate protoc --go_out=. *.proto
//go:generate go run github.com/golang/mock/mockgen -source=iface.go -destination=mock.go

package main

运行

go generate ./...

用途

  • Protocol Buffers代码生成
  • Mock生成
  • 字符串方法生成
  • 配置解析生成

第179题:pprof使用

问题:如何用pprof诊断性能问题?

答案

import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ...
}

分析

# CPU分析
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# 内存分析
go tool pprof http://localhost:6060/debug/pprof/heap

# 火焰图
go tool pprof -http=:8080 profile.pb.gz

类型

  • profile:CPU
  • heap:内存
  • goroutine:goroutine堆栈
  • mutex:锁竞争
  • block:阻塞

第180题:trace工具

问题:如何分析程序执行时间线?

答案

# 生成trace
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out

# 分析
go tool trace trace.out

可以看到

  • goroutine调度
  • GC事件
  • 阻塞事件
  • Syscall
  • 每个goroutine的时间线

第181题:运行时调试

问题:有哪些运行时调优参数?

答案

环境变量

# GC目标百分比(默认100,即2倍增长触发)
GOGC=100

# 最大处理器数
GOMAXPROCS=8

# 调试输出
GODEBUG=gctrace=1    # GC追踪
GODEBUG=schedtrace=1000  # 调度追踪

runtime函数

runtime.GOMAXPROCS(n)      // 设置P数量
runtime.SetGCPercent(100)  // 设置GC百分比
runtime.ReadMemStats(&m)   // 读取内存统计
runtime.NumGoroutine()     // goroutine数量
runtime.NumCPU()           // CPU核心数

十、Go版本新特性 (10题)

第182题:Go 1.22 for循环整数范围

问题:Go 1.22引入了哪些for循环新特性?

package main

import "fmt"

func main() {
    // Go 1.22+ 支持直接遍历整数范围
    for i := range 5 {
        fmt.Println(i)  // 输出 0 1 2 3 4
    }
    
    // 也可以指定起始和结束
    for i := range 2, 5 {
        fmt.Println(i)  // 输出 2 3 4
    }
}

答案

  • Go 1.22+支持for i := range N语法,等效于for i := 0; i < N; i++
  • 支持for i := range start, end,遍历[start, end)区间

注意

  • 这是Go 1.22的实验性特性,Go 1.23+正式支持
  • 需要GOEXPERIMENT=rangefunc环境变量(Go 1.22)

第183题:Go 1.21 log/slog结构化日志

问题:Go 1.21引入的slog包有什么优势?

package main

import (
    "log/slog"
    "os"
)

func main() {
    // 创建JSON格式logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 结构化日志
    logger.Info("request processed",
        "method", "GET",
        "path", "/api/users",
        "duration_ms", 42,
    )
}

答案

优势

  1. 结构化:支持key-value格式,便于机器解析
  2. 多格式:支持JSON和文本格式
  3. 高性能:比fmt.Sprintf更快
  4. 标准化:统一标准库日志接口

输出

{"time":"2024-01-01T12:00:00Z","level":"INFO","msg":"request processed","method":"GET","path":"/api/users","duration_ms":42}

第184题:Go 1.21 slices、maps、cmp包

问题:Go 1.21引入了哪些新的标准库包?

答案

slices包

import "slices"

s := []int{3, 1, 4, 1, 5}
slices.Sort(s)           // 排序
slices.Contains(s, 4)    // 是否包含
slices.Equal(a, b)       // 比较相等
slices.BinarySearch(s, 4) // 二分查找

maps包

import "maps"

m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 2, "a": 1}
maps.Equal(m1, m2)  // true(比较内容,不关心顺序)

m3 := maps.Clone(m1)  // 深拷贝

cmp包

import "cmp"

cmp.Compare(1, 2)    // 返回 -1(小于)、0(等于)、1(大于)
cmp.Less(1, 2)       // true
cmp.Or(a, b, c)      // 返回第一个非零值

第185题:Go 1.22 math/rand/v2

问题:Go 1.22的math/rand/v2有什么改进?

答案

import "math/rand/v2"

// 新的全局函数,更简洁
n := rand.IntN(100)  // [0, 100)

// 新的随机源接口
type Source interface {
    Uint64() uint64
}

// 性能更好,算法改进(PCG算法)

改进点

  1. 性能提升:PCG算法比之前的LCG更快
  2. API简化IntN(n)代替Intn(n)(注意大小写)
  3. 更清晰rand/v2明确表示v2版本

第186题:Go 1.23 iter包与range-over-func

问题:Go 1.23的iter包和range over func是什么?

package main

import (
    "fmt"
    "iter"
)

// 定义一个迭代器函数
func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    // 使用range遍历自定义迭代器
    for i := range Count(5) {
        fmt.Println(i)
    }
}

答案

iter.Seq[T]:表示一个序列的迭代器函数类型

range over func

  • Go 1.23支持for v := range func(yield func(T)bool)语法
  • 允许自定义类型支持range遍历
  • 标准库的slices.Allmaps.All等返回iter.Seq

使用场景

  • 自定义集合类型
  • 延迟生成数据
  • 链式操作

第187题:Go 1.23 unique包

问题:Go 1.23的unique包有什么用途?

答案

import "unique"

// 创建可比较值的唯一句柄
s1 := unique.Make("hello")
s2 := unique.Make("hello")
s3 := unique.Make("world")

// 相同值的句柄相等
fmt.Println(s1 == s2)  // true
fmt.Println(s1 == s3)  // false

用途

  1. 字符串驻留(interning):相同字符串只存储一份
  2. 节省内存:大量重复字符串的场景
  3. 快速比较:句柄比较比字符串比较更快

适用场景

  • 大量重复字符串(如日志标签、HTTP头名)
  • 需要去重的集合实现

第188题:Go 1.24 new关键字冲突

问题:Go 1.24有哪些值得注意的变化?

答案

1. 泛型类型别名

type MySlice[T any] = []T  // Go 1.24+支持

2. go.mod工具依赖

// 在go.mod中管理工具依赖
require (
    golang.org/x/tools/cmd/goimports v0.21.0
)

3. 字符串优化

  • 字符串拼接性能改进
  • strings.Builder进一步优化

4. 新的约束

// 清晰的类型约束语法
func Min[T ~int | ~float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

第189题:Go 1.22+循环变量语义变化总结

问题:Go 1.22循环变量语义变化有哪些影响?

答案

变化

  1. for i := 0; i < n; i++:每次迭代创建新的i变量
  2. for k, v := range …:每次迭代创建新的kv变量
  3. for i := range N:新增支持整数范围遍历

影响

// 旧代码(Go 1.21及之前可能有问题)
for _, v := range items {
    go func() {
        use(v)  // 可能全是最后一个值
    }()
}

// 新代码(Go 1.22+)没问题,但为了兼容旧版本建议:
for _, v := range items {
    go func(v Item) {
        use(v)
    }(v)
}

建议

  • 新项目直接使用Go 1.22+
  • 需要兼容旧版本的库代码,显式传递参数

第190题:Go 1.23+ timers/tickers停止

问题:Go 1.23对timer/ticker有什么改进?

答案

timer/ticker现在可以正确停止并回收资源

// Go 1.23之前:Stop()不保证回收
t := time.NewTimer(5 * time.Minute)
if !t.Stop() {
    <-t.C  // 需要Drain
}

// Go 1.23+:Stop()自动回收,无需Drain
t := time.NewTimer(5 * time.Minute)
t.Stop()  // 直接停止,无内存泄漏风险

改进

  • Stop()后无需再Drain channel
  • 减少goroutine泄漏风险
  • 简化代码

第191题:Go版本迁移建议

问题:从旧版本Go迁移到新版本有哪些注意事项?

答案

Go 1.18 → 1.21

  • 使用新的slog替代log
  • 使用slices、maps、cmp包
  • 泛型代码更稳定

Go 1.21 → 1.22(重点):

  • 检查循环变量相关的闭包代码
  • 旧代码可能依赖变量复用行为
  • 使用GOEXPERIMENT测试新特性

Go 1.22 → 1.23

  • 可以使用iter包简化代码
  • timer/ticker使用更简洁
  • 使用unique包优化字符串内存

通用建议

  1. 升级前运行完整测试
  2. 使用go mod tidy清理依赖
  3. 关注go fix自动修复
  4. 逐步升级,不要跳大版本

📝 总结

本面试题库包含190道题,涵盖:

章节 题数 核心内容
基础语法 20 变量、控制流、defer、panic/recover
数据类型 25 数组、切片、map、struct、内存布局
函数与方法 20 闭包、方法集、接口实现
接口与反射 20 隐式实现、类型断言、反射原理
并发编程 30 Goroutine、Channel、Sync、Context
内存与GC 15 逃逸分析、垃圾回收、内存分配
标准库 15 strings、bytes、json、time、http
工程实践 15 错误处理、测试、项目结构
源码与高级主题 20 调度器、map/channel底层、优化
Go版本新特性 10 Go 1.21+、1.22+、1.23+新功能

⚠️ 版本差异题标记

  • 第8题、第48题、第52题:Go 1.22+循环变量语义变化

学习建议

  1. 从基础开始,循序渐进
  2. 每道题在Go Playground运行验证
  3. 理解原理,不要死记硬背
  4. 结合实际项目加深理解
  5. 关注Go版本更新,了解新特性

题库整理完成,共计190题,覆盖Go语言面试的全面考察点。

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo