wind

package module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 15 Imported by: 0

README

Go Wind

极简、可组合的 Go 微服务框架

积木式架构 · 接口驱动 · 零魔法 · 生产就绪

中文 · English · 日本語


设计哲学

不是全家桶,而是积木盒。

go-wind 信奉 组合优于继承、接口优于实现 的 Go 原生哲学。框架只定义协议与生命周期骨架,不绑定任何具体基础设施。每一个模块——传输、注册中心、日志——都只暴露最小接口,由使用者按需拼装,就像搭积木一样。

全家桶框架 go-wind
绑定 gRPC + etcd + zap 你选 gRPC 还是 HTTP?你决定
框架接管一切 框架只管生命周期
升级框架 = 升级全家桶 升级框架 = 升级骨架
学习曲线陡峭 5 分钟读完源码

核心特性

  • 积木式组装 — 核心仅管生命周期,传输/日志只暴露最小接口。注册中心、配置中心等能力由 go-wind-plugins 提供具体实现
  • 优雅生命周期 — 信号感知、超时可控的 Server 启停;单服务崩溃自动级联全量优雅退出
  • 无侵入 Context — TraceID / UserID / ColorTag 通过 context 传播,深拷贝防 data race
  • 极简日志门面 — 4 方法接口 + Enabled + With,几行胶水代码即可适配 slog / zap / zerolog / kratos log;具体适配器(slog adapter、LevelFilter、MultiLogger)由 go-wind-plugins 提供
  • 功能选项模式WithServerWithName… 链式配置,类型安全,可读性强
  • 零外部依赖 — 仅依赖 golang.org/x/sync,框架本身不到 500 行代码

快速开始

安装
go get github.com/tx7do/go-wind
最小示例
package main

import (
    "context"
    "log"

    wind "github.com/tx7do/go-wind"
    "github.com/tx7do/go-wind/transport"
)

// MyServer 实现 transport.Server 接口
type MyServer struct{}

func (s *MyServer) Start(ctx context.Context) error {
    <-ctx.Done()
    return ctx.Err()
}

func (s *MyServer) Stop(ctx context.Context) error {
    // 执行清理逻辑(ctx 携带超时)
    return nil
}

func (s *MyServer) Endpoint() string {
    return "grpc://0.0.0.0:9000"
}

func main() {
    app := wind.New(
        wind.WithID("order-service-01"),
        wind.WithName("order-service"),
        wind.WithVersion("v1.0.0"),
        wind.WithServer(&MyServer{}),
    )

    if err := app.Run(context.Background()); err != nil {
        log.Fatal(err)
    }
}
多 Server 组合
app := wind.New(
    wind.WithName("gateway"),
    wind.WithServer(grpcServer, httpServer, wsServer),
)

// 三个 Server 并发启动,收到信号后并发优雅停止
app.Run(ctx)
服务实例构建
app := wind.New(
    wind.WithName("user-service"),
    wind.WithServer(grpcServer),
)

// 构建服务实例,用于注册到你选的注册中心实现(go-wind-plugins)
inst := app.Instance("grpc://0.0.0.0:9000")
// inst.ID / inst.Name / inst.Version / inst.Endpoints

app.Run(ctx)
日志接入
import windlog "github.com/tx7do/go-wind/log"

// 方式一:实现 log.Logger 接口,适配你的日志后端(slog / zap / zerolog…)
// 具体适配器(slog adapter、LevelFilter、MultiLogger)由 go-wind-plugins 提供
windlog.SetLogger(myZapAdapter{})

// 方式二:使用 go-wind-plugins/log/slog 提供的适配器
//   import pluginslog "github.com/tx7do/go-wind-plugins/log/slog"
//   windlog.SetLogger(pluginslog.New(mySlogLogger))

// App 级别日志器,未设置时回退到全局 logger
app.Logger().Info(ctx, "starting")

// 昂贵参数构造可用 Enabled 守卫
logger := app.Logger()
if logger.Enabled(windlog.LevelDebug) {
    logger.Debug(ctx, "detail", computeExpensiveData())
}
高级配置
app := wind.New(
    wind.WithServer(grpcServer),
    wind.WithStopTimeout(30*time.Second),  // 自定义优雅停机超时
    wind.WithSignal(syscall.SIGTERM),       // 自定义信号
    wind.WithLogger(myLogger),              // App 级别独立日志器
    wind.WithBeforeStop(func(ctx context.Context) error {
        // 停机前回调:注销服务、排空请求队列
        // 使用你选的注册中心实现(go-wind-plugins)
        return nil
    }),
    wind.WithAfterStop(func(ctx context.Context) error {
        // 停机后回调:关闭数据库连接、刷入缓冲
        return db.Close()
    }),
)

