第一章:Golang面试压轴题实战拆解(含竞态检测、pprof性能调优、trace火焰图解读)
Go 面试中常出现的“高并发计数器”压轴题,表面考察 sync/atomic,实则暗藏竞态、内存逃逸与 CPU 瓶颈三重陷阱。以下以典型实现切入,逐层暴露并修复问题:
竞态检测:用 -race 揭露隐藏缺陷
var counter int64
func increment() {
counter++ // 非原子操作,多 goroutine 下必触发 data race
}
// 启动 100 个 goroutine 调用 increment 后,执行:
// go run -race main.go
// 输出明确指出读写冲突位置及调用栈
pprof 性能调优:定位 CPU 与内存热点
启动 HTTP pprof 接口后,通过以下命令采集分析:
# 采集 30 秒 CPU profile
curl -o cpu.pprof "http://localhost:6060/debug/pprof/profile?seconds=30"
# 生成火焰图(需安装 go-torch 或 pprof 工具)
go tool pprof -http=:8080 cpu.pprof
# 查看内存分配热点
curl -o heap.pprof "http://localhost:6060/debug/pprof/heap"
trace 火焰图解读:识别调度延迟与阻塞
import "runtime/trace"
func main() {
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ... 启动业务 goroutine
}
执行后打开 trace.out:重点关注三类信号——
- 灰色矩形:goroutine 处于 runnable 状态但未被调度(反映 GOMAXPROCS 不足或调度器压力)
- 红色竖线:系统调用阻塞(如文件读写、网络等待)
- 绿色长条:GC STW 阶段(若频繁出现,需检查对象分配速率)
常见优化组合策略
| 问题类型 | 检测工具 | 典型修复方式 |
|---|---|---|
| 数据竞争 | go run -race |
替换为 atomic.AddInt64(&counter, 1) |
| CPU 密集瓶颈 | pprof --top |
减少重复计算、引入缓存、避免反射 |
| GC 频繁触发 | go tool pprof heap |
复用对象池(sync.Pool)、减少小对象分配 |
| Goroutine 泄漏 | pprof goroutine |
使用 runtime.NumGoroutine() 监控 + context 控制生命周期 |
第二章:Go并发安全与竞态检测深度剖析
2.1 Go内存模型与Happens-Before原则的工程化理解
Go内存模型不依赖硬件屏障,而是通过明确的happens-before关系定义goroutine间读写操作的可见性顺序。核心在于:若事件A happens-before 事件B,则所有对共享变量的修改在A中完成,B必能观察到该结果。
数据同步机制
以下代码演示无同步时的典型竞态:
var x, done int
func setup() {
x = 42 // A: 写x
done = 1 // B: 写done
}
func main() {
go setup()
for done == 0 { } // C: 读done(可能因重排序永远阻塞)
print(x) // D: 读x(可能读到0!)
}
逻辑分析:
done非原子变量,编译器/处理器可能重排序A与B;C与D之间无happens-before约束,故D无法保证看到A的写入。参数x和done均为全局int,无内存屏障或同步原语保障顺序。
Happens-Before关键规则(摘录)
| 规则类型 | 示例 |
|---|---|
| goroutine创建 | go f()前的写,对f中读可见 |
| channel通信 | 发送完成 → 接收开始 |
| sync.Mutex | Unlock → 后续Lock |
graph TD
A[goroutine G1: x=42] -->|happens-before| B[chan send]
B --> C[chan receive]
C -->|happens-before| D[goroutine G2: print x]
2.2 data race原理与go run -race实战定位技巧
数据竞争的本质
当多个 goroutine 同时访问同一内存地址,且至少一个为写操作,又无同步机制(如 mutex、channel)保护时,即触发 data race —— 行为未定义,结果不可预测。
go run -race 工作机制
Go 的竞态检测器在编译时注入读写钩子,运行时记录访问栈与时间戳,通过happens-before图动态判定冲突。
package main
import (
"sync"
"time"
)
var x int
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); x = 1 }() // 写
go func() { defer wg.Done(); println(x) }() // 读
wg.Wait()
}
此代码无同步,
x被并发读写。go run -race main.go将精准报告冲突位置、goroutine 栈及数据地址。-race会增加约2–3倍运行开销和内存占用,仅用于开发/测试阶段。
竞态检测关键参数
| 参数 | 说明 |
|---|---|
-race |
启用竞态检测器(必须显式指定) |
GODEBUG="schedtrace=1000" |
辅助观察调度行为(非必需但可联动分析) |
graph TD
A[源码编译] --> B[插入原子访问钩子]
B --> C[运行时记录访问事件]
C --> D[构建 happens-before 关系图]
D --> E[检测违反偏序的并发读写]
E --> F[输出带栈追踪的冲突报告]
2.3 sync.Mutex与sync.RWMutex在高并发场景下的选型实践
数据同步机制
Go 中 sync.Mutex 提供独占锁语义,适用于读写混合且写操作频繁的场景;而 sync.RWMutex 区分读锁(允许多个并发)与写锁(排他),更适合读多写少的典型负载。
性能对比关键维度
| 场景 | Mutex 吞吐量 | RWMutex 读吞吐 | RWMutex 写开销 |
|---|---|---|---|
| 95% 读 + 5% 写 | 低 | 高 | 略高于 Mutex |
| 50% 读 + 50% 写 | 中等 | 显著下降 | 高(升级冲突) |
典型误用示例
var rwmu sync.RWMutex
var data map[string]int
// ❌ 错误:未加读锁直接访问共享 map
func GetValue(key string) int {
return data[key] // 竞态风险!
}
// ✅ 正确:读操作必须显式加 RLock
func GetValueSafe(key string) int {
rwmu.RLock()
defer rwmu.RUnlock()
return data[key]
}
逻辑分析:
RWMutex.RLock()不阻塞其他读操作,但若在无锁状态下读取data,会触发go run -race检测到数据竞态。defer rwmu.RUnlock()确保锁及时释放,避免死锁。
选型决策流程
graph TD
A[请求类型分布?] -->|读 ≥ 80%| B[RWMutex]
A -->|写频繁或比例均衡| C[Mutex]
B --> D[是否存在写升级需求?]
D -->|是| E[警惕 Lock/Unlock 与 RLock/RUnlock 混用导致死锁]
D -->|否| F[安全启用 RWMutex]
2.4 atomic包原子操作的边界条件与性能对比实验
数据同步机制
Go 的 sync/atomic 提供无锁原子操作,但仅对基础类型(int32/int64/uintptr/unsafe.Pointer)及特定指针类型有效。超出范围的操作(如结构体字段原子更新)将触发 panic 或未定义行为。
边界条件示例
var counter int32
// ✅ 合法:对齐且类型匹配
atomic.AddInt32(&counter, 1)
// ❌ 非法:未对齐地址(在32位系统上取&struct{int8; int32}中int32字段地址)
// var s struct{ a int8; b int32 }; atomic.AddInt32(&s.b, 1) // 可能 panic
该代码依赖内存对齐保证——int32 必须位于 4 字节对齐地址,否则 runtime 会检测并 panic。
性能对比(100万次 increment)
| 方法 | 平均耗时 (ns/op) | 是否线程安全 |
|---|---|---|
atomic.AddInt32 |
2.1 | ✅ |
mutex.Lock() |
15.8 | ✅ |
普通 ++ |
0.3 | ❌ |
执行路径差异
graph TD
A[atomic.AddInt32] --> B[CPU CAS 指令]
C[Mutex.Lock] --> D[OS 线程调度+队列等待]
B --> E[单指令完成,无上下文切换]
D --> F[可能阻塞,引入调度开销]
2.5 Channel与Mutex协同设计:避免死锁与资源争用的真实案例复盘
数据同步机制
某高并发订单服务中,多个goroutine需更新共享库存计数器并通知下游。初版仅用sync.Mutex保护写操作,但通知逻辑(如发送消息到channel)在锁内执行,导致协程阻塞、channel满时死锁。
死锁复现路径
var mu sync.Mutex
var notifyCh = make(chan string, 1)
func updateStock() {
mu.Lock()
defer mu.Unlock()
// ... 更新库存逻辑
notifyCh <- "stock_updated" // 若channel已满,此处永久阻塞,锁无法释放
}
逻辑分析:
notifyCh容量为1,当消费者延迟消费时,后续updateStock()调用将卡在<-操作,mu持续被占用,其他goroutine无法获取锁,形成典型“锁内阻塞”死锁。
协同优化方案
- ✅ 将channel发送移出临界区,仅用mutex保护共享状态变更
- ✅ 使用带超时的非阻塞发送:
select { case notifyCh <- msg: default: } - ✅ 引入缓冲channel + worker goroutine异步消费
| 方案 | 是否规避死锁 | 是否保证状态一致性 |
|---|---|---|
| 锁内发送 | ❌ | ✅ |
| 锁外发送+超时 | ✅ | ✅ |
| Worker模型 | ✅ | ✅(需额外确认机制) |
graph TD
A[goroutine] --> B[Lock]
B --> C[Update stock]
C --> D[Unlock]
D --> E[Send to channel]
E --> F[Worker consume]
第三章:pprof性能分析体系构建与调优实战
3.1 CPU、Heap、Goroutine、Block、Mutex profile采集全流程演示
Go 程序性能分析依赖 net/http/pprof 提供的标准化接口,需在服务中显式启用:
import _ "net/http/pprof"
// 启动 pprof HTTP 服务(通常在独立端口)
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
启动后,
/debug/pprof/路径下自动暴露各 profile 接口。-http=localhost:6060是go tool pprof的默认连接目标。
常用采集方式:
go tool pprof http://localhost:6060/debug/pprof/profile(CPU,默认 30s)go tool pprof http://localhost:6060/debug/pprof/heapgo tool pprof http://localhost:6060/debug/pprof/goroutinego tool pprof http://localhost:6060/debug/pprof/blockgo tool pprof http://localhost:6060/debug/pprof/mutex
| Profile | 采样触发条件 | 典型用途 |
|---|---|---|
| CPU | 定时中断(~100Hz) | 识别热点函数与调用栈耗时 |
| Heap | GC 时快照 | 分析内存分配峰值与泄漏线索 |
| Goroutine | 即时 goroutine 列表 | 检测 goroutine 泄漏或堆积 |
| Block | 阻塞系统调用/chan 等 | 定位锁竞争或 I/O 等待瓶颈 |
| Mutex | runtime.SetMutexProfileFraction(1) 后启用 |
分析互斥锁持有时间与争用频率 |
# 示例:采集 15 秒 CPU profile 并交互分析
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/profile?seconds=15"
-http=:8080启动可视化 Web 界面;?seconds=15覆盖默认 30s,适用于高吞吐短周期场景。所有 profile 均基于 runtime 内置采样器,零侵入、低开销。
3.2 pprof Web UI与命令行交互式分析技巧(focus、peek、top、svg生成)
pprof 提供两种互补的分析入口:Web UI(pprof -http=:8080)和交互式命令行。二者共享核心指令集,但交互逻辑迥异。
常用交互命令语义
top:显示开销最高的函数栈(默认前10,支持top10/top5%)focus <regex>:仅保留匹配路径的调用栈,自动裁剪无关分支peek <regex>:高亮匹配函数及其直接上下游,保留上下文web/svg:生成可视化调用图(需 Graphviz)
SVG生成示例
# 生成带火焰图语义的调用图(按CPU时间着色)
pprof -svg cpu.pprof > profile.svg
此命令调用
dot渲染器将采样数据转为 SVG;-svg隐含-nodecount=100 -nodefraction=0.001,过滤微小节点以提升可读性。
交互式分析流程示意
graph TD
A[加载profile] --> B{选择视图}
B -->|top| C[定位热点函数]
B -->|focus| D[收缩至关键路径]
B -->|peek| E[观察局部依赖]
C --> F[web/svg导出]
| 指令 | 适用场景 | 关键参数 |
|---|---|---|
top |
快速识别瓶颈函数 | -cum 显示累积耗时 |
focus |
分析特定模块性能边界 | 支持正则,如 focus http.* |
3.3 内存泄漏定位:从alloc_objects到inuse_space的归因链路拆解
内存监控指标间存在强因果依赖:alloc_objects(累计分配对象数)持续增长而 inuse_space(当前驻留堆空间)同步攀升,是泄漏典型信号。
核心归因路径
alloc_objects→ 触发GC →heap_inuse未回落 →inuse_space持续高位- 关键断点:GC后
freed_objects偏低,heap_alloc与heap_inuse差值收窄但不归零
// runtime/metrics 示例:采集归因链关键指标
m := metrics.Read([]metrics.Description{
{"/gc/heap/allocs:objects"}, // alloc_objects
{"/gc/heap/inuse:bytes"}, // inuse_space
{"/gc/heap/frees:objects"},
}...)
该调用一次性拉取原子快照,避免采样时序错位;/gc/heap/allocs:objects 是累计计数器,需与前值做delta比对才具诊断意义。
指标关联性验证表
| 指标名 | 类型 | 含义 | 泄漏敏感度 |
|---|---|---|---|
/gc/heap/allocs:objects |
Counter | 累计分配对象总数 | ★★★★☆ |
/gc/heap/inuse:bytes |
Gauge | 当前堆中活跃对象占用字节数 | ★★★★★ |
graph TD
A[alloc_objects↑] --> B[GC触发]
B --> C{freed_objects ≈ alloc_objects?}
C -->|否| D[inuse_space↑ 持续]
C -->|是| E[内存健康]
第四章:Go trace火焰图全栈解读与性能瓶颈突破
4.1 runtime/trace机制原理与trace文件生成/可视化完整链路
Go 的 runtime/trace 是内核级采样与事件驱动的轻量级性能分析机制,基于 go:linkname 绕过公开 API 直接注入运行时事件钩子。
核心触发路径
- 启动
trace.Start()后,运行时开启 goroutine 调度、网络轮询、GC 等 20+ 类事件的原子计数器采集; - 所有 trace 事件通过环形缓冲区(
runtime.traceBuf)异步写入,避免阻塞关键路径; trace.Stop()触发 flush 并序列化为二进制格式(含 magic headergo trace\x00\x00\x00\x00)。
trace 文件结构关键字段
| 字段 | 长度 | 说明 |
|---|---|---|
| Magic | 8 bytes | go trace + 4字节零填充 |
| Version | 4 bytes | 当前为 0x00000001 |
| Event Header | 3 bytes | 类型+时间戳增量+PID |
// 启动 trace 示例(需在 main goroutine 早期调用)
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f) // ⚠️ 必须在 GC 启动前调用
// ... 应用逻辑 ...
trace.Stop()
该调用注册全局 trace.enable 原子标志,并初始化 trace.buf 环形队列;trace.Stop() 强制 flush 所有 pending buffer 并写入 EOF marker。
可视化链路
graph TD
A[trace.Start] --> B[Runtime 事件注入]
B --> C[环形 buffer 缓存]
C --> D[trace.Stop flush]
D --> E[trace.out 二进制文件]
E --> F[go tool trace trace.out]
F --> G[Web UI 可视化:Goroutine/Network/Scheduler 视图]
4.2 火焰图核心维度解析:goroutine调度延迟、GC STW、网络阻塞、系统调用热点
火焰图并非仅展示CPU耗时,其纵轴深度承载关键运行时语义,横轴宽度反映采样频次。四大核心维度需结合 Go 运行时特性交叉解读:
goroutine 调度延迟
表现为 runtime.schedule → runtime.findrunnable 长栈深、宽底座,常伴随 runtime.gopark 持续挂起。典型诱因是 P 队列空转或全局队列争抢。
GC STW 阶段
在 runtime.stopTheWorldWithSema 下出现尖锐、窄而高的峰值,持续时间与堆大小强相关。可通过 GODEBUG=gctrace=1 验证 STW 时长。
网络阻塞热点
net.(*pollDesc).waitRead 或 internal/poll.(*FD).Read 栈顶频繁出现,说明协程阻塞于 epoll/kqueue 等系统事件等待。
系统调用热点
syscall.Syscall / runtime.entersyscall 后紧接 runtime.exitsyscall,若中间栈帧稀疏且宽度大,表明 syscall 执行耗时(如 read 大文件、write 慢设备)。
| 维度 | 关键栈特征 | 典型原因 | 观测建议 |
|---|---|---|---|
| 调度延迟 | schedule → findrunnable → gopark |
P 不足、高并发抢占 | go tool trace 查看 Goroutine Scheduler Dashboard |
| GC STW | stopTheWorldWithSema 单峰突起 |
堆碎片多、对象存活率高 | GOGC=off 对比验证 |
// 示例:触发可观测的 syscall 热点(避免生产使用)
func slowSyscall() {
f, _ := os.Open("/dev/zero") // 实际中可替换为慢设备
buf := make([]byte, 1)
_, _ = f.Read(buf) // 阻塞式读,易在火焰图中标记为 syscall 热点
}
该函数强制进入 read(2) 系统调用,Go 运行时会记录 entersyscall → exitsyscall 时间差;火焰图中呈现为独立宽帧,横轴宽度正比于阻塞时长。参数 buf 大小影响内核拷贝开销,小缓冲区放大 syscall 调用频次,利于定位 I/O 路径瓶颈。
4.3 结合pprof与trace的交叉验证方法论:识别伪热点与真实瓶颈
为什么单靠pprof可能误判?
pprof 的 CPU profile 基于采样,易将高频调用但低耗时的函数(如 time.Now())误标为“热点”;而 trace 可揭示调用链上下文与阻塞点。
交叉验证三步法
- 启动带 trace 支持的服务:
go run -gcflags="all=-l" -ldflags="-s -w" main.go - 并行采集:
# 同时运行 pprof 和 trace go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 & curl "http://localhost:6060/debug/trace?seconds=30" -o trace.out
关键比对维度
| 维度 | pprof 侧重 | trace 侧重 |
|---|---|---|
| 时间粒度 | 毫秒级采样 | 微秒级事件序列 |
| 上下文 | 单一栈帧聚合 | Goroutine 状态变迁 |
| 瓶颈类型 | CPU 密集型 | 阻塞、调度、IO 等 |
实例:识别伪热点
func handler(w http.ResponseWriter, r *http.Request) {
for i := 0; i < 1e6; i++ {
_ = time.Now() // 高频调用 → pprof 显示为热点
}
io.WriteString(w, "OK")
}
该循环在 pprof 中占比高,但 trace 显示其 goroutine 始终处于 running 状态、无阻塞或调度延迟——判定为伪热点;真正瓶颈需关注 trace 中 Goroutine blocked 或 Syscall 持续段。
graph TD
A[pprof 热点函数] --> B{是否在 trace 中伴随长阻塞?}
B -->|是| C[真实瓶颈]
B -->|否| D[伪热点:优化价值低]
4.4 生产环境低开销trace采样策略与增量分析最佳实践
核心采样策略选型
在高吞吐服务中,固定率采样(如1%)易导致关键链路漏采,而全量采集则引发存储与计算爆炸。推荐动态分层采样:
- HTTP 5xx 错误链路:100% 强制采样
- P99 延迟 > 2s 的请求:50% 采样
- 其余流量:按服务QPS动态调节(0.1%–5%)
自适应采样代码示例
def adaptive_sample(trace_id: str, status_code: int, latency_ms: float, qps: float) -> bool:
if status_code >= 500:
return True # 强制捕获错误
if latency_ms > 2000:
return hash(trace_id) % 2 == 0 # 50% 采样慢请求
base_rate = max(0.001, min(0.05, 0.01 * (qps / 100))) # QPS 越高,基础率越低
return hash(trace_id) % int(1/base_rate) == 0
逻辑说明:
hash(trace_id)确保同请求ID采样一致性;base_rate随QPS线性缩放,避免突发流量打爆后端;int(1/base_rate)实现概率截断,规避浮点精度误差。
增量分析流水线设计
graph TD
A[原始Span流] --> B{采样过滤}
B -->|保留Span| C[按trace_id聚合]
C --> D[增量计算P99/错误率]
D --> E[变更检测:Δ > 5% 触发告警]
| 策略维度 | 静态采样 | 动态分层采样 | 基于特征的条件采样 |
|---|---|---|---|
| CPU开销 | 极低 | 中等(+12%) | 较高(+35%,需解析tag) |
| 关键问题捕获率 | 低 | 高 | 最高(可定制业务规则) |
第五章:Golang高阶能力全景图与进阶学习路径
并发模型的深度实践:从 goroutine 泄漏到结构化并发控制
在真实微服务网关项目中,曾因未正确处理超时导致数万 goroutine 积压。通过 errgroup.WithContext 替代裸 go 启动,结合 context.WithTimeout 实现请求级生命周期绑定,内存占用下降 73%。关键代码片段如下:
g, ctx := errgroup.WithContext(context.WithTimeout(parentCtx, 5*time.Second))
for _, endpoint := range endpoints {
ep := endpoint
g.Go(func() error {
return callExternalAPI(ctx, ep)
})
}
if err := g.Wait(); err != nil {
log.Error("failed to aggregate", "err", err)
}
接口设计的演进:从空接口到类型约束泛型
某日志聚合模块初版使用 interface{} 导致大量 type switch 和运行时 panic。升级至 Go 1.18 后重构为泛型函数:
func Filter[T any](items []T, pred func(T) bool) []T {
var result []T
for _, item := range items {
if pred(item) {
result = append(result, item)
}
}
return result
}
// 调用示例:Filter[User](users, func(u User) bool { return u.Active })
内存管理实战:pprof 分析与逃逸优化
通过 go tool pprof -http=:8080 cpu.prof 定位到 JSON 序列化中 []byte 频繁堆分配问题。将 json.Marshal 替换为预分配缓冲区的 json.NewEncoder(buf).Encode(),QPS 提升 22%,GC 次数减少 40%。
依赖注入的工程化落地
采用 uber-go/fx 构建可测试的依赖树,在电商订单服务中定义如下模块:
| 模块名称 | 职责 | 生命周期 |
|---|---|---|
DBModule |
初始化 PostgreSQL 连接池 | Singleton |
CacheModule |
Redis 客户端与 TTL 策略 | Singleton |
OrderServiceModule |
订单创建/查询业务逻辑 | Transient |
测试驱动的性能调优闭环
针对支付回调验签瓶颈,建立三级验证体系:
- 单元测试覆盖 ECDSA 签名边界条件(如空 payload、无效 curve)
- 基准测试
BenchmarkVerifySignature显示 1024 字节 payload 下延迟 8.3μs - 生产环境通过 OpenTelemetry 自动采集
verify_signature_duration_ms指标,P99
构建可观测性基础设施
在 Kubernetes 集群中部署 prometheus-operator + grafana,自定义 exporter 抓取 Go runtime 指标:
graph LR
A[Go App] -->|expose /metrics| B[Prometheus]
B --> C[Grafana Dashboard]
C --> D[AlertManager]
D --> E[Slack/Email]
核心监控项包括 go_goroutines、go_memstats_heap_alloc_bytes、http_request_duration_seconds_bucket,并设置 rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]) > 0.5 作为慢请求率阈值。
云原生编译与分发策略
采用多阶段 Dockerfile 构建生产镜像,基础镜像从 gcr.io/distroless/static:nonroot 切换为 cgr.dev/chainguard/go:1.22,镜像体积从 127MB 缩减至 18MB,CVE 高危漏洞归零。同时启用 go build -trimpath -buildmode=exe -ldflags="-s -w" 清除调试符号与路径信息。
错误处理的语义化演进
弃用 fmt.Errorf("failed to parse %s: %w", input, err) 的模糊包装,改用 pkg/errors 的 WithStack() 与自定义错误类型:
type ValidationError struct {
Field string
Code string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Code)
}
// 在 HTTP handler 中统一转换为 400 响应体
持续交付流水线中的 Go 特化检查
GitHub Actions 工作流集成以下质量门禁:
golangci-lint启用errcheck、govet、staticcheck插件go test -race -coverprofile=coverage.out覆盖率阈值 ≥ 85%go mod graph | grep -E "(github.com|golang.org)/" | wc -l控制第三方依赖数量 ≤ 32
生态工具链协同工作流
基于 buf.build 管理 Protocol Buffers,entgo.io 生成类型安全 ORM,oapi-codegen 自动生成 OpenAPI 3.0 客户端,三者通过 Makefile 统一触发:
generate: proto schema client
proto:
buf generate --template buf.gen.yaml
schema:
ent generate ./ent/schema
client:
oapi-codegen -generate types,client api/openapi.yaml 