gst

package module
v0.0.0-...-f88dca5 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

README

Ask DeepWiki

gst

gst 是一个面向 Go 后端项目的快速开发框架。它的核心使用方式是: 开发者在业务项目中编写 model DSL,随后用 gg 生成应用入口、路由注册、 model 注册和 service 注册,再在 service 中补充业务逻辑。

当前仓库是 gst 框架源码仓库。业务项目应通过 gg new 创建;不要在 gst 框架仓库根目录对业务项目运行 gg gen、gg migrate 等生成命令。

前后端接口对接(请求位置、通用查询参数、响应结构)以 API_CONTRACT.md 为准。

快速开始

安装 gg

业务项目直接安装发布版:

go install github.com/hydroan/gst/cmd/gg@latest

如果需要基于当前源码验证 gg 命令,可以在 gst 源码仓库安装本地版本:

make install
创建业务项目
gg new github.com/example/myapp
cd myapp
cp config.ini.example config.ini

gg new 会使用 module path 的最后一段创建项目目录,例如上面的目录名是 myapp。它会生成基础目录、main.go、config.ini.example 和各注册文件的空版本 (与在空项目上执行一次 gg gen 的结果相同,因此第一次 gg gen 留到写下第一个 model 之后),并执行 go mod tidy 和 git init;项目已经在某个 git 仓库里(例如 克隆下来的仓库,或大仓库里的子目录)时跳过 git init,不建嵌套仓库。

已经有项目目录(例如刚克隆下来的空仓库)时,进入该目录,在 module path 后面加 .,项目会直接生成在当前目录;当前目录名必须与 module path 的最后一段一致:

cd myapp
gg new github.com/example/myapp .

无论哪种写法,目标目录里只要已经有 gg new 要生成的目录或文件(如 model/、 main.go、.gitignore、go.mod),就报错退出,不写入任何文件;README.md、 .git 这类其他内容原样保留。

如果 go mod tidy 因网络、代理、本地 Go 缓存权限等原因失败,已生成的项目文件 通常仍保留在项目目录中。进入项目后修复环境并重新执行下面的命令;项目已经在 git 仓库里时,省略 git init:

go mod tidy
git init
了解生成目录

新项目的常用目录含义如下:

路径 责任
model/**/*.go 声明数据结构、接口 DSL、轻量 model hook
service/**/*.go 实现业务逻辑、复杂 hook、查询过滤和返回补充
service/**/*_test.go 与 service 文件同时由 gg gen 生成的测试骨架,含每个包的 main_test.go;生成后归项目维护,gg gen 不再改写
module/ 注册内置或自定义模块,例如 IAM
configx/ 扩展配置
cronjob/ 注册定时任务(每个调度时刻整个部署只领一次,被打断的一轮再跑一次)
leader/ 注册常驻任务(任一时刻整个部署只有一个副本在跑)
lock/ 声明锁(一件事同一时刻只做一次)
component/ 注册常驻组件(每个副本各跑一份、跑到进程结束)
middleware/ 注册中间件
interceptor/ 注册 gRPC 拦截器;只有声明了 GRPC() 模型的项目才有
pb/ 由 gg gen 从声明了 GRPC() 的模型推导的 .proto 和 Go 代码,提交进仓库;已提交的 .proto 决定字段编号,模型改动会破坏它时 gg gen 报错并写明在 .proto 里改哪一行来接受这次破坏(删掉的编号用 reserved 留住),不要删整个文件;模型不再走 gRPC 时 gg gen 直接删掉它的 Go 文件,.proto 留给 gg prune
router/router.gen.go 由 gg gen 生成的路由注册文件
model/model.gen.go 由 gg gen 生成的模型注册文件
model/apidoc.gen.go 由 gg gen 生成的注释与枚举注册文件,让 Swagger 文档在无源码的部署环境仍带字段说明和枚举值
service/service.gen.go 由 gg gen 生成的 service 注册文件
main.go 由 gg gen 生成的应用入口

通常只手写 model/**/*.go、service/**/*.go 和扩展目录中的业务代码。生成文件 不要手改;如果生成结果不符合预期,先检查 model DSL,再重新执行 gg gen。

开发主线