// App 级别日志器,未设置时回退到全局 logger
app.Logger().Info(ctx, "starting")

// Server.Endpoint() 返回实际监听地址(支持随机端口 :0)
endpoint := grpcServer.Endpoint()

// 等待 App 结束(可用于外部编排)
<-app.Done()
// 获取 Run 最终错误(需在 Done 关闭后调用)
if err := app.Err(); err != nil {
    log.Fatal(err)
}

模块架构

graph LR
    APP["wind.App<br/>生命周期编排"]
    APP -->|"管理"| SERVER["transport.Server<br/>接口约束"]
    APP -.->|"全局 fallback"| LOG["log.Logger<br/>接口(无内置实现)"]

核心只管 Server 生命周期。日志仅提供接口 + 全局注册器;注册中心、配置中心等由 go-wind-plugins 提供。

go-wind/
├── app.go              核心引擎:App 生命周期管理
├── errors.go           集中错误定义(包级哨兵错误)
├── context.go          请求级元数据传播(TraceID / UserID / Metadata)
├── instance.go         服务实例模型
├── errors/             结构化、传输感知的错误模型(WindError)
├── transport/          传输层抽象(Server)
└── log/                日志门面(Logger 接口 + Level + nop 实现 + 全局注册器)
模块总览
模块 核心接口 职责
wind App, Option 应用生命周期编排、优雅停机
wind Instance 服务实例建模
wind Metadata 请求级元数据(TraceID 等)链路传播
transport Server 传输层抽象,支持任意协议接入
log Logger, Level 日志接口契约 + 全局注册器;适配器由 plugins 提供

生命周期与优雅停机

go-wind 的核心能力是 可靠的应用生命周期管理

graph TB
    subgraph Phase 1: Start
        S1["srv.Start"] --> EG
        S2["srv.Start"] --> EG
        S3["srv.Start"] --> EG
    end

    SIG["信号到达<br/>SIGTERM / SIGINT / SIGQUIT"] --> TC
    CTX["ctx 取消"] --> TC
    CRASH["Server 崩溃 / 退出"] --> TC

    TC["triggerCancel()"] -->|"取消"| EG["errgroup<br/>egCtx"]

    EG --> P2["Phase 2: BeforeStop Hook<br/>同步执行,优先于 Stop"]
    P2 --> P3

    subgraph Phase 3: Server.Stop
        P3["并发 Stop"]
        P3 --> Stop1["srv.Stop<br/>独立超时 Ctx"]
        P3 --> Stop2["srv.Stop<br/>独立超时 Ctx"]
        P3 --> Stop3["srv.Stop<br/>独立超时 Ctx"]
    end

    Stop1 --> P4["Phase 4: AfterStop Hook<br/>同步执行,后于 Stop"]
    Stop2 --> P4
    Stop3 --> P4

设计要点:

机制 说明
独立停机上下文 Stop context 从 context.Background() 派生,从运行 context 派生,确保超时窗口真实有效
崩溃级联 任一 Server 崩溃或自行退出,errgroup 自动触发其余 Server 优雅停止
无双重 Stop App.Stop() 只触发取消信号,不直接调用 Server.Stop(),停机逻辑统一收口
信号感知 默认监听 SIGTERM / SIGINT / SIGQUIT,可自定义
生命周期 Hook WithBeforeStop / WithAfterStop 分阶段顺序执行:BeforeStop → Server.Stop → AfterStop,每阶段独立超时上下文
Server.Endpoint Server 实现暴露实际监听地址,支持 :0 随机端口绑定后获取真实地址用于注册
App.Err App.Err()Done() 关闭后返回 Run 的最终错误,便于外部编排感知结果

设计原则

1. 接口最小化

每个接口只定义必要方法。例如 Logger 有 4 个日志方法 + Enabled + With,适配任意后端只需几行胶水代码。Enabled 方法让使用者在昂贵参数构造前检查级别。

2. 零隐式依赖

框架不对你的注册中心、配置中心、日志库做任何假设。go.mod 中只有一个依赖:golang.org/x/sync

3. Context 原生

所有接口的第一个参数都是 context.Context,与 Go 标准库哲学一致,支持链路追踪和超时传播。

