小助手
本页文档记录了小而集中、不需要单独 feature 页的工具包:page、sortx、
monad、strx、dbx、httpx、version,以及关于已移除的 ptr 包及其
替代方案的说明。
ptr —— 已移除
ptr 包已从框架中移除,替换为 Go 内置的 new 和
github.com/samber/lo 的指针 helper。如果你在迁移仍然 import
github.com/coldsmirk/vef-framework-go/ptr 的代码,请参考下表。
旧 ptr API | 替代方案 | 说明 |
|---|---|---|
ptr.Of(v) | new(v)(builtin) | Go 内置的 new 现在接受一个值表达式,返回指向该值副本的指针。与旧的 ptr.Of 不同,new(v) 总是返回非 nil 指针——包括 ""、0、false 这样的 zero values。 |
ptr.Of(v)(zero-value 返回 nil 的语义) | lo.EmptyableToPtr(v) | v 是 zero value 时返回 nil,与旧的 ptr.Of 行为完全一致。 |
ptr.Zero[T]() | lo.Empty[T]() | 返回 T 的 zero value。 |
ptr.Value(p) | lo.FromPtr(p) | 解引用 p;p 为 nil 时返回 zero value。 |
ptr.Value(p, fallback) / ptr.ValueOrElse(p, fn) | lo.FromPtrOr(p, fallback) | 解引用 p;p 为 nil 时返回给定的 fallback。lo.FromPtrOr 会立即求值 fallback,不像旧的 ptr.ValueOrElse 那样是 lazy 的。 |
ptr.Coalesce(p1, p2, ...) | 普通的 nil 判断 if/switch | lo 没有直接对应的多指针 coalesce helper;手写一个简短判断反而更清晰。 |
import "github.com/samber/lo"
p := new(true) // *bool → true(builtin new,总是非 nil)
p2 := lo.EmptyableToPtr("") // *string → nil(zero value)
p3 := lo.EmptyableToPtr(42) // *int → 42
s := lo.FromPtr(p2) // ""(解引用,nil 时返回 zero)
s = lo.FromPtrOr(p2, "default") // "default"
page
分页请求参数与响应 helper。
公开 surface:
| API | Signature / wire shape |
|---|---|
DefaultPageNumber | int = 1 |
DefaultPageSize | int = 15 |
MaxPageSize | int = 1000 |
Pageable | type Pageable struct { Page int json:"page"; Size int json:"size" } |
Pageable.Page | int json:"page" |
Pageable.Size | int json:"size" |
Page[T] | type Page[T any] struct { Page int json:"page"; Size int json:"size"; Total int64 json:"total"; Items []T json:"items" } |
Page.Page | int json:"page" |
Page.Size | int json:"size" |
Page.Total | int64 json:"total" |
Page.Items | []T json:"items" |
New | page.New[T any](pageable page.Pageable, total int64, items []T) page.Page[T] |
Pageable.Normalize | func (p *Pageable) Normalize(size ...int) |
Pageable.Offset | func (p Pageable) Offset() int |
Page.TotalPages | func (page Page[T]) TotalPages() int |
Page.HasNext | func (page Page[T]) HasNext() bool |
Page.HasPrevious | func (page Page[T]) HasPrevious() bool |
行为契约:
Pageable.Page是 1-based 页码,Pageable.Size是请求页大小Normalize(size...)会原地修改 receiverNormalize会把Page < 1重置为DefaultPageNumber- 当
Size < 1时,Normalize只使用第一个可选 fallback size;没有 fallback 时使用DefaultPageSize - 大于
MaxPageSize的值会被截到MaxPageSize Normalize不会对负数自定义 fallback 再做一次校正,因此传入的 fallback size 应该是正数Offset()只是(Page - 1) * Size计算,调用前应先完成 normalizeNew会把pageable.Page、pageable.Size和total复制进响应New会把 nilitems转成非 nil 空 slice;非 nilitems按原样使用, 不会 cloneTotalPages()在Size == 0时返回0;否则按Total / Size向上取整HasNext()判断Page < TotalPages()HasPrevious()在Page > 1时返回 true- 这个 helper 不会校验负数 total、手工构造后的负数 size,或
Normalize之外不一致的Page值
sortx
排序表达的基础类型,常用于 query builder 和 API 参数。
公开 surface:
| API | Signature / value |
|---|---|
OrderAsc | sortx.OrderDirection = 0 |
OrderDesc | sortx.OrderDirection = 1 |
OrderDirection | type OrderDirection int |
OrderDirection.String | func (od OrderDirection) String() string |
OrderDirection.MarshalText | func (od OrderDirection) MarshalText() ([]byte, error) |
OrderDirection.UnmarshalText | func (od *OrderDirection) UnmarshalText(text []byte) error |
OrderDirection.MarshalJSON | func (od OrderDirection) MarshalJSON() ([]byte, error) |
OrderDirection.UnmarshalJSON | func (od *OrderDirection) UnmarshalJSON(data []byte) error |
ErrInvalidOrderDirection | exported error var |
NullsDefault | sortx.NullsOrder = 0 |
NullsFirst | sortx.NullsOrder = 1 |
NullsLast | sortx.NullsOrder = 2 |
NullsOrder | type NullsOrder int |
NullsOrder.String | func (no NullsOrder) String() string |
OrderSpec | type OrderSpec struct |
OrderSpec.Column | string |
OrderSpec.Direction | sortx.OrderDirection |
OrderSpec.NullsOrder | sortx.NullsOrder |
OrderSpec.IsValid | func (spec OrderSpec) IsValid() bool |
行为契约:
OrderDirection.String()只有在值为OrderDesc时返回DESC;其他值都会 渲染为ASCMarshalText()会把String()的结果转成小写,因此正常值会 marshal 成asc或descUnmarshalText(text)会 trim 首尾空白,并以大小写不敏感方式接受asc/desc- 非法 text 会返回包装了
ErrInvalidOrderDirection的错误 MarshalJSON()委托MarshalText(),并输出 JSON stringUnmarshalJSON(data)要求输入是 JSON string;number、boolean 和null会在方向解析前返回OrderDirection must be a JSON string错误NullsDefault.String()返回空字符串NullsFirst.String()返回NULLS FIRSTNullsLast.String()返回NULLS LAST- 其他
NullsOrder值也返回空字符串 OrderSpec.IsValid()只检查Column != "";它不会把 column 当 SQL identifier 校验,也不会校验Direction/NullsOrder
monad
闭区间工具。
公开 surface:
| API | Signature / field |
|---|---|
NewRange | monad.NewRange[T cmp.Ordered](start T, end T) monad.Range[T] |
Range[T] | type Range[T cmp.Ordered] struct |
Range.Start | T |
Range.End | T |
Range.Contains | func (r Range[T]) Contains(value T) bool |
Range.IsValid | func (r Range[T]) IsValid() bool |
Range.IsEmpty | func (r Range[T]) IsEmpty() bool |
Range.IsNotEmpty | func (r Range[T]) IsNotEmpty() bool |
Range.Overlaps | func (r Range[T]) Overlaps(other monad.Range[T]) bool |
Range.Intersection | func (r Range[T]) Intersection(other monad.Range[T]) (monad.Range[T], bool) |
行为契约:
Range[T]约束为cmp.Ordered- 区间两端都是闭区间:
[Start, End] - exported fields 没有 JSON tags;Go 默认 JSON 编码会使用
Start和End NewRange(start, end)会按原样保存两个边界,不会重排IsValid()和IsNotEmpty()返回Start <= EndIsEmpty()返回Start > EndContains(value)检查Start <= value && value <= End,因此包含两个端点Overlaps(other)检查Start <= other.End && other.Start <= End;共享一个 端点的相邻区间也算重叠Intersection(other)会先调用Overlaps(other)- 没有重叠时,
Intersection返回Range[T]{}, false;返回的 zero range 没有业务意义 - 有重叠时,
Intersection返回从max(Start, other.Start)到min(End, other.End)的闭区间,并返回true
strx
结构体 tag 与紧凑 key/value 字符串解析。
公开 surface:
| API | Signature / value |
|---|---|
DefaultKey | untyped string constant "__default" |
BareValueMode | type BareValueMode int |
BareAsValue | strx.BareValueMode = 0 |
BareAsKey | strx.BareValueMode = 1 |
ParseOption | ParseTag 接收的 option 类型 |
ParseTag | strx.ParseTag(input string, opts ...strx.ParseOption) map[string]string |
WithPairDelimiter | strx.WithPairDelimiter(delimiter rune) strx.ParseOption |
WithPairDelimiterFunc | strx.WithPairDelimiterFunc(fn func(rune) bool) strx.ParseOption |
WithSpacePairDelimiter | strx.WithSpacePairDelimiter() strx.ParseOption |
WithValueDelimiter | strx.WithValueDelimiter(delimiter rune) strx.ParseOption |
WithBareValueMode | strx.WithBareValueMode(mode strx.BareValueMode) strx.ParseOption |
行为契约:
- 默认解析使用 comma-separated pairs、
=作为 key/value separator,并使用BareAsValue ParseTag总是返回非 nil map- pair token 会在分隔后 trim;空 token 会被跳过
- key/value pair 只在第一个 value delimiter 处分割
- 显式重复 key 会以后出现的值覆盖先出现的值
- 在 value delimiter 处分割后,key 和 value 本身不会再 trim;pair 内部的 whitespace 会保留在 key 或 value 中
- empty keys 和 empty values 都会被接受(
=value、key=和=都能解析) - 特殊字符和 Unicode 会按 raw strings 保留
- 在
BareAsValue中,第一个 bare token 会写入DefaultKey;后续 bare tokens 会被忽略并记录 warnings - 在
BareAsKey中,每个 bare token 都会成为 value 为空字符串的 key; duplicate bare keys 按普通 map overwrite 行为折叠 WithPairDelimiter(delimiter)会把 pair separator 替换成单个 rune 的相等判断WithPairDelimiterFunc(fn)会把 pair separator 替换成fnWithSpacePairDelimiter()使用unicode.IsSpace,因此 spaces、tabs 和 newlines 都会分隔 pairsWithValueDelimiter(delimiter)会把 key/value separator 从=改成指定 rune- options 按顺序应用;后面的 options 可以覆盖前面的 separator 或 bare-value setting
- option callbacks 会被直接调用;nil
ParseOption或 nil delimiter function 在执行到时会 panic
dbx
数据库相关小工具。
| API | Signature | 作用 |
|---|---|---|
ColumnWithAlias | dbx.ColumnWithAlias(column string, alias ...string) string | 用第一个非空 alias 参数为 column 加前缀。 |
IsDuplicateKeyError | dbx.IsDuplicateKeyError(err error) bool | 通过 driver codes 和 message fallback 判断 duplicate-key 错误。 |
IsForeignKeyError | dbx.IsForeignKeyError(err error) bool | 通过 driver codes 和 message fallback 判断 foreign-key 错误。 |
ColumnWithAlias("name", "u") 返回 u.name;没有 alias 或 alias 为空时返回
name。只有第一个 alias 参数会被使用:ColumnWithAlias("email", "user", "profile") 返回 user.email。这个 helper 不会 validate、quote 或 escape
identifiers;ColumnWithAlias("", "t") 会返回 t.。
IsDuplicateKeyError(nil) 返回 false。它会先检查 wrapped PostgreSQL
pgdriver.Error code 23505,再检查 wrapped MySQL *mysql.MySQLError
numbers 1062 和 1169。如果 typed checks 没有命中,它会把 err.Error()
转成小写,并按 PostgreSQL、MySQL、SQLite、SQL Server、Oracle 的兼容 message
patterns 兜底,包括 duplicate key、unique violation、duplicate entry、
unique constraint failed、violation of primary key constraint、
violation of unique key constraint、cannot insert duplicate key、
ora-00001,以及同时包含 unique constraint 和 violated 的消息。
IsForeignKeyError(nil) 返回 false。它会先检查 wrapped PostgreSQL
pgdriver.Error code 23503,再检查 wrapped MySQL *mysql.MySQLError
numbers 1451 和 1452。message fallback 覆盖 PostgreSQL、MySQL、SQLite、
SQL Server、Oracle 的 patterns,例如 violates foreign key constraint、
foreign key violation、a foreign key constraint fails、cannot add or update a child row、cannot delete or update a parent row、foreign key constraint failed、sqlite_constraint_foreignkey、foreign key mismatch、
conflicted with the foreign key constraint、statement conflicted with the foreign key、ora-02291 和 ora-02292。它也会匹配包含 violated,且包含
parent key not found 或 child record found 的 Oracle integrity-constraint
消息。
httpx
Fiber 请求 helper。
| API | Signature | 作用 |
|---|---|---|
IsJSON | httpx.IsJSON(ctx fiber.Ctx) bool | 判断 Fiber 是否认为请求 content type 是 JSON。 |
IsMultipart | httpx.IsMultipart(ctx fiber.Ctx) bool | 判断 Content-Type 是否以 multipart/form-data 开头。 |
GetIP | httpx.GetIP(ctx fiber.Ctx) string | 返回 Fiber 解析出的客户端 IP。 |
这个包刻意暴露 helper 名称,而不是要求直接调用 Fiber:IsJSON、
IsMultipart 和 GetIP。
IsJSON 委托 Fiber 的 ctx.Is("json"),因此会接受标准 JSON content type,
包括带 charset 的形式。IsMultipart 检查 multipart/form-data 前缀。
它使用 strings.HasPrefix(...),因此会接受
multipart/form-data; boundary=... 这样的 boundary 参数。
GetIP 委托 ctx.IP()。在框架的 app 配置下,这意味着
vef.app.trusted_proxies 控制 proxy headers 是否可信:没有 trusted proxies
时,客户端直接伪造的 X-Forwarded-For 会被忽略;配置 trusted proxy 后,
Fiber 会按自己的 proxy settings 处理 X-Forwarded-For。
version
框架版本常量。
| API | Contract | 作用 |
|---|---|---|
VEFVersion | version.VEFVersion 是 untyped string constant。 | 当前框架版本字符串。 |
源码注释把该值描述为当前 VEF Framework version 的 semver format。公开值包含
前导 v prefix。
实际用法
用指针表达可选字段
由于 ptr 包已经移除,需要始终非 nil 的指针使用内置的 new;需要
zero-value 变成 nil 时使用 lo.EmptyableToPtr:
- 搜索结构体:可选筛选字段使用
*bool、*string等 - 模型字段:可空数据库列使用指针类型
- 参数结构体:可选更新字段
type UserSearch struct {
api.P
IsActive *bool `json:"isActive" search:"eq,column=is_active"`
DeptID *string `json:"deptId" search:"eq,column=department_id"`
}
// 设置可选搜索筛选条件
search := UserSearch{
IsActive: new(true),
DeptID: new("dept-123"),
}
分页往返
page.Pageable 解码请求参数,page.New 包装查询结果:
var pageable page.Pageable
pageable.Normalize() // 把 Page/Size 收敛到合理的默认值
items, total := queryItems(pageable)
response := page.New(pageable, total, items)
从请求参数构造排序
sortx.OrderDirection 通过 JSON 以 "asc" / "desc" 往返,可以直接用在
typed search 结构体里,配合 sortx.OrderSpec 生成 query builder 的
ORDER BY。
检测约束冲突
if err := db.NewInsert().Model(&user).Exec(ctx); err != nil {
if dbx.IsDuplicateKeyError(err) {
return result.Err("username already taken")
}
return err
}