Back to Article

06. 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。