feat: release v0.28 generic Go project scaffolding and lifecycle entry

This commit is contained in:
2026-09-29 23:58:20 +08:00
parent f8e0dcc5a6
commit 0b6c0043b6
42 changed files with 2134 additions and 117 deletions

6
examples/program/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/program
/var/
.env
.env.*
!.env.example
/.goOmni/project.lock

View File

@@ -0,0 +1,11 @@
{
"version": 1,
"kind": "project",
"templateVersion": "v0.28.0-preview",
"module": "example.com/program",
"generated": {
"internal/config/loadGen.go": "8a1a2746f46596a7893832ca411bd7e2d23978661943d322ee732d2c095cbe47",
"internal/svc/runtimeGen.go": "0eb681169112302add633cc7355fbd19844b01fa0b38b94625ec18127e5fb937",
"main.go": "36168d3e3fe822f74cede8eac3fffe5502174899e242c41526f6ad5cab7af4fd"
}
}

View File

@@ -0,0 +1,18 @@
# 通用程序示例
由 `go-omni project new` 生成,入口、加载和装配文件未经修改。项目仅在配置添加 `once`,在 `internal/run/run.go` 决定完成即退出或等待信号;无端口、数据库、队列和业务任务。
```sh
cd examples/program
go mod tidy
go run . -check-config
go run . # once: true,完成即退出
# 编辑 etc/config.yaml 为 once: false 后运行,用 Ctrl+C / SIGTERM 停止。
go build -o program .
go-omni project check
go test ./...
```
使用同版远程 v0.28.0-preview CLI 与框架,无本地 replace。源码发布前开发可临时 `go mod edit -replace git.yuntuo.hk/k2safe/go-omni=../..`,验证后移除此本地替换。
配置、svc、Run 归项目维护;main/Gen.go 和清单由生成器维护。背景组件在 Init 登记停止与排空,已接受工作使用 WorkContext;未结束时不会提前释放共享依赖。详细规则见 [PROGRAM.md](../../PROGRAM.md)。

View File

@@ -0,0 +1,12 @@
# Project-owned. Environment references are explicit: ${NAME} or ${NAME:-fallback}.
log:
level: info
format: json
output: stderr
lifecycle:
stopTimeout: 5s
drainTimeout: 10s
cancelTimeout: 5s
# true: finite run; false: wait for SIGINT/SIGTERM.
once: true

5
examples/program/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module example.com/program
go 1.22.0
require git.yuntuo.hk/k2safe/go-omni v0.28.0-preview

View File

@@ -0,0 +1,29 @@
package config
import (
omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
// Config, Default and Validate are project-owned. Add project settings here.
type Config struct {
Once bool `json:"once"`
Log logx.Config `json:"log"`
Lifecycle lifecycle.ShutdownConfig `json:"lifecycle"`
}
func Default() Config {
log := logx.Default()
log.Service = "example.com/program"
return Config{Once: true, Log: log, Lifecycle: lifecycle.DefaultShutdownConfig()}
}
func (c Config) Validate() error {
if err := c.Log.Validate(); err != nil {
return omniConfig.AtField("log", err)
}
if err := c.Lifecycle.Validate(); err != nil {
return omniConfig.AtField("lifecycle", err)
}
return nil
}

View File

@@ -0,0 +1,12 @@
// Code generated by go-omni project. DO NOT EDIT.
package config
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
func Load(path string) (Config, error) {
loader, err := NewLoader()
if err != nil {
return Config{}, err
}
return omniConfig.LoadProject(path, Default(), loader)
}

View File

@@ -0,0 +1,6 @@
package config
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
// NewLoader is project-owned. Register additional decoders here if needed.
func NewLoader() (*omniConfig.Loader, error) { return omniConfig.NewLoader() }

View File

@@ -0,0 +1,19 @@
package run
import (
"context"
"example.com/program/internal/svc"
)
// Run is project-owned. Return nil for normal completion, or an error to fail.
// The starter waits for shutdown; a one-shot program can simply return when done.
// Register background stop/drain hooks in svc.Init, not concurrently here.
// Accepted background work uses deps.WorkContext(), not the early-stop ctx.
func Run(ctx context.Context, deps *svc.ServiceContext) error {
if deps.Config.Once {
deps.Logger.InfoContext(ctx, "program.completed")
return nil
}
<-ctx.Done()
return nil
}

View File

@@ -0,0 +1,44 @@
// Code generated by go-omni project. DO NOT EDIT.
package svc
import (
"context"
"errors"
"example.com/program/internal/config"
"fmt"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
func New(ctx context.Context, logger *logx.Logger, cfg config.Config) (result *ServiceContext, err error) {
if ctx == nil {
ctx = context.Background()
}
if err = ctx.Err(); err != nil {
return nil, errors.Join(err, lifecycle.Cause(ctx))
}
if logger == nil {
return nil, errors.New("service context requires a logger")
}
if err = cfg.Validate(); err != nil {
return nil, err
}
runtime, err := lifecycle.NewRuntimeWithConfig(logx.WithLogger(ctx, logger), cfg.Lifecycle)
if err != nil {
return nil, err
}
c := &ServiceContext{Runtime: runtime, Config: cfg, Logger: logger}
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("dependency initialization panic: %v", p)
}
if err != nil {
result = nil
err = c.Finish(err)
}
}()
if err = c.Init(c.Context()); err != nil {
return nil, err
}
return c, nil
}

