Go
06. Go 语言从零到服务端
本章按“能写服务”为目标学习 Go,不追求一次覆盖所有语法。读者会 Java,所以重点放在 Go 和 Java 的思维差异。
1. 项目结构与模块
创建项目:
mkdir go-payments
cd go-payments
go mod init example.com/go-payments
推荐初学结构:
go-payments/
cmd/api/main.go
internal/payment/
handler.go
service.go
repository.go
model.go
internal/platform/
config/
database/
logging/
go.mod
规则:
cmd/api放启动入口。internal下的包不能被外部模块导入,适合放业务代码。- 不要一开始就做复杂分层框架。
- 包名短小,避免
utils泛滥。
2. Go 与 Java 的关键差异
2.1 没有类继承,组合优先
Go 使用 struct + method:
type PaymentService struct {
repo PaymentRepository
}
func (s *PaymentService) Create(ctx context.Context, cmd CreatePaymentCommand) (*Payment, error) {
return s.repo.Insert(ctx, cmd)
}
2.2 接口由使用方定义
type PaymentRepository interface {
Insert(ctx context.Context, cmd CreatePaymentCommand) (*Payment, error)
}
Java 常在 provider 侧定义接口,Go 更常在 consumer 侧定义小接口。好处是测试和替换更轻。
2.3 error 是返回值
payment, err := service.Create(ctx, cmd)
if err != nil {
return nil, err
}
不要忽略 error。金融服务中,每个错误都应该能映射到:
- 是否可重试。
- 是否用户可见。
- 是否需要告警。
- 是否影响状态机。
3. 错误处理
定义业务错误:
package payment
import "errors"
var (
ErrIdempotencyConflict = errors.New("idempotency conflict")
ErrPaymentNotFound = errors.New("payment not found")
)
包装错误:
if err != nil {
return nil, fmt.Errorf("insert payment order %s: %w", cmd.OrderNo, err)
}
判断错误:
if errors.Is(err, payment.ErrIdempotencyConflict) {
writeError(w, http.StatusConflict, "IDEMPOTENCY_CONFLICT")
return
}
不要把内部错误字符串直接返回给用户。
4. context:请求生命周期
服务端每个请求都有 context.Context。它用于:
- 超时。
- 取消。
- 传递请求级元数据。
- 让数据库、HTTP 客户端等下游感知取消。
HTTP handler 示例:
func (h *Handler) CreatePayment(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel()
var cmd CreatePaymentCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST")
return
}
p, err := h.service.Create(ctx, cmd)
if err != nil {
h.writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, p)
}
原则:
- 函数第一个参数通常是
ctx context.Context。 - 不要把 context 存在 struct 字段里。
- 下游调用必须传 ctx。
- 超时要按链路预算设置。
5. HTTP 服务
最小标准库服务:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
生产服务还要补:
- request id。
- access log。
- panic recovery。
- CORS 策略。
- body size limit。
- graceful shutdown。
- metrics。
6. 并发
6.1 goroutine
go func() {
doWork()
}()
goroutine 很便宜,但不是免费。必须考虑:
- 谁取消它。
- 错误如何返回。
- 并发上限。
- 是否会泄漏。
6.2 WaitGroup
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
callRisk()
}()
go func() {
defer wg.Done()
callAccount()
}()
wg.Wait()
6.3 errgroup
实际服务端推荐 errgroup 管理并发错误和取消:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return riskClient.Check(ctx, req)
})
g.Go(func() error {
return accountClient.Freeze(ctx, req)
})
if err := g.Wait(); err != nil {
return err
}
6.4 channel
channel 用于 goroutine 间通信,不要为了“像 Go”而滥用。简单共享状态用 mutex 可能更清晰。
7. 数据库
使用 database/sql 或封装库时都要理解连接池:
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
事务:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// execute statements
if err := tx.Commit(); err != nil {
return err
}
注意:
defer tx.Rollback()在 commit 成功后会返回已提交错误,通常可忽略。- 不要在事务里做长时间远程调用。
- SQL 参数化,禁止字符串拼接用户输入。
8. 测试
表驱动测试:
func TestValidateCreatePayment(t *testing.T) {
tests := []struct {
name string
cmd CreatePaymentCommand
wantErr bool
}{
{"valid", CreatePaymentCommand{Amount: 100}, false},
{"zero amount", CreatePaymentCommand{Amount: 0}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateCreatePayment(tt.cmd)
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr = %v", err, tt.wantErr)
}
})
}
}
运行 race detector:
go test -race ./...
运行漏洞检查:
govulncheck ./...
9. Graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
Kubernetes 终止 Pod 时会发送 SIGTERM。服务要停止接新请求,等待旧请求完成,再退出。
10. 本章练习
练习 1:支付创建 API
实现:
POST /payments- JSON 请求体。
- 参数校验。
- 幂等键。
- 结构化错误响应。
- 单元测试。
练习 2:并发聚合
实现:
GET /payment-preview- 并发调用 account 和 risk。
- 总超时 800ms。
- 任一失败返回明确错误。
练习 3:优雅关闭
启动服务后压测请求,同时发送 SIGTERM,观察是否能完成已有请求。
11. 本章检查清单
- 能解释 Go module。
- 能写出带 context 的 HTTP handler。
- 能正确包装和判断 error。
- 能区分 goroutine、channel、mutex、WaitGroup 的适用场景。
- 能写表驱动测试和运行 race detector。
- 能实现 graceful shutdown。