4. 并发安全

全局状态(logger)、元数据传播均做了并发安全处理,WithTraceID 对共享 map 做深拷贝避免 data race。


环境要求

要求
Go 版本 1.23+
外部依赖 golang.org/x/sync

开源许可

MIT License

Documentation

Overview

Package wind provides a minimalist microservice framework following a composable (Lego-like) design philosophy. The core App manages server lifecycles, while registration, logging and instance assembly are left entirely to the caller.

This is NOT a battery-included framework. Each subsystem (transport, log) exposes only interfaces and helper types so that callers mix and match implementations as needed.

Index

Examples

Constants

View Source
const (
	HeaderTraceID  = "x-wind-trace-id"
	HeaderUserID   = "x-wind-user-id"
	HeaderColorTag = "x-wind-color-tag"
)

Standard header keys used to propagate request-scoped metadata across service boundaries. Callers may use these keys or define their own.

Variables

View Source
var ErrAppAlreadyRunning = errors.New("wind: App.Run already called")

ErrAppAlreadyRunning is returned by App.Run when Run has already been called on the same *App instance. An *App is designed to be used once; create a new instance for each run.

Functions

func GetColorTag

func GetColorTag(ctx context.Context) string

GetColorTag returns the color tag from the context's Metadata, or an empty string if none is set.

func GetMetadata

func GetMetadata(ctx context.Context, key string) string

GetMetadata returns the value for key from the context's Metadata. It returns an empty string when the key is absent or no metadata exists.

func GetTraceID

func GetTraceID(ctx context.Context) string

GetTraceID returns the trace ID from the context's Metadata, or an empty string if none is set.

func GetUserID

func GetUserID(ctx context.Context) string

GetUserID returns the user ID from the context's Metadata, or an empty string if none is set.

func NewMetadataContext

func NewMetadataContext(ctx context.Context, md Metadata) context.Context

NewMetadataContext returns a copy of ctx with the given Metadata attached.

The provided map is deep-copied so that subsequent mutations by the caller do not affect the value stored in the context.

func WithColorTag

func WithColorTag(ctx context.Context, tag string) context.Context

WithColorTag returns a new context with the given color tag set in its Metadata. It is a convenience wrapper around WithMetadata.

func WithMetadata

func WithMetadata(ctx context.Context, key, value string) context.Context

WithMetadata returns a new context with the given key/value pair set in its Metadata.

The existing metadata map is deep-copied before modification to prevent concurrent write races when the parent context is shared across goroutines (BUG-2 regression guard).

Example

ExampleWithMetadata shows how to attach request-scoped metadata to a context and read it back. Each call deep-copies the map, so the parent context is never mutated (BUG-2 regression guard).

package main

import (
	"context"
	"fmt"

	"github.com/tx7do/go-wind"
)

func main() {
	ctx := context.Background()

	// Set a trace ID.
	ctx = wind.WithTraceID(ctx, "trace-abc")

	// Set a user ID on the same chain.
	ctx = wind.WithUserID(ctx, "user-42")

	fmt.Println(wind.GetTraceID(ctx))
	fmt.Println(wind.GetUserID(ctx))

}
Output:
trace-abc
user-42

func WithMetadatas added in v0.0.2

func WithMetadatas(ctx context.Context, extra Metadata) context.Context

WithMetadatas merges the key/value pairs from extra into the context's existing Metadata, performing a single deep-copy regardless of how many pairs are provided. This is more efficient than calling WithMetadata repeatedly when setting multiple keys at once (e.g. reconstructing context from inbound request headers).

If extra is empty and no existing metadata is present, ctx is returned unchanged.

func WithTraceID

func WithTraceID(ctx context.Context, traceID string) context.Context

WithTraceID returns a new context with the given trace ID set in its Metadata. It is a convenience wrapper around WithMetadata.

func WithUserID

func WithUserID(ctx context.Context, userID string) context.Context

WithUserID returns a new context with the given user ID set in its Metadata. It is a convenience wrapper around WithMetadata.

func WithoutMetadata added in v0.0.2

func WithoutMetadata(ctx context.Context, key string) context.Context

WithoutMetadata returns a new context with the given key removed from its Metadata. If the key does not exist or no metadata is present, ctx is returned unchanged.

Like WithMetadata, this operates on a private copy so the parent context is never mutated.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the central runtime that owns and manages the lifecycle of one or more transport.Server instances. It is intentionally free of any hard-coded integration — callers wire up servers, registries, loggers, etc. through the composable Option pattern.

