第一章:Go初学者最稀缺的不是语法,而是这5个被大厂隐藏的工程化脚手架思维
很多初学者花数周啃完《Effective Go》和《Go语言圣经》,却在真实项目中卡在“不知道从哪开始建仓库”。语法只是砖瓦,而工程化脚手架思维才是设计蓝图——它决定代码能否被协作、测试、部署和演进。
项目根目录的黄金结构意识
大厂项目几乎从不把 main.go 直接放在仓库根目录。标准起点是:
myapp/
├── cmd/ # 可执行入口(每个子目录对应一个二进制)
│ └── myapp/ # go run cmd/myapp/main.go
├── internal/ # 仅本项目可导入的私有逻辑(防止外部依赖泄漏)
├── pkg/ # 可被外部复用的公共组件(含 go.mod + versioned API)
├── api/ # OpenAPI 定义与生成代码(如通过 oapi-codegen)
└── go.mod # module 名必须为 github.com/org/myapp(非 ./)
执行 go mod init github.com/yourname/myapp 是第一道工程门槛——module path 不是路径别名,而是未来所有 import 路径的权威源头。
构建可验证的本地开发流
用 mage 替代零散 shell 脚本,统一生命周期命令:
# 安装 mage(需 Go 1.16+)
go install github.com/magefile/mage@latest
# 在项目根目录创建 magefile.go,定义:
// +build mage
package main
import "os/exec"
// Build 编译所有 cmd 下二进制
func Build() error { return exec.Command("go", "build", "./cmd/...").Run() }
// Test 运行带覆盖率的单元测试
func Test() error { return exec.Command("go", "test", "-cover", "./...").Run() }
之后只需 mage build 或 mage test,新人无需记忆 go test -race ./internal/... 等细节。
接口契约先行的模块拆分直觉
不先写实现,而是定义 pkg/storage/storage.go 中的接口:
type UserStore interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id string) (*User, error)
}
再让 internal/adapter/postgres/user_store.go 实现它——这种“接口在 pkg,实现藏 internal”的分层,天然隔离业务逻辑与基础设施。
配置即代码的声明式习惯
拒绝硬编码端口或数据库地址。使用 config/config.go + config/config.yaml,并通过 viper 统一加载,确保 TestMain 中可注入 mock 配置。
日志与追踪的上下文贯穿本能
每层函数签名优先接收 context.Context,日志使用 log.WithContext(ctx).Info("user created"),而非 log.Info()——这是分布式追踪链路可落地的前提。
第二章:模块化与依赖治理——从go.mod到企业级版本对齐策略
2.1 理解go.mod语义化版本与replace指令的生产级用法
语义化版本约束的本质
Go 模块使用 vMAJOR.MINOR.PATCH 格式表达兼容性契约:
MAJOR变更表示不兼容API修改;MINOR允许向后兼容新增;PATCH仅修复缺陷,保证完全兼容。
replace 的三大生产场景
- 替换私有仓库路径(如
gitlab.internal/pkg→./internal/pkg) - 临时集成未发布分支(
v1.2.0 => ./fix-auth) - 覆盖间接依赖的已知漏洞版本
安全替换示例
// go.mod 片段
replace github.com/some/lib => github.com/our-fork/lib v1.3.5
此声明强制所有对
some/lib的引用解析为our-fork/lib@v1.3.5,绕过原始模块的v1.2.0。replace仅在当前模块构建时生效,不传递给下游消费者,确保依赖图可控。
| 场景 | 是否影响 go list -m all |
是否写入 go.sum |
|---|---|---|
| 本地路径 replace | ✅ 显示重定向后路径 | ❌ 不生成校验项 |
| 远程模块 replace | ✅ 显示目标模块版本 | ✅ 记录目标模块哈希 |
2.2 使用go.work管理多模块协同开发的真实场景实践
在微服务架构下,auth-service、order-service 和共享模块 shared-utils 常需跨仓库协同迭代。此时 go.work 成为关键协调枢纽。
初始化工作区
go work init
go work use ./auth-service ./order-service ./shared-utils
该命令生成 go.work 文件,声明三个模块的本地路径。go build 或 go test 将自动解析依赖图,优先使用本地模块而非 replace 指令或 proxy 缓存。
依赖解析优先级(从高到低)
| 优先级 | 来源 | 示例说明 |
|---|---|---|
| 1 | go.work use 路径 |
直接读取 shared-utils/ 源码 |
| 2 | replace 指令 |
仅当模块未被 use 时生效 |
| 3 | go.mod 中版本声明 |
默认回退至 v1.2.0 发布版 |
多模块同步调试流程
graph TD
A[修改 shared-utils/log.go] --> B[运行 auth-service]
B --> C[自动加载最新 log 实现]
C --> D[无需 go mod edit -replace]
此机制显著降低灰度验证成本,使跨模块接口变更可即时验证。
2.3 依赖收敛与最小版本选择(MVS)原理与故障排查演练
依赖收敛是 Go 模块构建的核心机制:当多个路径引入同一模块时,Go 选择所有需求版本中的最小满足版本(即 MVS),而非最新版。
MVS 决策逻辑
Go 工具链遍历 go.mod 依赖图,对每个模块收集所有 require 声明的版本约束(如 v1.2.0, v1.5.0, >=v1.3.0),取其语义化版本的最大下界。
# 示例:项目中存在以下 require 行
github.com/example/lib v1.2.0
github.com/example/lib v1.5.0
github.com/example/lib v1.3.1
→ MVS 结果为 v1.5.0(因 v1.5.0 ≥ v1.2.0 ∧ v1.5.0 ≥ v1.3.1,且是满足全部约束的最小可能版本)
常见冲突场景
| 现象 | 根因 | 排查命令 |
|---|---|---|
build: ... requires github.com/x/y v0.1.0, but vendor/modules.txt claims v0.2.0 |
vendor 未同步 MVS 结果 | go mod vendor && go mod verify |
undefined: SomeFunc |
MVS 选了不兼容旧 API 的小版本(如 v2.0.0-xxx) | go list -m -versions github.com/x/y |
故障复现与验证流程
graph TD
A[执行 go build] --> B{是否报符号缺失?}
B -->|是| C[检查 go list -m all \| grep target]
B -->|否| D[跳过]
C --> E[比对各路径 require 版本]
E --> F[运行 go mod graph \| grep target]
依赖版本不一致时,优先使用 go mod edit -dropreplace 清理临时替换,再 go mod tidy 触发 MVS 重计算。
2.4 私有模块代理与校验和锁定机制在CI/CD中的落地实现
在 CI/CD 流水线中,私有模块代理(如 Nexus Repository 或 Verdaccio)与 pnpm-lock.yaml / Cargo.lock 等校验和锁定文件协同工作,确保依赖可重现性。
校验和验证流程
# 在 CI 构建前强制校验锁文件完整性
pnpm install --frozen-lockfile --strict-peer-dependencies
--frozen-lockfile阻止自动生成新锁文件;--strict-peer-dependencies拒绝不兼容的 peer 依赖——二者共同强化构建确定性。
私有代理配置示例(.npmrc)
registry=https://nexus.internal/repository/npm-group/
@myorg:registry=https://nexus.internal/repository/npm-private/
always-auth=true
//nexus.internal/repository/npm-private/:_authToken=${NEXUS_TOKEN}
关键校验项对比
| 验证环节 | 触发阶段 | 失败后果 |
|---|---|---|
| 锁文件哈希匹配 | pnpm install |
构建中断,退出码 ≠ 0 |
| 包签名验证 | 下载时 | 自动拒绝未签名包 |
| 代理元数据一致性 | 首次拉取 | 缓存跳过,直连源仓库 |
graph TD
A[CI Job Start] --> B{Lockfile exists?}
B -->|Yes| C[Verify integrity via SHA512]
B -->|No| D[Fail fast]
C --> E[Proxy resolves package from private repo]
E --> F[Compare artifact checksum with lock]
F -->|Match| G[Proceed to build]
F -->|Mismatch| H[Abort with audit log]
2.5 依赖图可视化分析与循环引用破除实战(go mod graph + graphviz)
Go 模块依赖关系复杂时,go mod graph 是定位隐式循环引用的首选工具。它输出有向边列表,每行形如 a/b c/d,表示 a/b 直接依赖 c/d。
生成原始依赖图
go mod graph | head -n 5
# 输出示例:
# github.com/example/app github.com/example/utils
# github.com/example/utils github.com/example/core
# github.com/example/core github.com/example/app ← 循环线索!
该命令无参数,输出全量依赖边;管道截断便于快速扫描可疑回边。
可视化闭环诊断
go mod graph | dot -Tpng -o deps.png
需提前安装 Graphviz。dot 将边列表转为 PNG 图像,直观暴露环路结构。
常见循环模式识别
| 模式类型 | 特征 | 破解策略 |
|---|---|---|
| 直接循环 | A → B → A | 提取公共接口到新模块 |
| 间接跨包循环 | A → B → C → A | 引入中间抽象层解耦 |
自动化检测流程
graph TD
A[go mod graph] --> B[grep 'pkgA.*pkgB\|pkgB.*pkgA']
B --> C{匹配到边?}
C -->|是| D[定位 import 语句]
C -->|否| E[排除该对]
第三章:可观测性基建前置——日志、指标、追踪三位一体设计
3.1 结构化日志接入Zap与上下文透传的中间件封装
日志中间件核心职责
- 拦截 HTTP 请求,自动注入请求 ID、路径、方法等元信息
- 将
context.Context中携带的 traceID、userID 等透传至 Zap 日志字段 - 统一错误捕获并结构化记录(含 status code、latency、error stack)
中间件实现示例
func LoggingMiddleware(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 执行后续 handler
// 提取上下文中的 traceID(若已由上游注入)
traceID, _ := c.Get("trace_id")
fields := []zap.Field{
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", time.Since(start)),
zap.String("trace_id", fmt.Sprintf("%v", traceID)),
}
logger.Info("HTTP request completed", fields...)
}
}
逻辑分析:该中间件在
c.Next()前后分别记录起止时间,确保 latency 准确;c.Get("trace_id")依赖上游中间件(如 Jaeger 注入)或自定义解析,需保证 context 传递一致性;所有字段均为结构化键值对,避免字符串拼接。
关键字段映射表
| 上下文 Key | 日志字段名 | 类型 | 说明 |
|---|---|---|---|
"trace_id" |
trace_id |
string | 分布式追踪唯一标识 |
"user_id" |
user_id |
string | 认证后用户主键(可选) |
"request_id" |
req_id |
string | 单次请求唯一 ID(兜底) |
请求生命周期日志流
graph TD
A[Client Request] --> B[TraceID 注入中间件]
B --> C[LoggingMiddleware 开始计时]
C --> D[业务 Handler 执行]
D --> E[LoggingMiddleware 记录结构化日志]
E --> F[Response 返回]
3.2 Prometheus指标暴露规范与Gauge/Counter/Histogram选型指南
Prometheus 指标暴露需遵循 文本格式规范 v0.0.4:以 # HELP 和 # TYPE 开头,指标名须小写、下划线分隔,且语义明确。
核心指标类型语义差异
- Counter:单调递增计数器(如
http_requests_total),适用于事件累计; - Gauge:可增可减瞬时值(如
memory_usage_bytes),反映当前状态; - Histogram:观测样本分布(如
http_request_duration_seconds),自动分桶并提供_sum/_count/_bucket。
选型决策表
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| API 调用总次数 | Counter | 不可逆累积,天然支持 rate() |
| 当前活跃连接数 | Gauge | 可上升/下降,需直接读取瞬时值 |
| 请求延迟分布 | Histogram | 需 P90/P99、中位数等分位统计 |
# Python client 示例:正确暴露 histogram
from prometheus_client import Histogram
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds',
'HTTP request latency in seconds',
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)
# 在请求处理结束时调用:.observe(duration)
buckets显式定义分位边界,避免默认宽泛桶导致精度丢失;_bucket时间序列数量 =len(buckets) + 1(含+Inf)。
3.3 OpenTelemetry SDK集成与Jaeger链路追踪全链路注入实践
OpenTelemetry(OTel)SDK 是实现可观测性的核心载体,其与 Jaeger 的对接需兼顾采集、传播与导出三重能力。
集成依赖配置
<!-- Maven: OpenTelemetry SDK + Jaeger Exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.exporter</groupId>
<artifactId>opentelemetry-exporter-jaeger-thrift</artifactId>
<version>1.38.0</version>
</dependency>
该配置引入轻量级 Thrift 协议导出器,兼容 Jaeger Agent 默认端口 6831;opentelemetry-sdk 提供 TracerSdkProvider 和上下文传播管理能力。
全链路注入关键步骤
- 初始化全局
OpenTelemetrySdk并注册 Jaeger exporter - 使用
W3CBaggagePropagator+W3CTraceContextPropagator实现跨服务 TraceID/B3/Baggage 注入 - 在 HTTP 客户端拦截器中自动注入
traceparent头
Jaeger 导出配置对照表
| 参数 | 示例值 | 说明 |
|---|---|---|
endpoint |
http://localhost:14250 |
gRPC endpoint(Jaeger Collector) |
agentHost |
localhost |
Thrift UDP agent 模式主机 |
agentPort |
6831 |
Jaeger Agent 默认 Thrift 端口 |
graph TD
A[Service A] -->|inject traceparent| B[Service B]
B -->|propagate context| C[Service C]
C -->|export via Thrift| D[Jaeger Agent]
D --> E[Jaeger Collector]
E --> F[Jaeger UI]
第四章:API生命周期管理——从接口定义到契约测试的工业化流水线
4.1 基于OpenAPI 3.1的Go代码生成与Swagger UI自动化托管
OpenAPI 3.1正式支持JSON Schema 2020-12,为强类型语言(如Go)提供了更精确的类型推导能力。
代码生成:oapi-codegen 实战
oapi-codegen -generate types,server,client \
-package api \
openapi.yaml > api/generated.go
-generate types,server,client 同时生成数据模型、HTTP服务骨架与客户端SDK;openapi.yaml 必须符合OpenAPI 3.1规范,否则将因nullable/const等新字段校验失败。
自动化托管流程
graph TD
A[CI触发] --> B[验证OpenAPI 3.1语法]
B --> C[生成Go服务桩]
C --> D[嵌入Swagger UI静态资源]
D --> E[启动/healthz探针校验]
关键依赖对比
| 工具 | OpenAPI 3.1支持 | Go泛型映射 | 内置UI托管 |
|---|---|---|---|
| oapi-codegen | ✅ | ✅(via oneOf→interface{}+type switch) |
❌(需手动集成) |
| go-swagger | ❌(仅3.0.3) | ⚠️(泛型需补丁) | ✅ |
自动化托管通过http.FileServer挂载embed.FS打包的Swagger UI 5.x资源,实现零配置文档即服务。
4.2 gRPC-Gateway双协议统一网关层设计与错误码标准化映射
为统一暴露 gRPC 服务与 RESTful 接口,采用 gRPC-Gateway 作为反向代理层,通过 protoc 插件自动生成 HTTP 路由绑定。
错误码映射核心原则
- gRPC 状态码(
codes.Code)→ HTTP 状态码 + 标准化 JSON 错误体 - 所有业务错误统一注入
error_code字段,与内部领域错误码对齐
映射配置示例(gateway.proto)
// 在 .proto 文件中声明 HTTP 映射与错误注解
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/v1/users/{id}"
additional_bindings: [{
post: "/v1/users:lookup"
body: "*"
}]
};
// 自定义错误码映射(需配合 custom error handler)
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
extensions: [
{ name: "x-error-mapping", value: "{\"INVALID_ARGUMENT\":400,\"NOT_FOUND\":404,\"INTERNAL\":500}" }
]
};
}
}
该配置驱动
protoc-gen-grpc-gateway生成路由,并在HTTPErrorHandler中解析x-error-mapping实现状态码与error_code的双向绑定。value字符串被运行时 JSON 解析为 map[string]int,确保 REST 响应体始终含{"error_code":"USER_NOT_FOUND","message":"..."}。
标准化错误响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
error_code |
string | 领域唯一错误码(如 AUTH_TOKEN_EXPIRED) |
message |
string | 用户可读提示(支持 i18n 占位) |
details |
object | 可选上下文(如 field_violations) |
graph TD
A[HTTP Request] --> B[gRPC-Gateway]
B --> C{Error Intercepted?}
C -->|Yes| D[Map gRPC Code → HTTP Status + error_code]
C -->|No| E[Forward to gRPC Server]
D --> F[JSON Response with standardized schema]
4.3 使用Specmatic进行消费者驱动契约测试(CDC)实战
消费者驱动契约测试的核心在于让消费方定义接口期望,再由提供方验证实现是否满足。Specmatic 以 OpenAPI 为契约载体,支持双向验证。
定义消费者契约(booking-contract.yaml)
openapi: 3.0.1
info:
title: Booking API
version: 1.0.0
paths:
/bookings:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
userId: { type: integer }
flightId: { type: string }
responses:
'201':
content:
application/json:
schema:
type: object
properties:
id: { type: string }
status: { type: string, enum: [CONFIRMED, PENDING] }
该契约声明了 POST /bookings 的请求结构与成功响应格式。Specmatic 将据此生成模拟服务(stub)供消费者集成测试,同时导出验证规则供提供方执行契约检查。
验证流程
# 启动消费者端 stub
specmatic stub booking-contract.yaml
# 提供方验证实现是否符合契约
specmatic contract-test booking-contract.yaml --provider-jar booking-service.jar
| 角色 | 工具命令 | 目的 |
|---|---|---|
| 消费者 | specmatic stub |
启动可信赖的依赖模拟服务 |
| 提供方 | specmatic contract-test |
自动化验证实现契约一致性 |
graph TD
A[消费者定义契约] --> B[生成Stub供集成测试]
A --> C[提供方加载契约]
C --> D[运行契约测试]
D --> E{通过?}
E -->|是| F[安全发布]
E -->|否| G[修复接口实现]
4.4 接口变更影响分析与自动生成BREAKING CHANGE报告流程
核心分析流程
使用静态解析 + 调用图追踪识别跨服务影响范围:
# 基于 OpenAPI 3.0 差分分析生成变更摘要
openapi-diff v1.yaml v2.yaml --break-only --output json \
--include-path "/api/v1/users" \
--exclude-tag "deprecated"
--break-only 仅输出不兼容变更(如字段删除、类型变更);--include-path 限定分析路径,提升精度;--exclude-tag 过滤已标记弃用的接口,避免误报。
影响传播可视化
graph TD
A[API Schema 变更] --> B[SDK 代码生成器]
B --> C[客户端调用点扫描]
C --> D[CI 环节触发报告生成]
D --> E[自动提交 BREAKING_CHANGE.md]
报告结构规范
| 字段 | 示例 | 说明 |
|---|---|---|
impacted-services |
[auth-service, billing-api] |
依赖该接口的下游服务列表 |
migration-guide |
v2: replace 'user_id' → 'uid' |
强制迁移操作指引 |
自动化流程覆盖 92% 的语义级破坏性变更识别。
第五章:极简go语言后端开发入门之道
初始化一个零依赖HTTP服务
使用Go标准库 net/http 三行代码即可启动Web服务。无需框架、不引入第三方模块,避免抽象层带来的理解负担。以下是最小可行示例:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from Go! 🚀")
})
http.ListenAndServe(":8080", nil)
}
运行后访问 http://localhost:8080 即可看到响应。该服务内存占用低于3MB,冷启动耗时
路由与请求解析实战
标准库虽无内置路由树,但可通过 r.URL.Path 和 r.Method 实现轻量级REST风格分发:
| 路径 | 方法 | 功能 |
|---|---|---|
/api/users |
GET | 返回用户列表(JSON) |
/api/users |
POST | 创建新用户(解析JSON body) |
/api/users/123 |
GET | 获取ID为123的用户 |
关键技巧:使用 json.NewDecoder(r.Body).Decode(&user) 直接反序列化,避免字符串拼接与手动字段提取。
中间件模式的极简实现
中间件本质是函数链式调用。以下为日志+超时中间件组合示例(无第三方包):
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[%s] %s %s\n", time.Now().Format("15:04:05"), r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func timeout(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 组合:http.ListenAndServe(":8080", timeout(logging(mux)))
错误处理与结构化响应
Go坚持显式错误检查,拒绝panic式全局错误捕获。定义统一响应结构体提升API一致性:
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, resp Response) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(resp)
}
当数据库查询失败时,直接返回 writeJSON(w, http.StatusInternalServerError, Response{Code: 500, Message: "DB query failed"}),前端无需解析非标准错误格式。
并发安全的内存缓存
利用 sync.Map 实现无锁高频读写缓存,替代Redis在单机场景中的角色:
var cache sync.Map // key: string, value: []byte
// 写入(带TTL)
cache.Store("user:123", struct {
Data []byte
Expire time.Time
}{Data: userData, Expire: time.Now().Add(10 * time.Minute)})
// 读取(自动过期检查)
if raw, ok := cache.Load("user:123"); ok {
if entry := raw.(struct{ Data []byte; Expire time.Time }); entry.Expire.After(time.Now()) {
w.Write(entry.Data)
}
}
构建与部署流水线
使用多阶段Dockerfile压缩镜像至12MB以内:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -ldflags="-s -w" -o server .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
CMD ["./server"]
配合GitHub Actions实现push即部署:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.REGISTRY }}/go-backend:latest
性能压测对比数据
使用wrk对相同功能接口进行基准测试(AWS t3.micro实例):
| 方案 | QPS | 平均延迟 | 内存峰值 |
|---|---|---|---|
| Go原生HTTP | 12,480 | 1.2ms | 8.3MB |
| Express.js (Node 20) | 7,920 | 2.8ms | 112MB |
| Flask (Python 3.11 + Gunicorn) | 3,150 | 6.7ms | 186MB |
所有测试启用连接复用与gzip压缩,Go版本未启用任何性能优化标志,纯默认编译。
环境配置的类型安全管理
通过结构体标签绑定环境变量,避免字符串硬编码:
type Config struct {
Port int `env:"PORT" default:"8080"`
Debug bool `env:"DEBUG" default:"false"`
Database string `env:"DB_URL" required:"true"`
}
func loadConfig() Config {
var cfg Config
env.Parse(&cfg) // 使用github.com/caarlos0/env
return cfg
}
启动时执行 PORT=9000 DEBUG=true ./server 即可动态覆盖配置。
日志输出到结构化JSON流
采用 log/slog 标准库输出机器可读日志,兼容ELK与Loki:
slog.SetDefault(slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
}),
))
slog.Info("user created", "id", 123, "ip", r.RemoteAddr, "ua", r.UserAgent())
日志字段自动转为JSON键值对,时间戳、源文件、行号内建支持,无需额外序列化逻辑。