View File

@@ -0,0 +1,24 @@
package svc
import (
"context"
"example.com/program/internal/config"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
// ServiceContext is project-owned. Add dependencies without global variables.
// Runtime owns registered resources; Logger is borrowed from the entry point.
type ServiceContext struct {
*lifecycle.Runtime
Config config.Config
Logger *logx.Logger
}
// Init runs before Run. Register resources immediately with OnClose.
// Register paired OnStop/OnDrain hooks before exposing background work.
// Use WorkContext for accepted work; Context is the early stop notification.
// OnDrain returns nil only after processing and final cleanup have completed.
// Complete all registrations here, before Run can race with shutdown.
// The drain name "go-omni/project/run" is reserved for the generated entry.
func (c *ServiceContext) Init(ctx context.Context) error { return nil }

89
examples/program/main.go Normal file
View File

@@ -0,0 +1,89 @@
// Code generated by go-omni project. DO NOT EDIT.
package main
import (
"context"
"errors"
"example.com/program/internal/config"
program "example.com/program/internal/run"
"example.com/program/internal/svc"
"flag"
"fmt"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
"git.yuntuo.hk/k2safe/go-omni/logx"
"log"
"os"
"os/signal"
"syscall"
)
func main() {
path := flag.String("config", "etc/config.yaml", "configuration file; empty uses Default()")
check := flag.Bool("check-config", false, "validate configuration without initializing dependencies")
flag.Parse()
if *check {
if _, err := config.Load(*path); err != nil {
log.Fatal(err)
}
return
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, *path); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context, path string) (runErr error) {
cfg, err := config.Load(path)
if err != nil {
return err
}
logger, err := logx.New(cfg.Log)
if err != nil {
return err
}
var deps *svc.ServiceContext
defer func() {
if p := recover(); p != nil {
runErr = fmt.Errorf("program initialization panic: %v", p)
}
if deps != nil {
runErr = deps.Finish(runErr)
}
// Unjoined work may still use its dependencies and logger. The process exits
// unsuccessfully; resource retention never means the work completed.
if !errors.Is(runErr, lifecycle.ErrResourcesRetained) {
runErr = errors.Join(runErr, logger.Close())
}
}()
deps, err = svc.New(ctx, logger, cfg)
if err != nil {
return err
}
if deps.Context().Err() != nil {
return nil
}
done := make(chan struct{})
// This reserved drain proves Run has actually returned, including its defers.
// Runtime bounds the wait even when Run ignores cancellation.
deps.OnDrain("go-omni/project/run", func(context.Context) error { <-done; return nil })
logger.InfoContext(deps.Context(), "program.starting")
go func() {
var result error
returned := false
defer func() {
if p := recover(); p != nil {
result = fmt.Errorf("program run panic: %v", p)
} else if !returned {
result = errors.New("program run exited without returning")
}
deps.RequestShutdown(result)
close(done)
}()
result = program.Run(deps.Context(), deps)
returned = true
}()
<-deps.Context().Done()
return nil
}