日常开发按这个顺序走:

  1. 在 model/**/*.go 中声明资源模型或动作模型。
  2. 每次修改 DSL 后运行 gg gen。
  3. 在生成的 service/** 文件中实现业务逻辑或 hook,并把同名 _test.go 里的骨架换成接口测试 (骨架被换掉之前 go test 会失败)。
  4. 使用 gg check 检查项目结构和依赖边界。
  5. 删除 model 或关闭 action 后,运行 gg prune 或 gg gen --prune 清理 model 不再需要的文件;它们只动 service/、middleware/、interceptor/ 和 pb/。

模型 DSL

model 是业务项目的主要输入。先判断当前接口属于哪一种模式。

数据库资源

普通资源使用 model.Base。如果这个资源需要建表或迁移,声明 Migrate()。

索引一律通过模型的 Indexes() []model.Index 方法集中声明:Fields 写 Go 字段名、顺序即列序,Unique 声明唯一索引,索引名由框架统一生成(idx_ 或 uniq_ 加表名加列名,超过方言上限的截短后带哈希:PostgreSQL 最长 63,MySQL 与 SQLite 64)。唯一的例外是主键——它由 model.Base/model.AutoBase 内置声明,业务模型不写主键。不要用 gorm tag 的 index/uniqueIndex/unique 配置索引,gg check 会拒绝。被索引的字符串字段要写列宽(如 gorm:"size:36"):没有长度的字符串在 MySQL 上是 longtext 列,MySQL 不能给它建索引,配置为 mysql 的项目 gg check 会拒绝,启动期与 gg migrate 也在建表之前拒绝。

package model

import (
	. "github.com/hydroan/gst/dsl"
	"github.com/hydroan/gst/model"
)

// Record is a database-backed resource.
type Record struct {
	UserID string `json:"user_id" query:"user_id"`
	Title  string `json:"title" query:"title"`

	model.Base
}

func (Record) Design() {
	Migrate()
	Endpoint("records")
	Param("rec")

	Create(func() {
		Service()
	})
	Patch(func() {
		Service()
	})
	List(func() {
		Service()
	})
	Get(func() {})
}

这个模型会生成类似下面的路由:

  • POST /api/records
  • PATCH /api/records/:rec
  • GET /api/records
  • GET /api/records/:rec

Param("rec") 控制单资源路由中的参数名。未声明 Param(...) 时,单资源路由默认 使用框架默认参数。Route(...) 里的参数段一律写 :name,路由只认这一种写法;写成 {name} 的 gg gen 直接拒绝。

需要自增整数主键的资源改用 model.AutoBase,字段和默认 hook 与 model.Base 一致,区别是 ID 由数据库在插入时分配(框架不会生成)。注意:这类模型通过 model.Register 注入 seed 记录时必须显式指定 ID 或依赖唯一索引,否则重复启动会 重复插入;Base 字符串 ID 的逗号分隔多 ID 查询写法对整数 ID 不适用。

自定义动作

不直接表示数据库表的接口优先使用 model.Empty,并为当前接口单独定义自己的 XXXReq、XXXRsp。即使字段完全一样,也不要复用其他接口的请求和响应结构体。

package tool

import (
	. "github.com/hydroan/gst/dsl"
	"github.com/hydroan/gst/model"
)

// Entry is a non-database action model.
type Entry struct {
	model.Empty
}

// EntryPair is one key/value pair submitted for merging.
type EntryPair struct {
	Key   string `json:"key"`
	Value string `json:"value,omitempty"`
}

// EntryMergeReq is the request for merging entries.
type EntryMergeReq struct {
	Entries []EntryPair `json:"entries"`
}

// EntryMergeRsp is the response returned after merging.
type EntryMergeRsp struct {
	Entries []EntryPair `json:"entries"`
}

func (Entry) Design() {
	Route("/entries/merge", func() {
		Create(func() {
			Service("merge")
			Payload[*EntryMergeReq]()
			Result[*EntryMergeRsp]()
		})
	})
}

这个接口会生成 POST /api/entries/merge,并生成 service/tool/entry/merge.go。Service("merge") 给动作起名:service 文件叫 merge.go、service 类型叫 Merge,模型声明了 GRPC() 时 rpc 名是它加模型名(MergeEntry), 避免同一个 model 内多个 Create action 都生成 create.go。

路由和可见性
  • Endpoint("records") 定义默认资源路径。
  • Route("/archive/documents", func() {...}) 定义额外路径或完全自定义路径。
  • Public() 表示公开接口,不走认证中间件;默认不写则需要认证。
  • Exact() 表示当前 action 按声明路径原样注册,不追加默认的 /:id、/batch 等后缀。
  • Payload[T]() 定义请求体类型;Result[T]() 定义响应体类型。
  • List、Get 是 HTTP GET 接口,没有请求体,禁止声明 Payload[T](); 只声明 Result[T]() 即成为自定义动作,生成的请求类型固定为 *model.Empty, 查询参数通过 ctx.Query()、路径参数通过 ctx.Param() 读取。
  • Import、Export 的 service 方法签名固定(Import(ctx, io.Reader) 读上传文件、 Export(ctx, ...M) 返回附件字节),禁止声明 Payload[T]() 和 Result[T]()。
  • Service() 表示当前 action 需要生成并注册业务 service,默认按动作命名 (Create 生成 create.go、类型 Creator);Service("merge") 同时给它起名, service 文件和 service 类型用这个名,rpc 名是它加模型名。
  • 只声明 Create(func(){})、List(func(){}) 等 action 就会启用对应接口; 不需要的接口不声明即可。
gRPC

模型顶层声明 GRPC(),它就同时走 gRPC:gg gen 从 Go 类型和 Design() 推导 .proto,进程内 编译出 pb/ 下的 Go 代码,并在 gRPC 服务上注册和 HTTP 路由相同的动作,业务代码只写 service/ 那一份。 字段用 pb tag 给消息编号(pb:"11",框架的 model.Base 字段占 1 到 10),缺的 gg gen 会补上。 只在 gRPC 上有的是流:自定义动作里用 Stream(func(){...}) 声明,流的那一侧写 StreamingPayload/StreamingResult, 另一侧写 Payload/Result,两侧都流就是双向流,service 方法收发 grpc.ServerStream、ClientStream 或 BidiStream。测试里 testutil.GRPCTarget() 给出测试服务器的 gRPC 地址;测试二进制没导入 pb 包、一个 gRPC 服务都没注册时,它直接 panic 并点名要加的导入,gg gen 写测试骨架时也会为此警告。完整示例见 examples/demo/model/board。

监听在 [grpc] 节配置,环境变量是 GRPC_PORT 这样的写法:listen、port(默认 8081,挨着 HTTP 的 8080)、 tls_enabled、cert_file、key_file(默认明文,和 HTTP 监听一样交给前面的入口终止 TLS)、reflection (默认开,grpcurl 能直接列出服务)。其余键就是 grpc-go 的服务端参数,不填就用 grpc-go 自己的默认值: keepalive_time、keepalive_timeout(服务端多久没动静就 ping 客户端、等多久没回音就断开,默认 2 小时、20 秒); keepalive_min_time、keepalive_permit_without_stream(客户端两次 ping 最少隔多久、没有调用时能不能 ping,默认 5 分钟、不能, ping 得更勤的客户端会被 too_many_pings 断开,客户端的 keepalive 要照这两个值配);max_connection_age、max_connection_age_grace (一条连接活多久就让客户端重连、在途的调用再宽限多久,默认不限;经 Kubernetes Service 这类按连接分流的入口,长连接不换就一直落在老副本上,靠它换到新副本); max_recv_msg_size(收多大的消息,写 8MiB 这样带单位的值,默认 4MiB 即 4,194,304 字节,更大的消息答 ResourceExhausted;MiB 按 1024 进、MB 按 1000 进,4MB 只有 4,000,000 字节,比默认值还小)。 时长键不能是负数,keepalive_time、max_connection_age 填了就至少 1 秒,max_recv_msg_size 要在 1 字节到 2GiB 之间;写错的值和读不出来的证书都让启动直接报错退出,没有模型声明 GRPC() 的项目也一样检查。 没有模型声明 GRPC() 的项目不开这个端口。监听上还有两个框架自带的服务:标准的健康服务 grpc.health.v1.Health,进程在服务时答 SERVING,收到停机信号后和 /-/readyz 同时变成 NOT_SERVING, 每个服务按全名也各报一份、和整体同起同落,Kubernetes 的 gRPC 探针指名 service 时问的就是它; 停机延迟过后两个监听一起关闭,在途的流在那一刻以 Unavailable 结束、客户端据此换副本重连,一元调用照常排空;以及反射服务。认证在 interceptor/ 里挂,和 middleware/ 一一对应: interceptor.RegisterAuth(interceptor.IAMSession()) 之后,每个没声明 Public() 的 rpc 都要在 authorization 元数据里带 Bearer <会话 id>;会话绑定登录时的 User-Agent,程序要拿会话调 gRPC,登录时得带和 gRPC 客户端 一样的 User-Agent(grpc-go 默认是 grpc-go/<版本>),浏览器登录的会话在 gRPC 上会被拒;健康与反射服务不经过项目挂的任何拦截器, Register 的通用拦截器和 RegisterAuth 的认证拦截器都不经过。多副本下的用法和核对步骤见 examples/cluster 的「gRPC 与认证」一章; 两条传输线怎么从声明、生成、注册、链路一路接到同一份 service 代码,见 TRANSPORTS.md:它只画架构全景, 不展开细节,细节以本文各节和代码为准。

业务 Service

生成后的业务逻辑主要写在 service/**。service 通常嵌入:

service.Base[M, REQ, RSP]

其中 M 是模型类型,REQ 是请求类型,RSP 是响应类型。

业务项目只需要关注 DSL、生成代码里的 router.Register / service.Register,以及 业务实现里的 service.Base。业务代码不需要依赖更底层的框架执行包;手写高级路由时 也应通过 router.Register 接入。

需要特别注意默认资源和自定义动作的 service 写法不同:

  • 默认资源 CRUD:当 M、REQ、RSP 是同一个类型时,框架会执行默认 数据库流程,业务侧主要实现 CreateBefore、CreateAfter、ListAfter、 Filter 等 hook。
  • 自定义动作:当 Payload 或 Result 让 REQ、RSP 不同于 M 时, 框架会调用 service 的 Create、List、Delete 等 action 方法。

自定义 List 动作接管整个请求,框架不再替它解析查询参数。这类 service 应该用 service.Base 上的 QueryXxx 方法把参数还原成查询条件,而不是自己重写一遍解析, 这样接口的筛选和分页行为与框架驱动的列表完全一致。解析针对的模型来自 Base 的 类型参数 M,不用也不能由调用方另外指定:

query, err := s.QueryModel(ctx)
if err != nil {
	return nil, gst.NewError(http.StatusBadRequest, err.Error())
}
query.TenantID = tenant // 客户端影响不了的强制条件

filters, err := s.QueryFilters(ctx)
if err != nil {
	return nil, gst.NewError(http.StatusBadRequest, err.Error())
}
opts := gst.QueryOptions{
	AllowEmpty:    true,
	PresentFields: s.QueryPresentFields(ctx),
	Filters:       filters,
}
cursor, err := s.QueryCursor(ctx)
if err != nil {
	return nil, gst.NewError(http.StatusBadRequest, err.Error())
}
orders, err := s.QueryOrders(ctx)
if err != nil {
	return nil, gst.NewError(http.StatusBadRequest, err.Error())
}
items := make([]*appmodel.Sample, 0)
if err = database.Database[*appmodel.Sample](ctx).
	WithQuery(query, opts).
	WithCursor(cursor).
	WithOrder(orders...).
	WithPagination(s.QueryPagination(ctx)).
	List(&items); err != nil {
	return nil, gst.NewErrorWithCause(http.StatusInternalServerError, "failed to list samples", err)
}

这六个方法覆盖 field[op] 过滤、零值过滤、排序、分页和 cursor 分页,各项是否生效取决于 model 声明的 model.Query、model.Pagination、model.Cursor。QueryOrders 会校验 _sort_by 的排序列,未知列名直接报错;但它们不保证 List 与 Count 用同一份查询 条件:统计总数时必须传入同样的查询值和 gst.QueryOptions(把 opts 存成变量复用是 最省事的写法),否则 total 会和当页数据对不上。

cursor 分页自带 ORDER BY,所以一次请求不能同时用 _cursor_value 和 _sort_by, 框架列表接口会直接返回 400。

OR 查询

QueryOptions 里的条件默认全部 AND 组合。需要 OR 时用 gst.FilterOr 构造分组, 组内条件之间 OR,分组整体仍与其他条件 AND——因此权限隔离这类强制条件不可能被 OR 掉:

Filters: []gst.Filter{
	appmodel.SampleCols.TenantID.Eq(tenant),      // 强制条件,始终 AND
	gst.FilterOr(                               // 一个搜索词横跨多列
		appmodel.SampleCols.Name.Like(keyword),
		appmodel.SampleCols.Code.Like(keyword),
	),
}
// WHERE tenant_id = ? AND (name LIKE ? OR code LIKE ?)

条件用 gg gen 为模型生成的列引用来写(上例的 appmodel.SampleCols),列名和值类型都在 编译期校验;拿不到具体模型的泛型工具函数用自己的类型参数现造列引用,例如 gst.NewColumn[M, string]("id").In(ids...),值类型同样在编译期校验。

gst.FilterAnd 用于在 OR 组内嵌套 AND,配合出 (a AND b) OR (c AND d):

Filters: []gst.Filter{
	appmodel.SampleCols.TenantID.Eq(tenant),
	gst.FilterOr(
		gst.FilterAnd(
			appmodel.SampleCols.Kind.Eq(KindPrimary),
			appmodel.SampleCols.Status.Eq(StatusDone),
		),
		gst.FilterAnd(
			appmodel.SampleCols.Kind.Eq(KindSecondary),
			appmodel.SampleCols.Status.Eq(StatusPending),
		),
	),
}
// WHERE tenant_id = ? AND ((kind = ? AND status = ?) OR (kind = ? AND status = ?))

分组可任意嵌套,深度不设限。FilterAnd 在顶层是合法但冗余的(等价于把子条件铺平 写)。空分组会收敛成空结果集而不是匹配全部:分组没有子条件一定是调用方写错了。

分组只能由服务端构造,URL 不提供该能力:?field[or]= 这类写法会返回 400, 客户端无法自行改变条件的组合方式。

需要一个恒为空集的条件时,例如权限钩子里拒绝当前用户看到任何行,用 gst.FilterFalse():它和其他过滤器一样是真实条件,渲染成 1 = 0。

默认资源的 hook 示例:

package record

import (
	"net/http"

	appmodel "github.com/example/myapp/model"

	"github.com/hydroan/gst"
	"github.com/hydroan/gst/service"
)

type Creator struct {
	service.Base[*appmodel.Record, *appmodel.Record, *appmodel.Record]
}

func (c *Creator) CreateBefore(ctx *gst.ServiceContext, record *appmodel.Record) error {
	if record.Title == "" {
		return gst.NewError(http.StatusBadRequest, "title is required")
	}
	return nil
}

自定义动作的 service 示例:

package entry

import (
	"github.com/example/myapp/model/tool"

	"github.com/hydroan/gst"
	"github.com/hydroan/gst/service"
)

type Merge struct {
	service.Base[*tool.Entry, *tool.EntryMergeReq, *tool.EntryMergeRsp]
}

func (m *Merge) Create(ctx *gst.ServiceContext, req *tool.EntryMergeReq) (*tool.EntryMergeRsp, error) {
	index := make(map[string]int, len(req.Entries))
	rsp := &tool.EntryMergeRsp{}

	for _, pair := range req.Entries {
		if i, ok := index[pair.Key]; ok {
			rsp.Entries[i] = pair // 同一个 key 以后出现的值为准
			continue
		}
		index[pair.Key] = len(rsp.Entries)
		rsp.Entries = append(rsp.Entries, pair)
	}
	return rsp, nil
}

查询和写库优先使用:

database.Database[*appmodel.Record](ctx)

并按需要组合 WithQuery、WithSelect、WithPagination、WithOrder、 WithLimit 等选项。一次查询或写入使用一个新的 database.Database[T](...) 链式调用,不要在无关操作之间复用同一个 database 句柄。

分析查询(Select)

看板和报表用 database.Select[M, R](ctx, 投影项...),不要把整表 List 进内存再用 Go 循环累加:分组聚合、窗口函数、联合、连接都从它出发。M 决定表、软删除范围和方言, R 是自己声明的结果行结构体:

type categoryTotal struct {
    Category string
    Amount   int64
    Records  int64
}

rows := make([]categoryTotal, 0)
err := database.Select[*appmodel.Record, categoryTotal](ctx,
        appmodel.RecordCols.Category.Group(), // 不带聚合函数的项即分组键
        appmodel.RecordCols.Amount.Sum(),     // 默认别名就是列名,多数情况不用写 As
        gst.Count().As("records"),
    ).
    Where(appmodel.RecordCols.TenantID.Eq(tenantID)).
    Scan(&rows)

框架从分组键推导 GROUP BY,所以 SELECT 和 GROUP BY 不可能写不一致。

项按值比较:同一个变量传两遍才是同一个项,As、Over、Where 都会造出新的项。 要在别处再引用的项先赋给变量:Having、OrderBy、Qualify、窗口的 PartitionBy 和 OrderBy、联合的排序、主查询读子投影的项,都是按项的值在投影里找同一个项,把同一个变量 传两遍最稳,改了别名的项就不再是同一个项。反过来,主查询自己能算出来的项(不带表的 gst.Count()、排名函数,主表或主查询连入模型的项)子投影也投影了同一个,而它还顶着构造时的 默认别名(列名、count、row_number 这些;没写 As 和写了默认名都算),框架分不清是谁的, 构建期报错。改法:想要主查询自己的,给它起个别名;想读子投影的,给子投影的项起个非默认别名再把它 传给主查询;两个都要就两边各起一个不同的别名。只有主查询自己也能算的项才有这一步,子投影自己 模型的项直接传即可。gst.Literal 常量无论别名都算主查询自己的,子投影投了同一个常量就报错; 要知道子投影有没有匹配到行,读它的键列,没匹配到的是 NULL。

几条会影响正确性的约定:

  • SUM 空集恒为 0(内部包了 COALESCE);AVG/MIN/MAX 空集是 NULL, 结果字段必须声明成指针,否则构建期报错。这样「没有数据」和「结果恰好 是 0」在报表上才可区分。可空列上的分组键、时间桶键和 MIN/MAX/AVG 同理: 没值的行自成一组、键是 NULL,结果字段也要是指针或 sql.Null。Where 里对这一列 写了条件就把 NULL 行排除了(IsNotNull、范围、等值都算,IsNull 除外,且不能在 FilterOr 组里),这时可以用普通字段。
  • 条件聚合复用普通过滤器,一次扫描出多列指标: Cols.Amount.Sum().Where(Cols.Status.Eq("done")).As("done_amount")。
  • As 是可选的。默认别名是列名,COUNT(*) 是 count。只有结果字段名和 列名不一致、或同一列上挂了两个度量时才需要写。
  • 别名和 R 的字段双向校验,任一侧对不上都是构建期错误,不会静默给出 一列 0。
  • 聚合规格写错一律报错(未知列、未知函数、别名对不上、Having 比较 nil 或切片等),不像客户端过滤器那样退化成空结果。
  • service 钩子里的隔离不会自动套用。模型自己声明的行规则跟着模型走:软删除和模型嵌入的 租户作用域对投影和 List 一样生效,连入的模型也在 ON 里带着。不会继承的是 List 时由 controller 跑的 service 钩子(Filter)加上的业务隔离:聚合是 service 直接 调用的,那些钩子不会执行——每个这样的条件都必须自己写进 Where。漏掉一个就会跨过钩子 划的边界聚合,且没有任何迹象。

单行结果用 ScanOne,分页报表的总组数用 Count。只问「有没有相关行」的跨表条件用 gst.FilterExists / FilterNotExists 半连接;要把另一张表的字段带进结果行,看下面的 「连接」小节,框架只放行每行最多对上一行的连接,join 到一对多子表让 SUM 静默翻倍的 那条路写不出来。子表与外层的关联列对用 子表列.EqCol(外层列) 作为谓词传入,复合键就多传几对,每一对都 必须成立;没有任何关联对的子查询按 fail closed 处理。它们是普通的 Filter 算子, List/Count/Export 同样能用。

窗口函数

窗口函数让每一行保留,旁边多出一列由同组其他行算出来的值:每组最新一行、累计、 排名、上一行的值都是它。窗口用 gst.PartitionBy 开出分区,再用 OrderBy 定 组内顺序,把它交给度量或排名函数的 Over;行级投影直接把列引用传给 Select, 它们按原值投影:

// 每个租户最新一条:ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY created_at DESC, id ASC)
rn := gst.RowNumber().
    Over(gst.PartitionBy(RecordCols.TenantID).OrderBy(RecordCols.CreatedAt.Desc())).
    As("rn")
err := database.Select[*appmodel.Record, latest](ctx, RecordCols.ID, RecordCols.TenantID, RecordCols.Amount, rn).
    Qualify(rn.Eq(1)). // WHERE 看不见窗口列,Qualify 由框架套一层派生表再筛
    Scan(&rows)

// 累计:COALESCE(SUM(amount) OVER (PARTITION BY tenant_id ORDER BY created_at ASC, id ASC
//                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 0)
running := RecordCols.Amount.Sum().
    Over(gst.PartitionBy(RecordCols.TenantID).OrderBy(RecordCols.CreatedAt.Asc())).
    As("running")

// 同组合计:每一行旁边带上本租户的总额,窗口不带 OrderBy 就不累计,整组一个数
// COALESCE(SUM(amount) OVER (PARTITION BY tenant_id), 0)
tenantTotal := RecordCols.Amount.Sum().Over(gst.PartitionBy(RecordCols.TenantID)).As("tenant_total")

// 排行:分组投影上开窗,窗口按度量排序,RANK() OVER (ORDER BY COALESCE(SUM(amount), 0) DESC)
total := RecordCols.Amount.Sum().As("total")
rank := gst.Rank().Over(gst.OrderBy(total.Desc())) // gst.OrderBy 是 gst.PartitionBy().OrderBy 的短写法:只排序、不分区的窗口

关键字与规则:

  • RowNumber、Rank、DenseRank 是包级函数,必须配带 OrderBy 的窗口; Lag、Lead 挂在列上,取窗内上一行、下一行的值,同样必须配带 OrderBy 的窗口, 分区两端是 NULL,结果字段必须能装 NULL。五个聚合函数加 .Over 即在窗口上算;CountDistinct 不能开窗, 分组键和时间桶也不能。
  • 决胜列由框架补:带 OrderBy 的窗口在行级投影末尾补主键升序,在分组投影补 分组键,同值行的编号和累计在每次运行都一样。Rank、DenseRank 不补,它们按定义 把并列行当同一名次;模型没有框架的 id 主键列时也不补,窗口就按写的顺序算。
  • 累计的帧固定为 ROWS:带顺序的聚合总是从分区第一行累加到当前行,不会像 SQL 默认的 RANGE 那样把同值行一起算进去。
  • 分组投影上的窗口读的是组:PartitionBy 只能用分组键或子投影的项,度量不行, 即便它读的列正是分组键;OrderBy 用分组键或已投影的度量;聚合函数在这里作用于组的度量,Sum().Over(...) 渲染成 COALESCE(SUM(SUM(amount)) OVER (...), 0),Avg、Lag、Lead 在分组投影上直接拒绝。
  • Qualify 按窗口列筛行,条件只能引用已投影的窗口项;Where、Having 留在 内层,排序、分页和 Count 作用在筛完之后的行集。
  • ScanOne 只读纯度量投影;行级投影用 Scan。既无聚合也无窗口的独立投影仍然拒绝, 那是 List 的活;作为联合分支或带连接时放行。行级投影里裸列直接传入即可,写成 .Group() 会把查询变成分组投影。
联合查询

几张表的同形态行叠成一份列表,例如收款和退款按时间统一分页,用 database.UnionAll[R]。 分支是普通的 Select,各自扫进同一个结果行 R;框架按 R 的字段顺序渲染每个分支的 SELECT 列表,按位置对齐写反的错误写不出来。列名不一致的用 列.As("...") 对齐,来源 标记用 gst.Literal 投影一列常量:

type flow struct {
    Kind   string    // 来源标记:payment 或 refund,由 Literal 写进每一行
    ID     string
    Amount int64
    At     time.Time // 付款的 paid_at、退款的 settled_at,两边都用 As 对齐到 at
}

paidAt := PaymentCols.PaidAt.As("at") // 排序还要用它,提成变量共享
payments := database.Select[*appmodel.Payment, flow](ctx,
    gst.Literal("payment").As("kind"), PaymentCols.ID, PaymentCols.Amount, paidAt).
    Where(PaymentCols.TenantID.Eq(tenantID))
refunds := database.Select[*appmodel.Refund, flow](ctx,
    gst.Literal("refund").As("kind"), RefundCols.ID, RefundCols.Amount, RefundCols.SettledAt.As("at")).
    Where(RefundCols.TenantID.Eq(tenantID))

feed := database.UnionAll[flow](ctx, payments, refunds).
    OrderBy(paidAt.Desc(), PaymentCols.ID.Desc()). // 按结果列 at、id 排
    Page(3, 20) // 第 3 页、每页 20 行
err := feed.Scan(&rows)  // 这一页
err = feed.Count(&total) // 总数

排序项按结果列名对齐,有两种写法。一是某个分支投影过的项(值相等即可,共享变量最稳),它要求 在场的分支里有一条投影了这个项;二是列名恰好等于结果列名的列引用,它只认名字,所属的表在这里 不参与判定,分支在不在场都成立(上例的 PaymentCols.ID)。As 改过的名字通常没有同名的列 引用,只能用第一种,分支会按条件增减时(比如只查一种类型就只放一条分支)就按在场的分支挑项:

settledAt := RefundCols.SettledAt.As("at") // refunds 分支投影的就是这个项
at := paidAt
if !withPayments { // 只查退款时 branches 里没有付款分支
    at = settledAt
}
feed := database.UnionAll[flow](ctx, branches...).OrderBy(at.Desc(), PaymentCols.ID.Desc())

结果列另有同名的列引用时(例如这份结果本身声明成了模型,带着生成的列引用),直接用它排也行, 分支怎么变都成立。

规则:

  • 只有 UNION ALL,不做去重的 UNION:两笔金额相同的记录会被去重版悄悄合并。 INTERSECT、EXCEPT 用 FilterExists、FilterNotExists 表达。
  • 分支可以是纯列投影,也可以分组、开窗、Qualify,各自带 Where、Having; 分支上不能写 OrderBy、Limit、Page,它们属于联合,写了构建期报错。 联合结果没有 Where,条件写进分支,每个分支用自己的索引。
  • 排序分页下推:带 Limit 或 Page 时框架把同样的排序和页尾行数(offset + limit)下推到每个分支, 各分支按索引只读前几十行,外层最多排 N 倍这个行数。
  • Count 是各分支计数相加,不物化任何一行,同样忽略排序分页。
  • Literal 的值只能是标识符(字母、数字、下划线,不以数字开头),内联为 'x' 而不是绑定参数,且必须 As 起别名。
  • 四种方言渲染同一条语句;ClickHouse 用 UnionAllOn,分支在同一实例上用 SelectOn 建。
连接

行里要带出另一张表的字段,例如付款单带出账户名称,用 Join:被连模型直接用它自己的 Cols 投影、过滤、分组、开窗、排序,多张表时列名自动带表名限定。只有裸列的投影也可以连接, 这是 List 跨不了的:

type paymentWithAccount struct {
    ID          string
    Amount      int64
    AccountName *string // LEFT JOIN 对不上的行这一列是 NULL,字段必须能装 NULL
}

err := database.Select[*appmodel.Payment, paymentWithAccount](ctx,
        PaymentCols.ID, PaymentCols.Amount, AccountCols.Name.As("account_name")).
    Join(gst.LeftJoin[*appmodel.Account](AccountCols.Code.EqCol(PaymentCols.Account))).
    Where(PaymentCols.TenantID.Eq(tenantID)).
    Scan(&rows)
// SELECT `payments`.`id` AS `id`, `payments`.`amount` AS `amount`, `accounts`.`name` AS `account_name`
// FROM `payments`
// LEFT JOIN `accounts` ON `accounts`.`code` = `payments`.`account` AND `accounts`.`deleted_at` IS NULL
// WHERE `payments`.`tenant_id` = ? AND `payments`.`deleted_at` IS NULL

规则:

  • 只允许每行最多对上一行的连接。ON 里的 EqCol 和常量等值必须覆盖被连模型的主键或某个 Indexes() 唯一索引,框架在构建期证明,证明不了报错。一对多从「多」的一侧出发写,或者 只问有没有就用 FilterExists;join 到一对多子表会让 SUM 静默翻倍,这条路写不出来。
  • Join 丢掉对不上的行,LeftJoin 保留它们并把被连列置 NULL;左连接来的字段必须是指针 或 sql.Null 类型,否则构建期报错。被连表的条件写在 Where 里会把 LeftJoin 没对上的行一起 过滤掉(那些行的被连列是 NULL),要保留它们就把条件写进 ON;ON 里不带表的裸列名指的是 被连模型的列。
  • 被连模型的软删条件和租户条件由框架写进 ON,List 在它上面看不到的行连接也看不到; ON 里可以再加条件,例如 AccountCols.Tier.Eq("gold");EqCol 两边先写谁都行,框架按表名分辨。
  • 分组投影里被连侧只能做分组键或 Min、Max、CountDistinct:组内每一行都对着同一 个被连行,Sum、Count、Avg 会把它按组内行数重复计入,直接拒绝。
  • 一张表只能作为一个来源:同一模型只能连一次,不能连自己,派生表连接也算在内;ClickHouse 没有唯一约束,模型连接在它上面直接报错。
  • Where 里的条件按列引用所属的表落到对应表上;引用了没连进来的表,构建期报错。列引用带着 它所属的表,这条规则在 List 那一侧同样生效:WithQuery 的过滤器、WithOrder、WithCursor 的排序和 UpdateByID 的赋值引用了别的模型,一律构建期报错,即便两个模型恰好有同名列。

一对多的关系用 JoinSelect:先把「多」的一侧按键分组成一条 Select,每个键只剩一行,再把它当 一张临时表连上来。主查询里直接传子投影的那个项,就能把它读成一列;对不上的行用 LeftJoinSelect 保留:

type tagsPerRecord struct {
    RecordID string
    Tags     int64
}
tags := TagCols.ID.Count().As("tags")
counts := database.Select[*appmodel.Tag, tagsPerRecord](ctx, TagCols.RecordID.Group(), tags)

type recordWithTags struct {
    ID   string
    Tags *int64 // 没有标签的记录对不上,这一列是 NULL
}
err := database.Select[*appmodel.Record, recordWithTags](ctx, RecordCols.ID, tags).
    Join(gst.LeftJoinSelect(counts, TagCols.RecordID.EqCol(RecordCols.ID))).
    Scan(&rows)
// SELECT `records`.`id` AS `id`, `j0`.`tags` AS `tags`
// FROM `records`
// LEFT JOIN (SELECT `record_id` AS `record_id`, COUNT(`id`) AS `tags`
//            FROM `tags` WHERE `tags`.`deleted_at` IS NULL GROUP BY `record_id`) AS `j0`
//   ON `j0`.`record_id` = `records`.`id`
// WHERE `records`.`deleted_at` IS NULL
  • 子投影必须是分组投影,ON 里的 EqCol 和常量等值要覆盖它的全部分组键,键通过子投影模型 的列引用来写;子投影自己不能带 OrderBy、Limit、Page。ON 里不能写 FilterExists: 临时表的行是子投影的组,没有可关联的行,这种条件写进子投影的 Where。
  • 子投影的分组键必须全是它自己模型的列,每列一次:它自己再连进来的表的列当不了键,主查询 没法用引用指到它;它从自己连的子投影里读来的项是临时表的列,不算键。
  • 主查询只能读子投影投影出来的项,传同一个项即可,改了别名就不是同一个项;子投影模型的 其他列不是临时表的列,ON 和 Where 里也只能用它的键列。要按子投影的度量筛行,条件写进子投影 的 Having;要筛它的行,写进子投影的 Where。子投影的项可以做主查询窗口的分区键和 排序项,行级、分组投影都行。读子投影的项,三步:子投影里把项赋给变量并 As 一个别名; 主查询的投影里再传这个变量;结果行按这个别名声明字段,用 LeftJoinSelect 时字段能装 NULL。
  • 主查询本身分组时,子投影的项会成为主查询的分组键,这只在主查询按连接列分组时才成立, 框架会检查,例如按账户分组再连每个账户的退款合计。主查询没有自己的度量时是行级读: 每条主表行带上子投影的项,时间桶只是每行的标签;要按桶汇总就加上度量。
  • 子投影读的表不能是主查询的表,也不能是已经连进来的表:临时表是通过子投影模型的列引用来 寻址的,同一张表出现两次就分不清。子投影自己再连进来的表不受此限。主查询自己能算出来的项 (不带表的 gst.Count()、排名函数,主表或主查询连入模型的项)子投影也投影了同一个、还顶着 默认别名时报 ErrDuplicateAlias,起了非默认别名的项当作读透;gst.Literal 常量无论别名都算 主查询自己的。要给每一行带上本表按某个维度的合计,用窗口 Sum().Over(PartitionBy(键)), 不用连接。子投影不能连到自己,也不能两两互连。
  • 子投影不能按时间桶分组后再连:桶是列的标签(如 2024-01-10),没有哪一列等于它,构建期报错。
  • 临时表由数据库物化,行数就是子投影的组数:条件写进子投影,物化得越少越好。
  • 派生表连接在 ClickHouse 上可用;那里的 LEFT JOIN 对不上的行也返回 NULL,框架连 ClickHouse 时开着 join_use_nulls。这是连接级的服务端设置,同一实例上业务自己写的 LEFT JOIN 对不上的行也会因此返回 NULL,而不是列的默认值。

派生表连接构建期报错时的改法:

报错 改法
ErrJoinSelectNotGrouped 子投影加分组键,Cols.X.Group()
ErrJoinSelectKey 子投影只按自己模型的列分组,每列一次
ErrJoinSelectBucketKey 不按时间桶分组后再连,同粒度的汇总改用窗口
ErrNestedSelectOrdered 子投影去掉 OrderBy、Limit、Page,它们属于主查询
ErrJoinSelectInstance 子投影用 SelectOn 开在主查询的实例上
ErrJoinNoCorrelation ON 里至少一对 EqCol 连到主查询或更早声明的来源:连到后面才声明的来源就调整声明顺序
ErrJoinNotUnique ON 用 EqCol、常量等值钉住子投影的全部分组键
ErrJoinSelectColumn 主查询只能读子投影投影出来的项,把同一个项(共享变量)再传一遍,别改它的别名,也别加 Over/Where;ON 和 Where 里只能用它的键列,别的条件写进子投影
ErrDuplicateAlias,两个子投影投了同一个项 给其中一个 As 别的别名
ErrDuplicateAlias,主查询自己能算的项子投影也投了且还顶着默认别名 想要主查询自己的:给它起个别名;想读子投影的:给子投影的项起个非默认别名,再把它传给主查询;两个都要就两边别名各不相同
ErrDuplicateAlias,主查询和子投影投了同一个 gst.Literal 常量 常量算主查询自己的,换个值或别名;要知道子投影有没有匹配到行,读它的键列,没匹配到的是 NULL
ErrDuplicateAlias,子投影的项加了 Over/Where 再传给主查询 开窗、加条件的项不再是子投影的项:想读子投影的就原样传;主查询自己要开窗、加条件就换个别名
ErrJoinSelectNotKeyed 主查询分组时按连接列分组;连接列是另一个子投影的键时,把那个键项也投影成分组键
ErrJoinDuplicateTable 一张表只能作为一个来源,同组合计用窗口;子投影不能连到自己或两两互连
ErrJoinMeasure 分组投影里被连模型的列只能做分组键或 Min、Max、CountDistinct
ErrWindowTermNotSelected 分区键用分组键或子投影的项,窗口排序用分组键或已投影的度量

框架不做递归 CTE,聚合能力也不向 URL 暴露: 报表口径属于服务端契约,让客户端自选分组键等于开放一个无界扫描入口。

调用其他 gst 服务

client 包是调用另一个 gst 服务的官方入口,也是接口测试发请求的入口。它按 DSL 的形态设计: Design() 能声明的每种 HTTP 接口在这里都有对应的调用方式,响应按统一信封解析;只走 gRPC 的 Stream 用生成的 gRPC 客户端调。

DSL 声明 client 入口
Create / Update / Patch / Delete / Get / List Post / Put / Patch / Delete / Get
Payload[*XxxReq]() 动词方法的 payload 参数
Result[*XxxRsp]() 动词方法的类型参数,从信封的 data 解码
Route("xxx/:id/action") 方法加路径组合
批量动作 BatchItems / BatchIDs 构造 /batch 路由的请求体
Export() / Import() Download / Upload
SSE 响应 Stream
分页、排序、展开、游标 WithPage / WithSortBy / WithExpand / WithCursor

一个客户端对应一个服务,带着地址、凭据和连接复用,可以并发使用,在包变量里建一次反复用即可; 路径、请求体和查询参数按调用传入:

cli, err := client.New("http://sample-service:8080",
	client.WithToken(token),
	client.WithTimeout(5*time.Second),
)
if err != nil {
	return err
}

// 默认 List:框架的列表参数和业务过滤都走请求选项。第一个参数是本次调用的
// 上下文,在 service 里就传 ctx 本身:调用方请求取消、超时,上游请求跟着结束
page, err := cli.Get[client.ListResult[model.Sample]](ctx, "/api/samples",
	client.WithPage(1, 20),
	client.WithQuery("status", "active"),
)

// 自定义动作:payload 对应 Payload,类型参数对应 Result
rsp, err := cli.Post[model.SampleSealRsp](ctx, "/api/samples/seal", &model.SampleSealReq{Reason: "audit"})

返回值分三种情况处理:调用成功得到解码后的响应;服务端拒绝(非 2xx)返回 *client.Error,带 HTTP 状态、消息和 trace_id,用 errors.As 取出后按状态决定 怎么处理;连接失败、超时这类传输错误是普通 error。需要信封本身(trace_id、响应 cookie)时用 Do。登录接口返回的 cookie 由客户端自动带到后续请求上。

两条注意事项:

  • 每个入口的第一个参数都是本次调用的上下文,它的截止时间和取消会结束这次请求。service 里传 ctx 本身,不要另起 context.Background();没有挂在请求上的调用(定时任务、常驻组件)传自己 收到的 ctx。WithTimeout 是客户端级别的兜底上限,和 ctx 的截止时间取先到者。
  • 请求带着 ctx 所属的追踪:OTEL 开启时注入 W3C 的 traceparent,并带上框架自己的 X-Trace-ID;ctx 不属于任何追踪(不在请求里,也不在定时任务或 leader 的一轮里)时两个头 都不带。上游沿用这个 trace_id,它的访问日志、SQL 注释和响应里的 trace_id 都是调用方 的那一个;client.Error 的 TraceID 也是它,排查时拿一个 id 就能把整条调用链查到底。

接口测试用的是同一个客户端:testutil.Run 启动整个应用,client.New(testutil.BaseURL()) 发请求, 上下文传 t.Context(),testutil.DecodeResp、testutil.RequireError 配合它断言,见 examples/demo/service/ping/list_test.go。

配置和迁移

config.ini.example 是新项目的默认配置模板。复制为 config.ini 后按环境修改。 默认模板会开启 sqlite,适合本地快速启动。SQLite 驱动依赖 cgo:用 CGO_ENABLED=0 构建的静态二进制照常编译, 但启用 SQLite 会在启动时报错,这类部署要换成 MySQL 或 PostgreSQL。

每个配置键都按"环境变量 > 配置文件 > 默认值"取值,配置文件里没写的键、configx 里 config.Register 注册的自定义段都一样。变量名是键名转大写、点换成下划线:server.port 对应 SERVER_PORT, logger.http_body.enabled 对应 LOGGER_HTTP_BODY_ENABLED,自定义段 Sample 的 endpoint 对应 SAMPLE_ENDPOINT。设成空串、false、0 同样生效,只有时长和 map 类型的键没有空的写法,设成空串启动失败。启动时这几种情况直接失败:变量的值转不成对应类型 (报错写明变量名和原值,例如 SERVER_PORT=tcp://…);自定义段和框架内置段重名(比如类型叫 Mysql); 两个类型注册成同一个段;自定义段的 default 标签写错(比如 bool 字段写成 default:"yes")。变量名写错、 或者只写到段名(比如 SERVER)都不会报错,也不生效,按上面的规则核对。

服务启动默认不自动建表、不自动迁移(database.auto_migrate = false):启动期只校验注册模型 的表是否存在,缺表直接报错退出并提示执行 gg migrate。gg new 生成的配置和 examples/demo 显式开启 auto_migrate = true,本地开发、测试环境按需开启即可;生产环境保持默认关闭, schema 变更一律走 gg migrate。

常用配置命令:

gg config list
gg config defaults --format ini
gg config defaults server --format yaml
gg config convert config.ini config.yaml
日志输出

logger.output 决定所有日志写到哪里,默认 stdout:

  • stdout:全局日志和访问、SQL、请求体、定时任务、选主、锁、各 provider 的日志都写进标准输出,一行一条, logger 字段标明来自哪一路(access、gorm、cronjob 等,即文件模式下的文件名去掉 .log;全局那一路取 logger.file 去掉 .log,没配时是 global)。不建文件、不轮转,Kubernetes 这类平台直接采集标准输出。
  • file:每一路写 logger.dir 下自己的文件,按 max_age、max_size、max_backups 轮转,适合由采集器读文件的部署。

写标准输出和写日志文件都先缓冲,最迟 1 秒写出,停机时写完;file 模式下 logger.file 没配或配成 /dev/stdout、/dev/stderr 时,全局那一路直接写、不缓冲。值写错启动直接失败。由采集器读日志文件的部署要写 output = file(或环境变量 LOGGER_OUTPUT=file),默认的 stdout 不落文件。

模型声明 Migrate() 后,字段变化先预览迁移计划:

gg migrate --dry-run

确认无误后执行:

gg migrate

命令会生成 generated/migrate/<dbtype>/schema.sql,并在执行前要求确认。 执行前先确认 config.ini 指向目标环境,避免把开发中的模型变化迁移到错误数据库。 用 SQLite 时迁移的是 sqlite.path 指向的库文件;内存库(sqlite.is_memory = true,默认值)每次启动都按模型建表, gg migrate 提示没有可迁移的内容后直接退出。

索引改名必须先迁移后发布

改索引名必须先执行 gg migrate 再发布新代码:先迁移则服务启动时按名命中、无额外动作。 若环境开启了 database.auto_migrate(本地开发、测试),先发布会让服务启动失败:启动期发现 同定义的索引还挂在旧名下,直接报错并给出改名语句,启动期不会 DROP 任何索引;生产环境默认 关闭 auto_migrate,先发布不会报错,但模型与库会持续漂移,直到执行 gg migrate 为止。

gg migrate 检测到疑似改名(同表索引有删有加)时会随迁移计划给出索引改名指引(按方言给语句: MySQL 是 RENAME INDEX,PostgreSQL 是 ALTER INDEX ... RENAME TO):确认列定义一致后手工执行 该语句(瞬时元数据操作),再重跑 gg migrate,对应删建项即消失。

表改名不要执行删表重建

改表名(TableName 返回值变化)时迁移计划会生成 DROP TABLE + CREATE TABLE,直接执行会 清空整表数据。只要新表列覆盖旧表全部列(改名不丢数据),gg migrate 就会随迁移计划给出表改名 指引(MySQL 是 RENAME TABLE,PostgreSQL 是 ALTER TABLE ... RENAME TO),并附上表名变化连带 的索引改名语句;同批的加列、索引调整会标注为剩余变更。手工执行改名语句后重跑 gg migrate, 剩余变更会以原地 ALTER 呈现。

内置模块

业务项目可以在 module/ 中注册内置模块。下面是注册 IAM 模块的形式:

package module

import "github.com/hydroan/gst/module/iam"

func init() {
	iam.Register()
}

应用入口会空导入 module,因此 init() 会在启动阶段执行。Register() 只注册模型和路由, 会话检查由项目自己挂:middleware/middleware.go 里 middleware.RegisterAuth(middleware.IAMSession()), 声明了 GRPC() 的项目再在 interceptor/interceptor.go 里 interceptor.RegisterAuth(interceptor.IAMSession()); 用 authz 模块时 middleware.Authz()、interceptor.Authz() 挂在它们之后(认证链按注册顺序跑);策略对象按路由清单的写法写, /api/records/{id}、/api/records/* 或静态路径两条线一致,写具体路径(/api/records/42)的只对 HTTP 生效,:id 这种写法哪边都不命中, 项目自己要读当前用户的中间件也排在会话检查之后。默认账号属于业务数据,框架不代为创建: 需要时由项目在启动钩子(如 router.OnRoutesReady)里通过标准数据库链写入,或者像 examples/demo 的测试那样走公开的 POST /api/signup 注册。

多副本部署

同一个项目可以起多个副本而不改代码。需要"整个部署只做一次"的能力都建在主库的一张租约表 gst_leases 上,不依赖 Redis 或 etcd;项目用上其中任何一种能力后,这张表随 gg migrate 一起建。

怎么选

按"这件事长什么样"选:

事情的样子 用什么
到点做,整个部署只做一次 cronjob.Register
到点做,每个副本各做一次:刷新本进程的缓存、上报本机状态 cronjob.RegisterPerInstance
一直跑的循环,同一时刻只能有一个副本在跑:轮询外部系统、转发待发消息 leader.Register
一直跑的循环,每个副本各跑一份:消费循环、监听变更 component.Register
由人或事件触发,同一时刻只能有一个在做 包变量里用 lock.New 声明,触发处调用 TryRun
结果只能发生一次:同一条记录不能处理两遍 数据库唯一约束或带状态条件的更新,上面几种都不保证

各自的用法:

  • cronjob.Register:表达式按 UTC 解释(按别的时区算要写 CRON_TZ= 前缀),@every 从 Unix 纪元起对齐, 所有副本算出同一组时刻;每个时刻整个部署只领一次,其他副本跳过;上一轮跑超时,期间到点的时刻直接跳过, 之后也不补;启动时补跑最近一个没人领过的时刻;被打断时 fn 把 ctx 的结束原样返回,这一轮会再跑,所以 fn 必须幂等。
  • cronjob.RegisterPerInstance:每个副本按同一个表达式各跑各的,不走租约,不补跑,被打断也不再跑。
  • leader.Register:任一时刻只有一个副本在跑 fn。fn 要一直跑到 ctx 结束,中途返回(有没有错误都算)就交出 这个名字,几秒后重新竞选、从头再跑。在跑的副本停机,别的副本约 6 秒内接手;进程崩溃的,最多约 21 秒。 接手后 fn 从头跑,进度要落库。每个名字单独竞选,不同名字可能落在不同副本上,没有"查询自己是不是主"的方法。
  • lock:在包变量里用 lock.New(name) 声明,启动之后再声明会 panic;在触发的地方调用 TryRun(ctx, fn)。 只试一次不等待,被占着返回 lock.ErrHeld,接口一般回 409,定时任务跳过即可;跑一半租约丢失返回 lock.ErrLost,fn 没报错也一样,因为别的持有者可能已经开始做同一件事;先拿锁、在 fn 里开事务,在已开的 事务里(包括模型钩子)调用直接返回 lock.ErrInTransaction,因为锁会在外层事务提交前放掉;fn 的 ctx 还跟着 调用方的 ctx 结束,请求断开时锁下的工作一起停。
  • component.Register:每个副本各跑一份,不做协调;播种之后、监听之前启动,停机时先于 provider 停下,fn 收到 的 ctx 在停机时结束。ctx 结束前返回(返回 nil 也算)或 panic 都按失败退出进程,暂时性的失败要在 fn 里重试。

leader、lock、component 包的 godoc 示例(example_*_test.go)逐个演示了这些用法和容易踩的坑。examples/cluster 把定时任务、选主和锁部署成三个副本,给了在 Kubernetes 里制造故障、核对结果的命令。

保证与边界

框架对这几种能力保证什么、管不到什么,逐条列在仓库根目录的 LEASE_PROTOCOL.md。写业务代码时最容易 出问题的几条:

  • fn 收到的 ctx 结束时必须停下:丢租约后 5 秒还不返回,进程立即以失败退出。
  • 只有在 fn 的 ctx 上用 database.Transaction / TransactionOn 开的事务会先核对租约;普通的 Create / Update / Delete 和外部调用都不核对,外部调用要幂等,关键写入要自带条件(唯一约束、按状态更新)。
  • 不要用 context.Background() 另起 ctx 写库:框架看不见它,gg check 的「Detached context」规则会拦。
  • 定时任务每个时刻至多跑两轮,fn 必须幂等;所有副本同时停机期间错过的调度,启动时只补最近一个。
  • 本地用 SQLite 开发时,fn 里单个事务控制在 8 秒内,连续的事务每个 5 秒内;内存 SQLite 不能用于多副本部署。
  • 副本必须直连主库或走会话级连接池;租约表的结构有变化时,先 gg migrate 再发布。

生成和检查命令

命令 用途
gg gen 根据 model DSL 生成注册文件、service action 文件和它们的测试骨架
gg gen --prune 生成后联动清理 model 不再需要的文件,和 gg prune 相同
gg gen ts 生成接口收发类型的 TypeScript 声明到 generated/typescript/,供前端复制使用
gg module copy <name> 将内置模块复制为业务项目本地源码(model、service、中间件,项目用 gRPC 时还有模块的 gRPC 拦截器),并删除框架源已移除的过时 model/service 文件(_test.go 与生成文件除外);gg module 各子命令的完整规则见 cmd/gg/MODULE.md
gg check 检查业务项目结构、命名、依赖边界和 tag 约束
gg prune 清理 model 不再需要的文件,只动 service/、middleware/、interceptor/ 和 pb/,删前问一次
gg routes 按 model 层级打印当前生成的接口路径
gg route-tree 按 URL 层级打印当前生成的路由树
gg migrate 生成当前数据库方言的 schema,预览并按确认执行数据库迁移

gg check 会检查依赖边界、model/service 文件边界、命名规范、json tag、 REQ/RSP 命名、业务项目根目录结构,以及声明了 GRPC() 的 model 的 protobuf 定义(pb tag、撞名、与 pb/ 下已提交契约的兼容)和它们的 service 有没有调用 只有 HTTP 才有的 ServiceContext 方法。gg gen 生成前也会执行这些检查(protobuf 定义那条由生成本身完成,不重复跑);检查失败会停止生成。

gg check、gg gen(含 gg gen ts)、gg routes 和 gg route-tree 读项目 代码时,跳过项目 Git 忽略规则排除的路径,也跳过 Go 工具链内置忽略的路径。 后者不是 gg 自定的名单,就是 go build ./... 本来就不看的那些(见 go help packages):名字以 . 或 _ 开头的文件和目录、vendor、 testdata、自带 go.mod 的子目录,以及 go.mod 里 ignore 声明的目录。 gg prune 认项目代码的方式也一样(找 model、判断 service 目录还有没有代码 在用),但删东西时这两类规则不保护任何路径,只认 prune.ignore,见下文。

生成 TypeScript 类型

gg gen ts 把 model Design() 中各路由收发的 Go 类型(请求 Payload、响应 Result 与模型本身) 生成为 TypeScript 声明,写到 generated/typescript/。目录结构镜像 model 目录:model/sample 生成 sample.ts,model/sample/item 生成 sample/item.ts,model 包自身生成 model.ts,路径里不重复 model 这一层;model 之外被模型引用到的类型(例如 pkg/notifier 里作为字段类型出现的结构)保留 自己的相对路径 pkg/notifier.ts,一眼能看出它不来自 model。另有一个以 app.name 命名的文件(未配置时用框架名 gst.ts),声明响应信封 Envelope<T>、默认列表结果 ListResult<T> 和默认批量请求体 ItemsPayload<T>、IDsPayload。

产物只有类型,文件之间用相对路径互相引用,不依赖任何 npm 包,也不限定请求库和前端框架:把整个 目录复制到前端项目即可使用。命令和 gg gen 一样先执行项目检查、应用 gst.yaml 的忽略规则;模型 变化后重新执行,再把目录复制给前端。

产物始终与 model 保持一致:模型删除、改名或被 gst.yaml 屏蔽后,上次为它生成的文件会被删除,空目录 一并清理;所有模型都没有了,整个 generated/typescript 目录也会消失。目录里不是 gg 生成的文件一律 不动:挡在产物路径上的直接报错,其余原样保留并在输出里提示。

类型按 encoding/json 的实际编码规则生成:

  • 字段名取 json tag;json:"-" 的字段不出现;嵌入结构体的字段展开到外层。
  • 指针、切片、map 字段写成 ?: T | null;带 omitempty 或 omitzero 的字段写成 ?:;其余字段必有。底层是切片或 map 的具名类型,声明本身就带 | null:它的 nil 值编码为 null,作为响应 Result 时 data 也可能是 null。
  • 数字一律是 number,超过 2^53 的 64 位整数在前端会丢失精度;带 ,string 选项的字段是 string。
  • 同包声明了常量的 string、整数类型生成字面量联合,常量注释写进类型注释,与 Swagger 一致;常量用 <<、| 等位运算定义的视为位标志,类型是 number;没有零值常量的枚举在使用处补 | "" 或 | 0。
  • time.Time、datatypes.Date 是字符串;any、json.RawMessage、datatypes.JSON 是 unknown。

JSON 形态由代码决定、无法从类型声明读出的写法,命令会列出全部位置后失败,不写任何文件:自定义了 MarshalJSON、MarshalText 等编码方法的类型(上面列出的类型除外)、项目内的泛型类型、带方法的接口、 channel 与函数字段、键不是字符串或整数的 map,以及 encoding/json 不认的 tag 名和 tag 选项。只定义了 UnmarshalJSON 等解码方法的类型照常按自身结构生成,即服务端返回它时的形态。

gg module add 或 module.Use 在运行期注册的模块路由不在生成范围内,它们收发的类型需要前端自行声明; gg module copy 复制进项目的模块和业务 model 一样会生成。

项目级配置 gst.yaml

在业务项目根目录(与 go.mod 同级)可放置可选的 gst.yaml,这是 gg 工具的 构建期工程配置,与运行时 config.ini 无关。

当前支持在 gg gen(含 gg module copy 后的重新生成)中忽略指定路由。 忽略只作用于生成的注册文件:router/router.gen.go 不注册路由、 service/service.gen.go 不注册 service,也不会为其生成新的 service 文件; 磁盘上已有的 service 文件(例如 module copy 拷贝来的)原样保留, gg gen --prune 和 gg prune 都不会把它们当作待删除文件,项目文件与 module copy 输出保持一致。适合屏蔽 module copy 带来的不需要的接口, 或把被框架模块占用的路径让给业务自己的实现:

gen:
  routes:
    ignore:
      /api/signup: [POST]
      /api/iam/admin/users/:id: [GET, DELETE]
      # 对象形式:from 限定只忽略声明在该目录下的 model,
      # 业务可在自己的 model 目录重新声明同一路由
      /api/iam/admin/users:
        methods: [GET]
        from: model/iam
  • 每个 path 写一次,值是要忽略的 HTTP method 列表;/api 前缀可省略, 因此路径可直接粘贴 gg routes 的输出(其路径不带 /api 前缀)。 参数段(:id)按位置匹配,不比较参数名。
  • 需要用自己的实现替换框架路由时,用对象形式加 from(如 model/iam) 把规则限定到框架模块目录;否则规则会把业务自己声明的同路径 action 一并 忽略。无 from 的规则命中多个 model 目录时会输出 warning 提醒。
  • 未匹配到任何路由的条目会在生成时输出 warning,提示配置可能已过期。
  • 忽略不影响 model 的 Migrate 注册:表结构照常创建,模块内部逻辑 (如登录查询用户表)不受影响。

gg prune(以及 gg gen --prune)只动 service/、middleware/、interceptor/ 和 pb/ 四个目录, 项目其他地方一个文件都不删、不改。它清理停用 action 的 service 文件连同配对的测试 文件、孤儿 service 目录、被删掉的复制模块留下的中间件和 gRPC 拦截器文件,以及 pb/ 下这次 gg gen 不会再写出的 .proto 文件、由它们编出来的 .pb.go、_grpc.pb.go 文件和对应的 .gen.go 适配文件 (model 去掉 GRPC()、model 被删或改了路径之后留下的),要删的先一次列出、问一次再删; prune.ignore 列出的路径一律跳过:

prune:
  ignore:
    - service/legacy           # 该目录及其下全部内容
    - service/record/list.go   # 只保护这一个文件
  • 每项是 service/、middleware/、interceptor/ 或 pb/ 下的一个路径(相对项目根目录),按目录层级匹配: service/legacy 覆盖该目录及其下全部内容,但不覆盖 service/legacyx; 写到具体文件就只保护这一个文件。它不是通配符,也不是正则。
  • 列出的路径在任何情况下都不会被删:停用 action 的 service 文件及其配对测试文件、 孤儿目录里的文件、清理后变空的目录、被删掉的复制模块留下的中间件和拦截器文件、 过期的 protobuf 文件都算。
  • 要保留的路径只能写进 prune.ignore:service/ 和 pb/ 归 gg 管,要保持干净,项目 的 Git 忽略规则和 Go 工具链的内置忽略不保护任何路径。被 Git 忽略的文件、 testdata、以 _ 开头的目录等和其他路径一样按规则清理;这些位置里的代码 也不算在用 service 目录,不会让它们留下来。
  • 不在 service/、middleware/、interceptor/ 或 pb/ 下、写法不规整或重复的条目直接报错;指向不存在路径的条目 在 prune 时输出 warning。
  • gg 只读取 gst.yaml,项目里如果还有 .gg.yaml、gst.yml 这类同类 文件,会输出 warning 提示它们不会被读取。

prune 删什么、不删什么、按什么顺序删、什么时候问你,完整规则见 cmd/gg/PRUNE.md。

示例

当前仓库的 examples/demo 是推荐阅读的完整业务项目示例:

examples/bench 是压测专用项目(由 gg new 生成),提供 BENCHMARK.md 中全部压测接口。

examples/cluster 是多副本部署示例:纯 YAML 清单在 Kubernetes 里起三个副本,演示并检验定时任务、选主、锁和多副本同时建表,见它的 README。

性能

框架不是瓶颈,有数据支撑(单机本地口径,完整压测协议、对照程序与 pprof 分析见 BENCHMARK.md):

  • 中间件链净开销为零:带完整中间件链(tracing、access log 落盘、body logger、CORS、recovery)的 ping 约 13.1w QPS,与同场裸 gin.New() 对照(约 12.5w)持平甚至略高。
  • 框架完整 CRUD 路径(dry run,无 DB I/O)10.5w-12w QPS:参数绑定、路由、钩子判定、SQL 构建、日志、响应序列化合计每请求不足 10µs。
  • 真实 MySQL 路径贴着往返数上限跑:单语句写(update/delete/updatebyid)3.6w-3.9w QPS 与单键读(get)同量级,每请求恰好一次 DB 往返。
  • pprof 定量实证:纯框架路径上应用层合计仅约 2% CPU,带 DB 路径上框架自有 CPU 份额 <5%、无单点热点,其余为网络 syscall、DB driver 与 runtime。

常见问题

什么时候用 model.Base,什么时候用 model.Empty?

需要数据库表、默认 CRUD、迁移和模型生命周期 hook 时使用 model.Base。只表示一个 动作、工具接口、登录跳转、批处理等非数据库接口时使用 model.Empty。

什么时候用 model.AutoBase?

数据库资源默认用 model.Base(UUIDv7 字符串主键)。写入量大、增长快、且不需要 对外暴露不可猜测 ID 的表(例如流水、明细类),可以改用 model.AutoBase 获得更窄 的自增整数主键和更小的二级索引。

什么时候需要 Service()?

默认 CRUD 没有额外业务逻辑时不需要。需要 hook、过滤、返回补充、复杂查询,或当前 action 是自定义动作时再开启 Service()。

为什么我写了 service 的 Create 方法但没有被调用?

如果 M、REQ、RSP 是同一个类型,默认资源 CRUD 会执行框架内置流程, 只调用 service hook 和过滤方法。要让 action 主方法被调用,需要用 Payload[T]() 或 Result[T]() 绑定当前接口专用的 REQ/RSP,让它成为自定义动作。 List、Get 只能通过 Result[T]() 触发自定义动作,请求类型固定为 *model.Empty。

Route 和 Endpoint 有什么区别?

Endpoint 是资源默认路径;Route 是额外路径或完全自定义路径。同一个 model 可以 同时声明默认资源路由和多个额外 Route。如果多个 Route 中有相同 phase 的 service,比如多个 Create,应各自 Service("name") 起名,避免生成文件冲突。

生成文件可以手改吗?

通常不要。main.go 和所有 .gen.go 文件(model/model.gen.go、 model/apidoc.gen.go、service/service.gen.go、router/router.gen.go 等) 由 gg gen 维护。手写业务逻辑放在 model/**/*.go、service/**/*.go 和扩展目录。