func New

func New(opts ...Option) *App

New creates an *App with the given options. Sensible defaults are applied:

  • Listens for SIGTERM, SIGINT and SIGQUIT for graceful shutdown.
  • A 10-second stop timeout is enforced during shutdown.
Example

ExampleNew demonstrates creating an *wind.App with composable options. The framework does not start any server until App.Run is called.

package main

import (
	"fmt"

	"github.com/tx7do/go-wind"
)

func main() {
	app := wind.New(
		wind.WithID("svc-1"),
		wind.WithName("user-service"),
		wind.WithVersion("v1.0.0"),
	)

	fmt.Println("ID:", app.ID())
	fmt.Println("Name:", app.Name())
	fmt.Println("Version:", app.Version())

}
Output:
ID: svc-1
Name: user-service
Version: v1.0.0

func (*App) Done

func (a *App) Done() <-chan struct{}

Done returns a channel that is closed when [Run] finishes — either after a normal graceful shutdown or after a server crash. It allows external supervisors to wait for the app to terminate without calling [Stop] or wrapping [Run] in their own error channel. Done is provided for read-only observation; it must not be closed by the caller.

Before [Run] is called the channel is open (not closed).

func (*App) Err

func (a *App) Err() error

Err returns the error that caused [Run] to exit. It must be called after [Done] is closed; calling it before returns nil.

This complements [Done] by allowing external supervisors to observe both the termination and the outcome without wrapping [Run] in their own goroutine:

<-app.Done()
if err := app.Err(); err != nil { ... }

func (*App) ID

func (a *App) ID() string

ID returns the unique identifier set via WithID.

func (*App) Instance

func (a *App) Instance(endpoints ...string) *Instance

Instance builds an *Instance from the app's configured ID, Name and Version, plus the provided endpoint URLs. This is a convenience helper for callers who wish to register with a service registry — it does NOT perform any registration on its own (composable design: the caller chooses whether and how to register).

Example

ExampleApp_Instance demonstrates building a *wind.Instance from the app's configured identity fields. This is a convenience helper — callers still choose whether and how to register the instance.

package main

import (
	"fmt"

	"github.com/tx7do/go-wind"
)

func main() {
	app := wind.New(
		wind.WithID("svc-1"),
		wind.WithName("user-service"),
		wind.WithVersion("v1.0.0"),
	)

	inst := app.Instance("grpc://0.0.0.0:9000")

	fmt.Println(inst.ID, inst.Name, inst.Version)
	fmt.Println(inst.Endpoints[0])

}
Output:
svc-1 user-service v1.0.0
grpc://0.0.0.0:9000

func (*App) InstanceID added in v0.0.2

func (a *App) InstanceID() string

InstanceID returns the instance identifier. If set via WithInstanceID, that value is returned. Otherwise an auto-generated ID is returned on first access. The auto-generated format is:

"{id}-{version}@{hostname}@{randomShortHex}"

func (*App) Logger

func (a *App) Logger() log.Logger

Logger returns the app-specific logger set via WithLogger. If no logger was set, it falls back to the package-level global logger (log.GetLogger).

func (*App) Name

func (a *App) Name() string

Name returns the application name set via WithName.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run starts the application and blocks until all servers have stopped.

All registered servers are started concurrently inside an errgroup. The method returns when:

  • A registered OS signal (SIGTERM/SIGINT/SIGQUIT) is received.
  • The provided ctx is cancelled.
  • Any server's Start returns an error (server crash) or nil (server self-exit).

On any of these triggers, every server receives a Stop call with a fresh context derived from context.Background() — NOT from the run context — so the configured stopTimeout is honoured even after a.cancel() fires.

If no servers are registered, Run blocks until ctx is cancelled, a signal is received, or [Stop] is called. This is useful for pure worker applications that do not expose a network server but still want graceful shutdown.

Run must be called at most once per *App instance. Calling Run a second time returns ErrAppAlreadyRunning immediately.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop gracefully stops the application by cancelling the main context and waiting for all registered servers to finish shutting down.

Stop does NOT call Server.Stop directly — it only triggers cancellation and lets the Stop watchers inside [Run] perform the actual shutdown. This avoids double-Stop when Stop is called concurrently with an active Run (ISSUE-1).

Stop must be called from a different goroutine than [Run]. If Run has not been started, Stop blocks until ctx is done (there is nothing to stop).

