Skip to content

第11章 面试题精选 —— 标准库


★ 基础题

Q1:Go 的 io.Reader 和 io.Writer 设计为什么这么牛?

两个最小接口,把所有数据流抽象掉了

go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
  • 文件、网络、内存 buffer、HTTP body……都实现了它们
  • io.Copy(dst Writer, src Reader) 一行代码搞定任何拷贝
  • 设计哲学:接口越小,越能被复用

举例:把 HTTP 响应直接保存到文件:

go
io.Copy(file, resp.Body)

Q2:encoding/json 怎么忽略字段?怎么处理空值?

go
type User struct {
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`   // 空时不输出
    Password string `json:"-"`                 // 永不输出
    Age      int    `json:"age,string"`        // 编码成 "30" 而不是 30
}

注意omitempty0""nilfalse、空 slice/map 都视为空。


Q3:怎么写一个最简单的 HTTP 服务器?

go
package main

import "net/http"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello"))
    })
    http.ListenAndServe(":8080", nil)
}

5 行代码,没有任何第三方框架。


Q4:Go 的时间格式化为什么用 2006-01-02 15:04:05

:这是 Go 设计的"魔法时间",记忆口诀:

01-02 03:04:05 PM '06 -0700
1  2  3  4  5  PM 6   7

按数字 1 2 3 4 5 6 7 排列,对应:月-日 时:分:秒 PM 年 时区。

比起其他语言的 %Y-%m-%d 风格,Go 的方案更"无歧义"——不会把月当成日。


Q5:os.Exit、log.Fatal、panic 有什么区别?

触发 defer?打日志?退出码
os.Exit(1)1
log.Fatal(err)❌(内部调 os.Exit)1
panic("err")✅(堆栈)2

生产代码:通常 main 函数里只用 log.Fatal,业务里用 errorpanic(仅限"不可恢复"场景)。


★★ 进阶题

Q6:bufio.Scanner 和 bufio.Reader 怎么选?

  • Scanner:按行/词读取,每行不能超过 64KB(默认)
  • Reader:通用缓冲读,可读任意长度

读大文件按行:

go
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 10*1024*1024)  // 提升上限到 10MB
for sc.Scan() {
    line := sc.Text()
}

Q7:HTTP 客户端怎么设置超时?

至少 3 处可设

go
client := &http.Client{
    Timeout: 10 * time.Second,    // 1. 整体超时(推荐)
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout: 3 * time.Second,    // 2. 建连超时
        }).DialContext,
        TLSHandshakeTimeout: 3 * time.Second, // 3. TLS 握手
        ResponseHeaderTimeout: 5 * time.Second,
    },
}

或用 context(更细粒度):

go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, ...)

Q8:Response.Body 必须 Close 吗?为什么?

必须

go
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()    // 一定要!

否则:

  • 底层 TCP 连接无法复用,导致连接泄漏
  • 对端服务可能无法及时回收资源
  • 长期运行会耗尽 fd

即使你不读 Body 也要 Close。建议 io.Copy(io.Discard, resp.Body) 把 body 读完,TCP 才能 keep-alive 复用。


Q9:context.Context 的常用四件套是?

go
context.Background()                       // 根 context(main、初始化用)
context.TODO()                             // 不知道用什么时(占位)
context.WithCancel(parent)                 // 主动取消
context.WithTimeout(parent, 5*time.Second) // 超时取消
context.WithDeadline(parent, t)            // 到点取消
context.WithValue(parent, key, val)        // 携带请求级数据

使用规范

  • 第一个参数永远叫 ctx
  • 不要把 ctx 存到 struct(除非是 server)
  • cancel() 一定要 defer

Q10:log/slog 比 log 强在哪?

  1. 结构化输出:每个字段是 key=value,方便日志收集
  2. 支持级别:Debug、Info、Warn、Error
  3. 可切 JSON Handler,对接 ELK / Loki 直接食用
  4. 支持 LogGroup / WithAttrs:批量加上下文(如 trace_id)
go
slog.Info("user login", "user", "alice", "ip", "1.2.3.4")
// time=... level=INFO msg="user login" user=alice ip=1.2.3.4

Go 1.21+ 新增。生产推荐 slog 或 zap、zerolog。


★★★ 深度题

Q11:怎么实现一个反向代理?

net/http/httputil 自带:

go
import "net/http/httputil"

target, _ := url.Parse("http://backend:8080")
proxy := httputil.NewSingleHostReverseProxy(target)
http.ListenAndServe(":80", proxy)

几十行就能写一个简易 Nginx


Q12:encoding/json 的性能瓶颈在哪?怎么优化?

瓶颈:反射。每次 Marshal/Unmarshal 都要遍历 struct 字段类型。

优化方案:

  1. 复用 bufferjson.NewEncoder(w)json.Marshal(...)+Write 节省一次 alloc
  2. 预编译生成代码easyjsonffjson 直接生成静态序列化代码
  3. 第三方高性能库
    • json-iterator/go:API 兼容、~5x 性能
    • bytedance/sonic:JIT、~10x(仅 amd64)
    • goccy/go-json:纯 Go、~3x

Q13:HTTP/2 在 Go 里怎么开?

默认 TLS 服务自动开 HTTP/2

go
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", handler)
// HTTP/1.1 + HTTP/2 (h2)

明文 HTTP/2 需要 golang.org/x/net/http2/h2c

客户端可以用 http2.Transport{} 显式配置。


Q14:如何"优雅关闭" HTTP 服务?

go
srv := &http.Server{Addr: ":8080", Handler: h}
go srv.ListenAndServe()

// 监听信号
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig

// 给 30 秒处理已有请求
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)   // 不再接受新连接,等已有请求完成

关键:用 srv.Shutdown(ctx) 而不是 srv.Close()


Q15:embed 是什么?怎么用?

Go 1.16 引入,编译期把静态资源打进二进制

go
import "embed"

//go:embed assets/*
var assets embed.FS

func main() {
    http.Handle("/static/", http.FileServer(http.FS(assets)))
    http.ListenAndServe(":80", nil)
}

构建出的二进制文件自带 HTML/CSS/图片,单文件部署超方便。


Q16:testing 包怎么写性能基准测试?

go
func BenchmarkAppend(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, 1000)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}
bash
go test -bench=. -benchmem

输出:

BenchmarkAppend-8    500000    2400 ns/op    8192 B/op    1 allocs/op

📌 速记

io 抽象一切;json 看 tag;http 自带框架;time 用魔法时间;context 传超时;slog 写结构化日志。


🎯 自测练习

  1. 写一个工具:把 stdin 的内容同时写到 stdout 和 out.txt(用 io.MultiWriter)
  2. 把一个有嵌套 struct 的对象序列化为带缩进的 JSON
  3. 写一个 HTTP 服务,支持 GET /users 返回所有用户、POST /users 添加
  4. 写个 5 秒超时的 HTTP 客户端,超过则取消
  5. 用 slog + JSON handler 写一行带 request_id 的日志

进入第12章 →