如何确认接口路径?

修改 DSL 后运行:

gg gen
gg routes

如果想按 model 文件层级查看 model 和接口关系,可以运行:

gg routes --model

需要排查生成的请求、响应和路径参数绑定时,可以运行:

gg routes --detail

如果只想查看认证或公开路由,可以加上 scope 过滤:

gg routes --scope auth
gg routes --scope pub

也可以启动服务后访问 Swagger 文档:

/docs/index.html
运维端点不做认证

/-/healthz、/-/readyz、/metrics、/openapi.json、/docs/* 这一类端点,以及 gRPC 监听上的 健康与反射服务,不是业务接口,不走 /api 那套认证,框架也不为它们提供口令——框架发行的默认口令保护不了任何东西,只会让人 误以为这里有防护。

保护它们是部署的事。在 Kubernetes 下这件事由拓扑决定,而不是由配置决定:指标抓取直接访问 Pod 端口,Ingress 只转发写进规则的路径——只转发 /api 前缀,这一类端点在集群外就不可达, 不需要谁额外记住一条规则。反过来,把整个端口挂到公网地址上,等于把下面这些一起发布出去:

端点 暴露什么
/-/healthz、/-/readyz 进程是否存活、是否正在停机,此外没有别的
/metrics 已被访问过的路由(gin 路由模式)及其请求数与延迟分布、缓存计数器上的数据库表名、进程内存与 CPU、构建信息;框架自己的指标名以 gst_backend_ 开头、进程指标以 gst_process_ 开头,gRPC 的是 grpc-go 生态默认的 grpc_server_
/openapi.json 本服务注册的全部路由,以及每个路由的请求模型、成功响应与会答的失败状态
/docs 同一份文档的 Swagger UI 渲染;页面资源编译进二进制,不从任何 CDN 加载脚本,离线可用
gRPC 的 grpc.health.v1.Health 与反射服务 进程和每个服务是否在服务,以及注册了哪些服务和消息;不经过项目挂的任何拦截器,Register 的通用拦截器和 RegisterAuth 的认证拦截器都不经过
为什么 gg gen 之后 go test 是红的?

gg gen 每新建一个 service 文件,就在旁边生成同名的 _test.go 骨架(外部测试包): 测试的第一行是一句 t.Fatal,下面是用框架 client 包按这个接口的方法、路由和请求、 响应类型写好的示例请求(流式动作则是用生成的 gRPC 客户端调它的 rpc);删掉第一行, 示例就是一个能跑的测试,再按业务补断言。包里还没有 TestMain 时,再生成一个只声明 TestMain 的 main_test.go,用 testutil.Run 起默认的测试 服务器(sqlite,不需要容器);需要 MySQL、Redis 或播种数据的包,在它的 testutil.Server 上改一次。骨架引用 testify,新项目第一次生成骨架后先跑一次 go mod tidy。这些文件生成后 归项目维护,gg gen 不再改写;gg check 要求每个由 gg gen 生成的 service 文件都有配对的 测试文件,删掉骨架而不写测试会被它拦下。

为什么删除 action 后 service 文件还在?

gg gen 默认保留已有 service 文件,避免误删手写业务代码。确认旧文件不再需要后 运行 gg prune,或使用 gg gen --prune,清理规则见 cmd/gg/PRUNE.md。

为什么测试跑完后 gst-test-mysql-8-4 这些容器还在运行?

testutil 的测试容器按镜像版本全局共享:所有测试二进制挂到同一个固定名容器上 (gst-test-mysql-8-4、gst-test-redis-7-alpine、gst-test-postgres-17-alpine、 gst-test-clickhouse-clickhouse-server-24-8-alpine),隔离在容器内部完成——每个 测试二进制拿到自己的数据库(Redis 是自己的 DB index),结束时删库/清空;进程被 强杀漏掉的残留库会在下一次测试运行时按持有进程存活状态自动回收。容器本身刻意 常驻,让后续测试免掉容器冷启动。

  • 清理或重置容器:docker rm -f <容器名>,下次测试会自动重建。修改容器启动参数 (如连接数上限)后也要先这样删掉旧容器才会生效。
  • 需要每个测试二进制独占容器时(远程 docker daemon、容器被折腾坏、需要全新实例 调试),设置 GST_TEST_DEDICATED_CONTAINERS=1 切换到独占模式,容器随测试结束销毁。

Documentation

Overview

Package gst defines the public contracts between the framework and business projects: the Model, Service, Database, Selector, Cache, RBAC, and Logger interfaces, the query building blocks they exchange (Filter, Order, Cursor, Column, Term, Window), the per-request ServiceContext, and the Error a request is answered with.

Index

Constants

View Source
const (
	FilterOpEq         = types.FilterOpEq         // equal: column = value
	FilterOpNe         = types.FilterOpNe         // not equal: column <> value
	FilterOpGt         = types.FilterOpGt         // greater than: column > value
	FilterOpGte        = types.FilterOpGte        // greater than or equal: column >= value
	FilterOpLt         = types.FilterOpLt         // less than: column < value
	FilterOpLte        = types.FilterOpLte        // less than or equal: column <= value
	FilterOpIn         = types.FilterOpIn         // set membership: column IN (comma-separated values)
	FilterOpNotIn      = types.FilterOpNotIn      // set exclusion: column NOT IN (comma-separated values)
	FilterOpLike       = types.FilterOpLike       // substring match: column LIKE %value%
	FilterOpNotLike    = types.FilterOpNotLike    // substring exclusion: column NOT LIKE %value%
	FilterOpStartsWith = types.FilterOpStartsWith // prefix match: column LIKE value% (can use an index)
	FilterOpEndsWith   = types.FilterOpEndsWith   // suffix match: column LIKE %value
	FilterOpIsNull     = types.FilterOpIsNull     // null check: value true means IS NULL, false means IS NOT NULL
)

URL-exposed operators, which a request spells as "field[op]=value".

View Source
const (
	FilterOpRegex        = types.FilterOpRegex        // regular expression match: column REGEXP value (dialect-aware)
	FilterOpNotRegex     = types.FilterOpNotRegex     // regular expression exclusion: NOT (column REGEXP value)
	FilterOpJSONContains = types.FilterOpJSONContains // JSON array membership: value is a member of the JSON array column
	FilterOpOr           = types.FilterOpOr           // group: the []Filter value is OR-combined, the group itself AND-combined
	FilterOpAnd          = types.FilterOpAnd          // group: the []Filter value is AND-combined, for nesting inside an OR group
	FilterOpExists       = types.FilterOpExists       // correlated subquery: EXISTS or NOT EXISTS over a related model
	FilterOpEqCol        = types.FilterOpEqCol        // column equals another column: the enclosing query's inside a subquery, a table read beside it inside a join
	FilterOpFalse        = types.FilterOpFalse        // constant predicate: matches nothing, see FilterFalse
)

Service-only operators, which service code builds and no request can spell.

View Source
const DefaultCountAlias = types.DefaultCountAlias

DefaultCountAlias is the alias COUNT(*) projects under when the caller does not rename it. A column term defaults to its column name, but COUNT(*) names no column, so without a default of its own it would be the one term that always had to be renamed.

Variables

View Source
var (
	// NewError creates the error a request is answered with: status and a
	// client-safe message.
	//
	// The status must be a 4xx or 5xx HTTP status code. Invalid statuses, including
	// 2xx/3xx success or redirect statuses such as http.StatusOK, are normalized to
	// http.StatusInternalServerError and the provided message is discarded.
	NewError = types.NewError

	// NewErrorWithCause creates the error a request is answered with, carrying
	// an internal cause.
	//
	// The status must be a 4xx or 5xx HTTP status code. Invalid statuses, including
	// 2xx/3xx success or redirect statuses such as http.StatusOK, are normalized to
	// http.StatusInternalServerError and the provided message is discarded.
	//
	// The cause is reported by Error for logs and available through Unwrap, but
	// is never exposed as the response message.
	NewErrorWithCause = types.NewErrorWithCause
)

The constructors are forwarded as variables instead of wrapper functions on purpose: a wrapper function would add its own frame on top of the stack trace captured at the construction site.

View Source
var ErrEntryNotFound = types.ErrEntryNotFound

ErrEntryNotFound is returned when a cache entry is not found, or when the stored value cannot be decoded as the handle's type and the entry is dropped.

View Source
var ErrTTLNotSupported = types.ErrTTLNotSupported

ErrTTLNotSupported is returned by Cache.Set when the backend cannot honor the requested ttl semantics, such as a per-entry lifetime on a backend without per-entry expiration.

Functions

func RequestUserID

func RequestUserID(ctx context.Context) string

RequestUserID reports the authenticated subject of the request ctx descends from, or "" when no request is behind it.

Types

type AnyColumnRef

type AnyColumnRef = types.AnyColumnRef

AnyColumnRef is the type-erased view of every generated column reference, for options that take a heterogeneous column list: WithSelect accepts columns of different Go types in one call, which the parameterized ColumnRef cannot express. The unexported method keeps the set of implementations closed to the framework, so a stray type that happens to carry a Name method cannot slip into a column list.

type Assignment

type Assignment = types.Assignment

Assignment is one column-value write, the unit UpdateByID accepts. Service code builds assignments through the generated column references (SampleCols.Status.Set(v)), whose typed front end stops a wrong-typed value or a misspelled column at compile time; generic code assigns through a reference minted for its type parameter. Its fields are unexported, so those references are the only way to build one; Table, Column and Value read it back.

type Bound

type Bound[T any] = types.Bound[T]

Bound is one end of the range a column's comparison filters confine it to: the value at that end, whether the range includes it, and whether any filter gave that end at all. An absent end leaves the range open on that side.

type Cache

type Cache[T any] = types.Cache[T]

Cache provides a typed key/value cache abstraction.

type Column

type Column[T any] = types.Column[T]

Column is a typed reference to a database column, generated per model by gg gen. T is the Go type of the column, so a filter built through a Column cannot name a column that does not exist nor bind a value of the wrong type: both mistakes stop at compile time instead of surfacing as a SQL error or a silently wrong result set.

func NewColumn

func NewColumn[M TableNamer, T any](name string) Column[T]

NewColumn returns a typed reference to the named column of M's table. gg gen emits the calls in each model's generated file, naming the model as the first type argument, so the table comes from the model's own TableName and is never restated as a literal. Handwritten code, model hooks included, reads those generated Cols vars; gg check flags project code that mints a reference instead, with two exceptions. Generic code has no concrete model and so no Cols var: it names its type parameter as the model, and NewColumn[M, string]("id").In(ids...) keeps the value type checked where a plain column name would not. Module sources have no generated file and name their model the way gg gen does. The fields are unexported so a shared reference cannot be repointed at another column after construction.

type ColumnRef

type ColumnRef[T any] = types.ColumnRef[T]

ColumnRef is the shared typed view of every generated column reference. Helpers that accept a column take this interface rather than a concrete struct, because embedding is not subtyping in Go: NumericColumn[T] cannot be passed where Column[T] is expected, so a helper typed on the struct would reject exactly the numeric and time columns it is most often used with.

type ControllerConfig

type ControllerConfig[M Model] = types.ControllerConfig[M]

ControllerConfig customizes how router.Register builds an internal handler for a route. It is the public configuration surface for controller behavior; the concrete controller handlers and their runtime state remain framework-owned.

type Cursor

type Cursor = types.Cursor

Cursor is where a cursor-paginated read starts and which way it goes: the feed's stable ordering, the boundary row, and whether the read travels along that ordering or back down it. Its fields are unexported, so a cursor comes from CursorForward, CursorBackward or the framework's URL parsing; Order, Value and Backward read it back.

func CursorBackward

func CursorBackward(order Order, value string) Cursor

CursorBackward pages against order, starting just before value.

func CursorForward

func CursorForward(order Order, value string) Cursor

CursorForward pages along order, starting just past value.

type Database

type Database[M Model] = types.Database[M]

Database defines the model-scoped database operation contract. It provides CRUD operations, query builders, and optional dry-run behavior for a single Model type.

type DatabaseOption

type DatabaseOption[M Model] = types.DatabaseOption[M]

DatabaseOption provides chainable options for a single Database operation chain. Options apply to the next terminal operation and are reset afterward. Start a new chain with database.Database[M](ctx) for each independent operation.

type Decision

type Decision = types.Decision

Decision is the outcome of one authorization check.

type ESDocumenter

type ESDocumenter = types.ESDocumenter

ESDocumenter represents a document that can be indexed into Elasticsearch. Types implementing this interface should be able to convert themselves into a document format suitable for Elasticsearch indexing.

type Error

type Error = types.Error

Error is the error a request is answered with: a status and a client-safe message, with an internal cause for the logs. A service method, a model hook, a middleware or an interceptor returns it to refuse a request with a status of its own; any other error is the server's failure, answered 500.

type Expr

type Expr = types.Expr

Expr is what a projection selects and a window partitions by: a column reference, projected as it is stored, or a Term. The set is closed to the framework, so a projection can never carry SQL text.

type Filter

type Filter = types.Filter

Filter is one field-level filter to apply as an AND condition: the column it compares, the table that column belongs to, the operator and the value. Its fields are unexported, so a filter comes from the generated column references, a reference minted for a type parameter, the grouping, subquery and constant constructors below, or the framework's URL parsing. Table, Column, Op and Value read it back; Split, Values, ExcludedValues and Bounds on a column reference read one column's filters converted to the column's type. A filter carrying another table is applied to that table when the query joins it and fails closed otherwise, and the value is always bound as a statement parameter.

func FilterAnd

func FilterAnd(filters ...Filter) Filter

FilterAnd groups filters that are AND-combined with each other. Filters are already AND-combined at the top level, so the group exists to nest an AND inside an OR group.

func FilterExists

func FilterExists[C Model](filters ...Filter) Filter

FilterExists matches rows of the queried model that have at least one related row in C satisfying filters. EqCol predicates tie the related rows to the queried row, one per column pair, next to the ordinary conditions narrowing them.

func FilterFalse

func FilterFalse() Filter

FilterFalse matches nothing. It is the condition a permission hook returns when the caller may see no row at all. Unlike an empty filter list it is a real condition, so it disables the empty-query safety check; unlike a filter the renderer cannot apply it is deliberate, so nothing is logged. It renders as 1 = 0 on every dialect and composes like any other filter, inside groups, subqueries and conditional measures included.

func FilterNotExists

func FilterNotExists[C Model](filters ...Filter) Filter

FilterNotExists matches rows that have no related row in C satisfying filters. Note that it is not the negation of a filtered FilterExists over the same rows: a row whose related rows all fail filters matches, and so does a row with no related rows at all. A subquery without any EqCol predicate fails closed here as well: negating "match nothing" would otherwise widen into "match everything".

func FilterOr

func FilterOr(filters ...Filter) Filter

FilterOr groups filters that are OR-combined with each other. The group as a whole stays AND-combined with every other condition of the query, so a mandatory condition such as tenant scoping can never be absorbed into the alternatives.

type FilterOp

type FilterOp = types.FilterOp

FilterOp is a field-level filter operator: the comparison a Filter applies, which Filter.Op reads back. Operators never widen a query: unknown values are rejected during parsing, and the database layer fails closed on conditions it does not recognize.

type JoinSource

type JoinSource = types.JoinSource

JoinSource is a source a select joins to its model. The set is closed to the framework: Join and LeftJoin join a model on a unique key, JoinSelect and LeftJoinSelect join a grouped select on its group keys.

func Join

func Join[C Model](on ...Filter) JoinSource

Join joins model C on a unique key: JOIN, keeping only the rows of the query that match a row of C. The predicates are the ON condition.

func JoinSelect

func JoinSelect[R any](sub SelectBranch[R], on ...Filter) JoinSource

JoinSelect joins a grouped select as a derived table, JOIN (SELECT ...) AS jN ON ..., keeping only the rows of the query that match one of its groups. This is how a one-to-many relation is read beside its one side: the many side is grouped by the key first, so every key has one row, and that row is joined.

func LeftJoin

func LeftJoin[C Model](on ...Filter) JoinSource

LeftJoin joins model C on a unique key, keeping the rows of the query that match no row of C with the joined columns NULL: LEFT JOIN. The rules match Join; the result fields the joined columns bind to must hold NULL.

func LeftJoinSelect

func LeftJoinSelect[R any](sub SelectBranch[R], on ...Filter) JoinSource

LeftJoinSelect joins a grouped select as a derived table, keeping the rows of the query that match none of its groups with the select's terms NULL: LEFT JOIN. The rules match JoinSelect; the result fields the select's terms bind to must hold NULL.

type Logger

type Logger = types.Logger

Logger is the logger the framework hands to services and modules and keeps in the logger package's streams. It writes an entry plain and printf-style, sugared with key/value fields (the "w" methods) and with typed zap.Field values (the "z" methods); With attaches string key/value fields and WithContext derives a logger carrying request metadata fields.

type Model

type Model = types.Model

Model defines the framework contract for database-backed and action models. Typical database resources embed model.Base (UUIDv7 string primary key) or model.AutoBase (auto-increment integer primary key). Action-only models may use model.Empty when they do not represent persistent rows.

type Module

type Module[M Model, REQ Request, RSP Response] = types.Module[M, REQ, RSP]

Module describes a registered API module: route metadata, auth exposure, resource parameter name, and the service implementation used by controllers.

type NumericColumn

type NumericColumn[T any] = types.NumericColumn[T]

NumericColumn is the reference generated for a column whose Go type is numeric. It embeds Column, so every filter and order stays available, and adds the aggregate functions that only make sense over a number.

func NewNumericColumn

func NewNumericColumn[M TableNamer, T any](name string) NumericColumn[T]

NewNumericColumn returns the numeric reference to the named column of M's table, carrying Sum and Avg on top of everything Column has.

type Order

type Order = types.Order

Order is one ORDER BY term: a column and the direction to sort it by. Its fields are unexported, so an order comes from a generated column reference, a reference minted for a type parameter, or the framework's URL parsing; Table, Column and Descending read it back, and SortsBy tells whether it sorts by a given column. Table is filled in by a column reference and left empty by URL parsing. The chain's reads and a select check it: a select that joins tells two tables' columns of one name apart by it, and a chain refuses an order of another model. A union orders its result columns by name and reads no table, and WithExpand orders the associated table, so neither checks it. An Order with an empty column is skipped rather than rendered.

type Ordering

type Ordering = types.Ordering

Ordering is what the OrderBy methods of a select, a window and a union accept: an Order sorting by a column reference, or a TermOrder sorting by a projection term. The set is closed, so an ordering can never carry SQL the way a free-form string could.

type Permission

type Permission = types.Permission

Permission is one operation a role is allowed to perform on one object. It is the unit the whole-set replacement methods on RBAC take, so a caller states a role's permissions as a set rather than as a sequence of grants.

type QueryOptions

type QueryOptions = types.QueryOptions

QueryOptions tunes how WithQuery turns a model value into WHERE conditions. Every condition it produces is AND-combined; the zero value means exact matching with the empty-query safety check enabled. See the WithQuery method for usage examples.

type RBAC

type RBAC = types.RBAC

RBAC provides tenant-scoped role, permission, and subject assignment operations. A process holding no policy set — RBAC disabled, or not initialized — answers reads as the deployment they describe, denying every request and reporting no roles, and refuses every write rather than reporting a change it did not make.

type Request

type Request = types.Request

Request and Response are the framework-facing types of one action's request and response payloads. They constrain the REQ and RSP type parameters of Service and Module; the concrete types are declared per action by the model layer.

type Response

type Response = types.Response

Request and Response are the framework-facing types of one action's request and response payloads. They constrain the REQ and RSP type parameters of Service and Module; the concrete types are declared per action by the model layer.

type SQLStatement

type SQLStatement = types.SQLStatement

SQLStatement contains a generated SQL statement in executable and rendered forms.

type SelectBranch

type SelectBranch[R any] = types.SelectBranch[R]

SelectBranch is a select in the role of a branch of a union: every Selector is one, with its model type erased, so that selects over different models stack into one result as long as they scan into the same row type. The role is what UnionAll takes. Only the selects the database layer builds can fill it; UnionAll fails when handed anything else, a union among them.

type Selector

type Selector[M Model, R any] = types.Selector[M, R]

Selector runs an analytical read over the table of M and scans the result rows into R. It is deliberately separate from Database[M]: a projected row is not a model row, so model hooks, association preloading and cursor pagination have nothing to act on and are absent here rather than present and inert.

type Service

type Service[M Model, REQ Request, RSP Response] = types.Service[M, REQ, RSP]

Service defines the controller-facing business operation contract for a model. Generated controllers call these methods for CRUD, batch CRUD, lifecycle hooks, import/export, filtering, and logging.

type ServiceContext

type ServiceContext = types.ServiceContext

ServiceContext is the per-request context the framework hands to every service method. It implements context.Context by delegating to the request context, exposes request metadata (route, params, user identity, trace), and carries the response helpers a service needs without touching Gin directly.

type TableNamer

type TableNamer = types.TableNamer

TableNamer is the one method a column reference needs from its model: the table the column belongs to. Every Model satisfies it; the narrower interface lets a reference name its model as a type argument alone, with nothing else of the model contract in play.

type Term

type Term = types.Term

Term is one term of a projection: a group key, a plain column, a constant, a measure, or a window function. Column references build it, as do Count, the ranking functions and Literal below.

func Count

func Count() Term

Count counts rows: COUNT(*). It counts a row even when every column is NULL, which is what a plain row count means; use a column reference's Count for COUNT(column), which skips NULLs.

func DenseRank

func DenseRank() Term

DenseRank ranks like Rank without skipping: 1, 2, 2, 3.

func Literal

func Literal(value string) Term

Literal projects a constant, which is how the branches of a union tell their rows apart.

func Rank

func Rank() Term

Rank ranks the rows of each partition in the window's order. Rows sorting equal share a rank and the next rank skips past them: 1, 2, 2, 4.

func RowNumber

func RowNumber() Term

RowNumber numbers the rows of each partition from 1 in the window's order, with no ties: two rows sorting equal still get consecutive numbers, in a stable order the framework completes with the primary key. Like Rank and DenseRank it only exists over a window whose OrderBy is set, which Over declares: without an order there is no first row to number.

type TermCondition

type TermCondition = types.TermCondition

TermCondition is one condition on a projected term: a Having condition on a measure, or a Qualify condition on a window function. It carries the term itself rather than an alias string, which has two consequences: a condition can never name a term the projection did not declare, and the renderer can emit the full expression instead of the alias, which HAVING requires because PostgreSQL does not accept an output alias there.

type TermOrder

type TermOrder = types.TermOrder

TermOrder is one ORDER BY term of a select or of a window. Unlike Order it sorts by a projection term, which is what a TopN report ranks by.

type TimeColumn

type TimeColumn = types.TimeColumn

TimeColumn is the reference generated for a column holding a time value: time.Time, or datatypes.Date, the calendar day a date column stores and binds as a time at midnight. It embeds Column and adds time bucketing, which is only meaningful over a time value and produces garbage rather than an error on some dialects when it is not.

func NewTimeColumn

func NewTimeColumn[M TableNamer](name string) TimeColumn

NewTimeColumn returns the time reference to the named column of M's table, carrying the bucketing group keys on top of everything Column has.

type Union

type Union[R any] = types.Union[R]

Union stacks the rows of several selects into one result: UNION ALL, the one set operation the framework offers. UNION proper would fold two rows that happen to be equal — two payments of the same amount on the same day — into one, which no report wants; INTERSECT and EXCEPT are the semi joins FilterExists and FilterNotExists already express.

type Window

type Window = types.Window

Window names the rows a window function reads for each row: PartitionBy splits the rows into partitions, and OrderBy orders each partition, which is what gives a running total its direction and a row number its sequence.

func OrderBy

func OrderBy(orders ...Ordering) Window

OrderBy returns a Window with no partition and the given orders: the window a ranking across every row reads. It is the short spelling of PartitionBy().OrderBy(orders...), the two building the same window; a window with keys starts from PartitionBy. It orders the window, not the result: the result is ordered by the Selector's OrderBy.

func PartitionBy

func PartitionBy(keys ...Expr) Window

PartitionBy opens a window partitioned by keys. Without keys the whole result is one partition, which is what a ranking over every row wants, and the window is the one OrderBy opens: the two spellings build the same value, so a term declared with one is found by the other.

Directories

Path Synopsis
Package apidoc keeps a registry of Go struct doc comments used to enrich the generated OpenAPI document.
Package apidoc keeps a registry of Go struct doc comments used to enrich the generated OpenAPI document.
Package authn exposes the authentication extension points that optional modules install into the mandatory IAM login flow.
Package authn exposes the authentication extension points that optional modules install into the mandatory IAM login flow.
jwt
authz
rbac
Package rbac decides authorization from stored policy rules and keeps those rules in step with the records they are derived from.
Package rbac decides authorization from stored policy rules and keeps those rules in step with the records they are derived from.
Package bootstrap brings a gst process up and takes it down again.
Package bootstrap brings a gst process up and takes it down again.
Package cache is the public facade of the framework's in-memory cache.
Package cache is the public facade of the framework's in-memory cache.
Package client is the official HTTP client for gst backends, designed as the client-side pairing of the framework's DSL: every HTTP interface shape a model's Design() can declare has a first-class counterpart here; the Stream action is served over gRPC alone and has none.
Package client is the official HTTP client for gst backends, designed as the client-side pairing of the framework's DSL: every HTTP interface shape a model's Design() can declare has a first-class counterpart here; the Stream action is served over gRPC alone and has none.
cmd
gg command
Package component runs a project's own long-running work alongside the server — a consumer loop, a poller, a watcher: work that runs on every replica for the life of the process.
Package component runs a project's own long-running work alongside the server — a consumer loop, a poller, a watcher: work that runs on every replica for the life of the process.
Package config loads the application configuration: the framework's own sections, held by App, and the sections a project adds with Register.
Package config loads the application configuration: the framework's own sections, held by App, and the sections a project adds with Register.
Package consts holds the names a project shares with the framework: the phases of an action, the row lock modes a query asks for, the operations an operation log records, the context keys of the caller, the authorization vocabulary, and the rule of the path of a route.
Package consts holds the names a project shares with the framework: the phases of an action, the row lock modes a query asks for, the operations an operation log records, the context keys of the caller, the authorization vocabulary, and the rule of the path of a route.
Package cronjob runs the jobs a project registers on a schedule.
Package cronjob runs the jobs a project registers on a schedule.
Package database provides the framework database facade built on top of GORM.
Package database provides the framework database facade built on top of GORM.
Package dbmigrate renders registered Go models into a target schema and migrates a database towards it.
Package dbmigrate renders registered Go models into a target schema and migrates a database towards it.
Package dcache provides a replicated in-memory cache: a per-process store whose set and delete operations propagate to the store of every other instance through Kafka events.
Package dcache provides a replicated in-memory cache: a per-process store whose set and delete operations propagate to the store of every other instance through Kafka events.
debug
pprof
Package debugpprof provides an HTTP server for pprof endpoints; the package name avoids conflicting with the standard library "pprof" packages.
Package debugpprof provides an HTTP server for pprof endpoints; the package name avoids conflicting with the standard library "pprof" packages.
ds
list/arraylist
Package arraylist provides a generic implementation of a resizable array-backed list.
Package arraylist provides a generic implementation of a resizable array-backed list.
list/linkedlist
Package linkedlist provides an implementation of a doubly-linked list with a front and back.
Package linkedlist provides an implementation of a doubly-linked list with a front and back.
Package dsl provides a Domain Specific Language (DSL) for defining REST API designs for Go models.
Package dsl provides a Domain Specific Language (DSL) for defining REST API designs for Go models.
Package grpc is the gRPC listener's API for a project's own code.
Package grpc is the gRPC listener's API for a project's own code.
Package interceptor is the gRPC counterpart of package middleware: the interceptors a project mounts on the gRPC listener.
Package interceptor is the gRPC counterpart of package middleware: the interceptors a project mounts on the gRPC listener.
internal
cache/cachetest
Package cachetest provides the conformance suite every types.Cache backend must pass.
Package cachetest provides the conformance suite every types.Cache backend must pass.
cache/capacity
Package capacity resolves the per-type entry bound shared by the entry-addressed cache backends, so the configuration is read and validated in one place rather than copied into each of them.
Package capacity resolves the per-type entry bound shared by the entry-addressed cache backends, so the configuration is read and validated in one place rather than copied into each of them.
cache/codec
Package codec encodes cache values for the backends that store bytes rather than live objects.
Package codec encodes cache values for the backends that store bytes rather than live objects.
cache/freelru
Package freelru is a sharded LRU cache that stores live values.
Package freelru is a sharded LRU cache that stores live values.
cache/otter
Package otter is a W-TinyLFU cache.
Package otter is a W-TinyLFU cache.
cache/registry
Package registry provides the shared per-type singleton store used by the in-memory cache backends.
Package registry provides the shared per-type singleton store used by the in-memory cache backends.
cache/ristretto
Package ristretto is a TinyLFU-admission cache.
Package ristretto is a TinyLFU-admission cache.
cache/tracing
Package tracing wraps cache backends that talk to remote systems with OpenTelemetry spans.
Package tracing wraps cache backends that talk to remote systems with OpenTelemetry spans.
cmd/apidocgen command
Command apidocgen regenerates the apidoc registration file of the framework package that ships struct doc comments to the OpenAPI generator.
Command apidocgen regenerates the apidoc registration file of the framework package that ships struct doc comments to the OpenAPI generator.
cmd/selfcheck command
Command selfcheck holds the framework's own source to the rules golangci-lint cannot express, and prints each violation it finds.
Command selfcheck holds the framework's own source to the rules golangci-lint cannot express, and prints each violation it finds.
cmd/testcachefix command
Command testcachefix writes back the test results go keeps under one key only, so a cached test never links its binary again.
Command testcachefix writes back the test results go keeps under one key only, so a cached test never links its binary again.
controller
Package controller serves the actions of the registered routes on both transports: over HTTP through the handlers the router mounts (CreateHandler and its kind), over gRPC through the call functions the generated pb package runs (CreateCall and its kind).
Package controller serves the actions of the registered routes on both transports: over HTTP through the handlers the router mounts (CreateHandler and its kind), over gRPC through the call functions the generated pb package runs (CreateCall and its kind).
dbruntime/dbnode
Package dbnode names the roles of the nodes a database handle writes to and reads from, and stamps the role that serves a statement on the statement's context, for the SQL log to report which node ran it.
Package dbnode names the roles of the nodes a database handle writes to and reads from, and stamps the role that serves a statement on the statement's context, for the SQL log to report which node ran it.
dsl
Package dsl reads the designs the models of a project declare with the keywords of the public dsl package: Parse turns a model file into the Design of each of its models, Validate reports the declarations the generator cannot honor, and Design and Action are what gg gen, gg check and the generators consume.
Package dsl reads the designs the models of a project declare with the keywords of the public dsl package: Parse turns a model file into the Design of each of its models, Validate reports the declarations the generator cannot honor, and Design and Action are what gg gen, gg check and the generators consume.
errorstack
Package errorstack extracts stack traces embedded in errors, shared by tracing and logging so both report the same error origin stack format.
Package errorstack extracts stack traces embedded in errors, shared by tracing and logging so both report the same error origin stack format.
execctx
Package execctx carries the identity of the unit of work a context belongs to.
Package execctx carries the identity of the unit of work a context belongs to.
ggcheck
Package ggcheck holds the rules gg check holds a business project to.
Package ggcheck holds the rules gg check holds a business project to.
ggconfig
Package ggconfig loads the project-level gst configuration file (gst.yaml) that gg commands consume at build time.
Package ggconfig loads the project-level gst configuration file (gst.yaml) that gg commands consume at build time.
ggconst
Package ggconst holds the conventions the framework's tooling shares: the import paths of the framework packages, the names of the files, packages and fields gg generates and reads, and the patterns it recognizes them by.
Package ggconst holds the conventions the framework's tooling shares: the import paths of the framework packages, the names of the files, packages and fields gg generates and reads, and the patterns it recognizes them by.
gggen
Package gggen builds the code gg gen writes: the registration files, the service files and their test scaffolds, and, in its sub-packages, the column references, the TypeScript declarations and the protobuf definitions.
Package gggen builds the code gg gen writes: the registration files, the service files and their test scaffolds, and, in its sub-packages, the column references, the TypeScript declarations and the protobuf definitions.
gggen/columns
Package columns generates the typed column references of a project's models, so filters name columns through the compiler instead of through string literals.
Package columns generates the typed column references of a project's models, so filters name columns through the compiler instead of through string literals.
gggen/jsonshape
Package jsonshape reads what the types of a gst project look like on the wire: the JSON encoding/json writes for them, which is the codec of the framework's response envelope and request binding.
Package jsonshape reads what the types of a gst project look like on the wire: the JSON encoding/json writes for them, which is the codec of the framework's response envelope and request binding.
gggen/jsonshape/fixture/a-b
Package ab lives in a directory whose name maps to the same TypeScript import name as the a_b fixture package.
Package ab lives in a directory whose name maps to the same TypeScript import name as the a_b fixture package.
gggen/jsonshape/fixture/a_b
Package ab lives in a directory whose name maps to the same TypeScript import name as the a-b fixture package.
Package ab lives in a directory whose name maps to the same TypeScript import name as the a-b fixture package.
gggen/jsonshape/fixture/gst
Package gst sits where its output file would be gst.ts, the name of the framework prelude.
Package gst sits where its output file would be gst.ts, the name of the framework prelude.
gggen/jsonshape/fixture/mode
Package mode declares an enum type another fixture package declares a further constant of.
Package mode declares an enum type another fixture package declares a further constant of.
gggen/jsonshape/fixture/model/record
Package record declares a fixture type the sample fixture refers to from another package.
Package record declares a fixture type the sample fixture refers to from another package.
gggen/jsonshape/fixture/model/sample
Package sample declares the fixture types the TypeScript generator tests start from.
Package sample declares the fixture types the TypeScript generator tests start from.
gggen/jsonshape/fixture/pkg/notifier
Package notifier declares a fixture type a model refers to from outside the model directory.
Package notifier declares a fixture type a model refers to from outside the model directory.
gggen/jsonshape/fixture/unsupported
Package unsupported declares fixture types whose JSON shape the TypeScript generator cannot describe.
Package unsupported declares fixture types whose JSON shape the TypeScript generator cannot describe.
gggen/pb
Package pb generates the protobuf definitions of a gst project's gRPC services and the Go files serving them: for every model whose Design declares GRPC(), the messages its Go types encode to and the service exposing its actions, printed as .proto files that mirror the model directory under pb/; beside each, a .gen.go with the type serving the service, the calls of its actions, the handlers of its rpcs and the conversions between the messages and the Go types (see handlerFile); and, in every package under pb/, a pb.gen.go registering its services on the listener (see registrationFiles).
Package pb generates the protobuf definitions of a gst project's gRPC services and the Go files serving them: for every model whose Design declares GRPC(), the messages its Go types encode to and the service exposing its actions, printed as .proto files that mirror the model directory under pb/; beside each, a .gen.go with the type serving the service, the calls of its actions, the handlers of its rpcs and the conversions between the messages and the Go types (see handlerFile); and, in every package under pb/, a pb.gen.go registering its services on the listener (see registrationFiles).
gggen/ts
Package ts generates the TypeScript declarations of the Go types a gst project's API routes send and receive.
Package ts generates the TypeScript declarations of the Go types a gst project's API routes send and receive.
gghelper
Package gghelper holds what more than one of gg's packages needs about the project gg runs in: the module path its go.mod declares, the paths gg leaves out of it, the ones its Git ignore rules exclude and the ones the go command leaves out, programs and package listings run against its module, and the small file and path helpers the commands share.
Package gghelper holds what more than one of gg's packages needs about the project gg runs in: the module path its go.mod declares, the paths gg leaves out of it, the ones its Git ignore rules exclude and the ones the go command leaves out, programs and package listings run against its module, and the small file and path helpers the commands share.
ggmodule
Package ggmodule implements the behavior behind the gg module command family: discovering framework modules (list), registering a module import in the project's module/module.go (add, remove), and materializing a module into the project as project-owned source (copy).
Package ggmodule implements the behavior behind the gg module command family: discovering framework modules (list), registering a module import in the project's module/module.go (add, remove), and materializing a module into the project as project-owned source (copy).
ggnew
Package ggnew scaffolds the project gg new creates: the first version of every package the generated main.go imports, so the project compiles before its first generation.
Package ggnew scaffolds the project gg new creates: the first version of every package the generated main.go imports, so the project compiles before its first generation.
ggprune
Package ggprune works out what gg prune deletes from a project's service directory, and deletes it: the service files of disabled actions, the unmanaged files of service directories no model owns, and the directories that leaves empty.
Package ggprune works out what gg prune deletes from a project's service directory, and deletes it: the service files of disabled actions, the unmanaged files of service directories no model owns, and the directories that leaves empty.
grpcserver
Package grpcserver serves the gRPC services of the models declaring GRPC(), on a listener of its own beside the HTTP one and on the same lifecycle: bootstrap starts it with the other listeners, drains it with the readiness probe and stops it side by side with the HTTP listener within the shutdown's window, its streams ending as the stop begins.
Package grpcserver serves the gRPC services of the models declaring GRPC(), on a listener of its own beside the HTTP one and on the same lifecycle: bootstrap starts it with the other listeners, drains it with the readiness probe and stops it side by side with the HTTP listener within the shutdown's window, its streams ending as the stop begins.
hookoverride
Package hookoverride tells whether a type overrides a method beyond the framework base it embeds, reading the answer off the compiled binary.
Package hookoverride tells whether a type overrides a method beyond the framework base it embeds, reading the answer off the compiled binary.
instance
Package instance answers one question — which process is this — for every part of the framework that tells replicas apart: the field every log entry carries, the tracing resource, the origin a replicated cache event names.
Package instance answers one question — which process is this — for every part of the framework that tells replicas apart: the field every log entry carries, the tracing resource, the origin a replicated cache event names.
lease
Package lease is the coordination primitive the framework's distributed capabilities stand on: a name that at most one healthy process among those sharing the primary database holds at a time.
Package lease is the coordination primitive the framework's distributed capabilities stand on: a name that at most one healthy process among those sharing the primary database holds at a time.
lifecycle
Package lifecycle is the registry of the framework components that have a lifetime of their own: clients of external systems, the scheduler, election loops, anything that owns a connection or a background goroutine — and, through the component package, a project's own long-running work.
Package lifecycle is the registry of the framework components that have a lifetime of their own: clients of external systems, the scheduler, election loops, anything that owns a connection or a background goroutine — and, through the component package, a project's own long-running work.
logfield
Package logfield declares the log fields more than one stream of the framework writes, each as one constructor fixing the field's key and its type.
Package logfield declares the log fields more than one stream of the framework writes, each as one constructor fixing the field's key and its type.
middleware
Package middleware holds the middleware the framework mounts itself and the machinery that mounts the rest: the built-in chain ahead of every route, the registry of the middlewares projects and modules register, and the registries the router fills as routes register — route parameters and streaming routes — together with the circuit breaker built from configuration at startup.
Package middleware holds the middleware the framework mounts itself and the machinery that mounts the rest: the built-in chain ahead of every route, the registry of the middlewares projects and modules register, and the registries the router fills as routes register — route parameters and streaming routes — together with the circuit breaker built from configuration at startup.
modelinfo
Package modelinfo reads the models of a project the way gg sees them: the model files under the model directory, each model with the design its DSL declares, its routes resolved through the resource hierarchy and the gst.yaml ignore rules, the service files its actions map to, and the doc comments the API document registers.
Package modelinfo reads the models of a project the way gg sees them: the model files under the model directory, each model with the design its DSL declares, its routes resolved through the resource hierarchy and the gst.yaml ignore rules, the service files its actions map to, and the doc comments the API document registers.
modelinspect
Package modelinspect inspects a project's models by running them.
Package modelinspect inspects a project's models by running them.
modelregistry
Package modelregistry contains the internal model infrastructure behind the public model package.
Package modelregistry contains the internal model infrastructure behind the public model package.
modelschema
Package modelschema resolves what a model struct declares about its table: its database columns, the secondary indexes it declares (see Index), its optimistic-locking column (see Version) and the columns identifying one of its rows (see IdentifyingColumns).
Package modelschema resolves what a model struct declares about its table: its database columns, the secondary indexes it declares (see Index), its optimistic-locking column (see Version) and the columns identifying one of its rows (see IdentifyingColumns).
router
Package router builds the HTTP server a gst process serves: the engine carrying the framework's middleware chain and operational endpoints, the route groups registered routes attach to, the routes-ready hooks, and the server's start and shutdown.
Package router builds the HTTP server a gst process serves: the engine carrying the framework's middleware chain and operational endpoints, the route groups registered routes attach to, the routes-ready hooks, and the server's start and shutdown.
serviceregistry
Package serviceregistry owns the framework-internal service registry used by controllers and modules.
Package serviceregistry owns the framework-internal service registry used by controllers and modules.
sse
Package sse implements Server-Sent Events (SSE) for the framework.
Package sse implements Server-Sent Events (SSE) for the framework.
structdoc
Package structdoc parses Go source code and extracts the doc comments of exported struct declarations and their exported fields.
Package structdoc parses Go source code and extracts the doc comments of exported struct declarations and their exported fields.
testutil/oteltest
Package oteltest turns real OpenTelemetry tracing on inside one framework test and reads back the spans it exports.
Package oteltest turns real OpenTelemetry tracing on inside one framework test and reads back the spans it exports.
testutil/swap
Package swap replaces process-wide values for the duration of one test.
Package swap replaces process-wide values for the duration of one test.
testutil/testcontainer
Package testcontainer prepares the services the framework's tests run against, in containers started through testcontainers-go.
Package testcontainer prepares the services the framework's tests run against, in containers started through testcontainers-go.
testutil/testlog
Package testlog keeps the framework's logs of a test process out of the test output and out of the package source tree, where log files changing with every run would make go's test cache rerun every test that reads or lists the tree.
Package testlog keeps the framework's logs of a test process out of the test output and out of the package source tree, where log files changing with every run would make go's test cache rerun every test that reads or lists the tree.
types
Package types defines the contracts between the framework and business projects: the Model, Service, Database, Selector, Cache, RBAC, and Logger interfaces, the query building blocks they exchange (Filter, Order, Cursor, Column, Term, Window), and the per-request ServiceContext.
Package types defines the contracts between the framework and business projects: the Model, Service, Database, Selector, Cache, RBAC, and Logger interfaces, the query building blocks they exchange (Filter, Order, Cursor, Column, Term, Window), and the per-request ServiceContext.
urlquery
Package urlquery translates URL query parameters into the arguments the database layer builds a query from.
Package urlquery translates URL query parameters into the arguments the database layer builds a query from.
Package leader runs, on one replica of a deployment at a time, the work a project registers: a loop that polls an external system, a relay that forwards an outbox, the one connection a protocol allows.
Package leader runs, on one replica of a deployment at a time, the work a project registers: a loop that polls an external system, a relay that forwards an outbox, the one connection a protocol allows.
Package lock runs a piece of work once at a time across a deployment: an administrator's "rebuild the report", a refresh of a credential every replica shares, anything that must not run twice at once and is done when it returns.
Package lock runs a piece of work once at a time across a deployment: an administrator's "rebuild the report", a refresh of a credential every replica shares, anything that must not run twice at once and is done when it returns.
Package logger holds the framework's log streams, one per component, and the loggers behind them: Init opens the streams the configuration names, and New builds a logger of its own for a component that has one.
Package logger holds the framework's log streams, one per component, and the loggers behind them: Init opens the streams the configuration names, and New builds a logger of its own for a component that has one.
Package prommetrics provides Prometheus metrics for the application; the name avoids conflicting with standard library or common "metrics" package names.
Package prommetrics provides Prometheus metrics for the application; the name avoids conflicting with standard library or common "metrics" package names.
Package middleware is the HTTP middleware a project mounts: Register adds middleware to every API route, RegisterAuth to the routes of the authenticated group, and the constructors here build the middleware the framework ships for either.
Package middleware is the HTTP middleware a project mounts: Register adds middleware to every API route, RegisterAuth to the routes of the authenticated group, and the constructors here build the middleware the framework ships for either.
Package modelschema exposes the model column resolution used by the framework.
Package modelschema exposes the model column resolution used by the framework.
Package module provides a unified module registration system that automatically registers models, services, and HTTP routes for CRUD operations.
Package module provides a unified module registration system that automatically registers models, services, and HTTP routes for CRUD operations.
helloworld
Package helloworld is the complete module example projects are pointed to: one module on an empty model with request and response types of its own, and one on a table-backed model with before and after hooks for each action.
Package helloworld is the complete module example projects are pointed to: one module on an empty model with request and response types of its own, and one on a table-backed model with before and after hooks for each action.
iam
mfa
version
Package versionmod provides the version API module; the name avoids conflicting with the standard library "runtime/version" package.
Package versionmod provides the version API module; the name avoids conflicting with the standard library "runtime/version" package.
Package otel provides OpenTelemetry tracing integration using OTLP exporters.
Package otel provides OpenTelemetry tracing integration using OTLP exporters.
pkg
httpwrapper
Package httpwrapper provides JSON round-trip wrappers around *http.Request and *http.Response.
Package httpwrapper provides JSON round-trip wrappers around *http.Request and *http.Response.
tunnel
Package tunnel is communication protocol between with server and user, server and client.
Package tunnel is communication protocol between with server and user, server and client.
version
Package pkgversion provides version string helpers; the name avoids conflicting with the standard library "runtime/version" package.
Package pkgversion provides version string helpers; the name avoids conflicting with the standard library "runtime/version" package.
provider
clickhouse
Package clickhouse provides the native ClickHouse client for analytical workloads: high-throughput batch ingestion and queries that bypass the gorm dialect.
Package clickhouse provides the native ClickHouse client for analytical workloads: high-throughput batch ingestion and queries that bypass the gorm dialect.
Package redis holds the process-wide Redis client, the typed cache backend built on it, and the helpers that share its keyspace.
Package redis holds the process-wide Redis client, the typed cache backend built on it, and the helpers that share its keyspace.
Package requestctx is the public entry to the request metadata that storage and service logs name a request by.
Package requestctx is the public entry to the request metadata that storage and service logs name a request by.
Package response exposes the response entry points that code outside the framework's controller path needs: middleware, and the middleware a module ships to the projects that copy it.
Package response exposes the response entry points that code outside the framework's controller path needs: middleware, and the middleware a module ships to the projects that copy it.
Package router registers a project's HTTP routes and the hooks that run once they are ready.
Package router registers a project's HTTP routes and the hooks that run once they are ready.
Package service exposes the public service extension points for application code.
Package service exposes the public service extension points for application code.
Package sse exposes the framework's Server-Sent Events support to application code.
Package sse exposes the framework's Server-Sent Events support to application code.
Package tenant scopes a model's rows to the tenant the caller acts in.
Package tenant scopes a model's rows to the tenant the caller acts in.
Package testutil is the test suite a gst project writes its tests against.
Package testutil is the test suite a gst project writes its tests against.

Jump to

Keyboard shortcuts

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