func (*App) Version

func (a *App) Version() string

Version returns the application version set via WithVersion.

type Instance

type Instance struct {
	ID        string            `json:"id"`
	Name      string            `json:"name"`
	Version   string            `json:"version"`
	Endpoints []string          `json:"endpoints"`
	Metadata  map[string]string `json:"metadata"`
}

Instance describes a single service instance registered with (or discovered from) a service registry. It carries the information a client needs to connect: identity, version, network endpoints and arbitrary metadata.

App.Instance returns a populated *Instance for the caller to use with their chosen registry implementation in go-wind-plugins.

func (*Instance) FirstEndpoint

func (i *Instance) FirstEndpoint() string

FirstEndpoint returns the first endpoint URL or an empty string if the instance has no endpoints. This is a convenience for the common single-endpoint case.

type Metadata

type Metadata map[string]string

Metadata is a simple string-keyed map carried through the context chain. It is the vehicle for trace IDs, user IDs, color tags and other request-scoped attributes.

func MetadataFromContext

func MetadataFromContext(ctx context.Context) (Metadata, bool)

MetadataFromContext extracts the Metadata from ctx, if present.

The returned map is shared with the context and other callers. It MUST be treated as read-only: mutating it will corrupt the context value and cause data races when the context is shared across goroutines. To modify metadata, use WithMetadata, WithMetadatas, or WithoutMetadata which always operate on a private copy.

Use GetMetadata instead when only a single value is needed — it avoids exposing the underlying map entirely.

type Option

type Option func(*App)

Option configures an *App via functional options.

func WithAfterStop

func WithAfterStop(fn func(ctx context.Context) error) Option

WithAfterStop registers a callback invoked AFTER all servers have stopped. Typical uses include closing database connections, flushing log buffers, or releasing other resources.

Multiple callbacks are executed in registration order. If any callback returns an error, the error is logged but the error is not returned from Run (the servers have already stopped successfully).

func WithBanner added in v0.0.2

func WithBanner(enabled bool) Option

WithBanner enables or disables the startup banner. When enabled, App.Run prints the application name, version, appId, instanceId, PID and hostname at startup. Disabled by default to keep output clean.

func WithBeforeStop

func WithBeforeStop(fn func(ctx context.Context) error) Option

WithBeforeStop registers a callback invoked BEFORE any server's Stop is called during graceful shutdown. Typical uses include deregistering from a service registry, draining an incoming-request queue, or writing a final health-check ping.

Multiple callbacks are executed in registration order. If any callback returns an error, the error is logged but shutdown continues.

func WithID

func WithID(id string) Option

WithID sets the unique identifier of the application. It is typically used to construct an Instance for service registration.

func WithInstanceID added in v0.0.2

func WithInstanceID(id string) Option

WithInstanceID sets a custom instance identifier. If not set and banner is enabled, one is auto-generated in the format "{id}-{version}@{hostname}@{random}".

func WithLogger

func WithLogger(l log.Logger) Option

WithLogger sets an app-specific log.Logger. If not set, App.Logger falls back to the package-level global logger (log.GetLogger). This allows callers to give each *App instance its own logger without affecting the global state.

func WithName

func WithName(name string) Option

WithName sets the human-readable name of the application.

func WithServer

func WithServer(srv ...transport.Server) Option

WithServer attaches one or more transport.Server instances to the App. All servers are started concurrently when App.Run is called and stopped concurrently during graceful shutdown.

func WithSignal

func WithSignal(sigs ...os.Signal) Option

WithSignal overrides the default set of OS signals that trigger graceful shutdown. By default the app listens for SIGTERM, SIGINT and SIGQUIT.

func WithStopTimeout

func WithStopTimeout(d time.Duration) Option

WithStopTimeout sets the maximum duration allowed for graceful shutdown. Each server's Stop call receives a context with this deadline. The default is 10 seconds.

func WithVersion

func WithVersion(version string) Option

WithVersion sets the semantic version of the application.

Directories

Path Synopsis
Package errors provides a structured, transport-aware error model for the go-wind framework.
Package errors provides a structured, transport-aware error model for the go-wind framework.
Package log provides a minimal, backend-agnostic logging interface for the go-wind framework.
Package log provides a minimal, backend-agnostic logging interface for the go-wind framework.
Package transport defines the core transport-layer abstractions for the go-wind framework.
Package transport defines the core transport-layer abstractions for the go-wind framework.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL