扩展包
extend/ 下是框架自带的工具库。它们不依赖框架主体,可以单独使用。这里按实用程度分组介绍,重点包给出完整 API。
包名前缀统一为 cherryXxx,import 时建议起短别名。
时间 — extend/time
import ctime "github.com/cherry-game/cherry/extend/time"CherryTime 内嵌 time.Time,增加了时区/时间偏移支持和大量便捷方法。
构造
ctime.Now() // 当前时间(应用全局时区与偏移)
ctime.NewSecond(1700000000) // 由秒时间戳
ctime.NewMillisecond(1700000000000) // 由毫秒时间戳
ctime.CreateFromTimestamp(ts) // 按 10/13/16/19 位自动判断秒/毫秒/微秒/纳秒
ctime.CreateFromDateTime(2026, 10, 10, 12, 0, 0)
ctime.Yesterday()
ctime.Tomorrow()时区与时间偏移
ctime.SetOffsetLocation("Asia/Shanghai") // 设置全局时区,默认 "Local"
ctime.AddOffsetTime(8 * time.Hour) // 全局加偏移
ctime.SubOffsetTime(8 * time.Hour)全局状态
时区与偏移是进程级全局变量。所有 ctime.Now() 都受它影响。跨服对齐时间时通常在这里设一次,不要在中途反复改。
常用方法与格式
now := ctime.Now()
now.ToDateTimeFormat() // "2006-01-02 15:04:05"
now.ToDateFormat() // "2006-01-02"
now.ToShortDateFormat() // "20060102"
now.ToMillisecond() // 毫秒时间戳
now.ToSecond() // 秒时间戳
now.Year(); now.Month(); now.Day()
now.DayOfWeek(); now.WeekOfYear()
now.StartOfDay(); now.EndOfMonth()
now.IsToday(); now.IsWeekend()
now.DiffInDays(other); now.DiffInHours(other)
now.NowDiffSecond() // 距现在多少秒
now.AddDays(1); now.SubHours(2); now.AddMonths(1)格式方法还有 ToTimeFormat、ToDateMillisecondFormat、ToShortDateTimeFormat、ToShortTimeFormat 等。
时间轮 — extend/time_wheel
import ctimeWheel "github.com/cherry-game/cherry/extend/time_wheel"5 层分级时间轮,tick 驱动。Actor 系统的定时器就建立在它之上。
// 创建(tick 小于 1ms 会回落到 10ms)
tw := ctimeWheel.NewTimeWheel(10 * time.Millisecond)
tw := ctimeWheel.NewTimeWheelWithHint(10*time.Millisecond, 1024) // 预分配,量大时用
tw.Start()
defer tw.Stop() // 停止后不可重启
// 周期性定时器
timer := tw.AddTimer(5*time.Second, func() {
// 每 5 秒
}, false)
// 一次性
tw.AddTimer(1*time.Second, fn, true)
// 按调度规则
tw.AddScheduleTimer(&ctimeWheel.FixedDateSchedule{
Hour: 4, Minute: 0, Second: 0,
}, func() {
// 每天 04:00:00
})
// 动态改变下次间隔
timer.SetNext(func() time.Duration {
return time.Duration(rand.Intn(10)) * time.Second
})
tw.RemoveTimer(timer.ID())内置调度器:
| 类型 | 说明 |
|---|---|
EverySchedule{Interval} | 固定间隔 |
FixedDateSchedule{Hour, Minute, Second} | 每天固定时刻 |
也可以实现 Scheduler 接口自定义:
type Scheduler interface {
Next(time.Time) time.Time
}Timer 句柄非并发安全
Timer 的 Start / Stop / SetNext 需要同一个 goroutine 驱动。业务层一般不用直接操作它——用 Actor 的 Timer() 门面更安全,回调会回到 Actor 自己的 goroutine。
雪花 ID — extend/snowflake
import cherrySnowflake "github.com/cherry-game/cherry/extend/snowflake"cherrySnowflake.SetDefaultNode(serverId) // 每个节点一个不同的数字
id := cherrySnowflake.NextID() // int64
id := cherrySnowflake.Next() // snowflake.ID 类型- 纪元:
2021-01-01 - 节点位:16 bit,步长位:6 bit
InitDefaultNode(str)可对字符串取 CRC32 再取模得到节点号
节点号必须唯一
不同节点用同一个节点号会产生重复 ID。集群示例里 game 节点直接把纯数字 nodeID 当节点号用,这也是 game 节点要求 nodeID 必须是数字的原因。
// ID 的常用方法
id.Int64()
id.String()
id.Base58()
id.Time() // 该 ID 的生成时间
id.Node() // 生成它的节点号HTTP 客户端 — extend/http
import cherryHttp "github.com/cherry-game/cherry/extend/http"默认超时 5 秒。
// GET,可带 query
body, resp, err := cherryHttp.GET("http://127.0.0.1:8081/hello")
body, resp, err := cherryHttp.GET(url, map[string]string{"page": "1"})
// POST 表单
body, resp, err := cherryHttp.POST(url, map[string]string{"name": "abc"})
// POST JSON
body, resp, err := cherryHttp.PostJSON(url, myStruct)
// 工具
cherryHttp.AddParams(url, url.Values{...})
cherryHttp.ToUrlValues(map[string]string{...})服务端之间互调
集群里 web 节点对外提供 HTTP 接口,其他节点可以用它直接调。示例的压测机器人就是这么注册账号的。
并发工具 — extend/sync 与 extend/queue
import (
csync "github.com/cherry-game/cherry/extend/sync"
cqueue "github.com/cherry-game/cherry/extend/queue"
)// WaitGroup:直接收函数,省掉 Add/Done 配对
// 注意 Add 会把任务丢到新的 goroutine 里执行,本身不阻塞
wg := &csync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(func() {
// 工作(在新 goroutine 中执行)
})
}
wg.Wait()
// 并发限流
limiter := csync.NewLimit(10)
limiter.Borrow() // 阻塞直到拿到名额
defer limiter.Return()
if limiter.TryBorrow() { // 非阻塞版本
defer limiter.Return()
}queue.Queue 是无锁 MPSC(多生产者单消费者)队列:
q := cqueue.NewQueue()
q.Push(item) // 可多 goroutine
item := q.Pop() // 只能单 goroutine
q.Empty()常用工具包
crypto — 哈希与编解码
ccrypto.MD5("abc")
ccrypto.MD5WithBytes(b)
ccrypto.CRC32("abc")
ccrypto.Base64Encode("abc")
ccrypto.Base64Decode(s) // (string, error)
ccrypto.Base64DecodeBytes(s) // ([]byte, error)string — 字符串与类型转换
cstring.IsBlank(s); cstring.IsNotBlank(s)
// 带默认值的安全转换
v, ok := cstring.ToInt("123") // 第二返回值表示成功与否
v := cstring.ToIntD("abc", 100) // 失败返回默认值 100
cstring.ToInt32D / ToInt64D / ToUintD
cstring.ToString(anyValue)
cstring.CutLastString(text, "/", ":")
cstring.SplitIndex(s, ",", 2)slice — 切片操作
cslice.Int64In(v, list) // (index, found)
cslice.StringIn(v, list)
cslice.StringInSlice(v, list) // bool
cslice.Sum(list); cslice.Merge(a, b); cslice.Filter(list, fn)
cslice.Diff(a, b); cslice.Intersect(a, b); cslice.Chunk(list, n)
cslice.Rand(list); cslice.Shuffle(list)
cslice.StringToInt64(strings)
// 泛型去重
cslice.Unique(1, 2, 2, 3) // [1 2 3]
cslice.Uniques(a, b) // 合并后去重map — 并发安全 Map
// 泛型,可选并发安全
m := cmap.NewMap[string, int](true) // true = 带锁
m.Put("k", 1)
v, ok := m.Get("k")
m.Remove("k")
// string → any
sm := cmap.NewStrAnyMap()
sm.Set("k", 1)
v, ok := sm.Get("k")
v := sm.GetOrSetFuncLock("k", func() any { return compute() })regex — 带缓存的 regexp
cregex.IsMatchString(`^\d+$`, "123") // bool,编译结果会被缓存
cregex.MatchString(pattern, src)
cregex.ReplaceString(`\d`, "x", src)
cregex.Split(pattern, src)
cregex.Validate(pattern) // 校验表达式合法性file — 路径与文件
cfile.JudgeFile(path) // (绝对路径, 是否成功)
cfile.JudgePath(path) // 依次在工作目录/可执行目录/栈目录查找
cfile.IsDir(p); cfile.IsFile(p)
cfile.GetWorkDir(); cfile.GetCurrentDirectory()
cfile.GetFileName(path, true) // removeExt = true 去掉扩展名
cfile.WalkFiles(root, ".json") // 递归收集
cfile.ReadDir(root, "prefix", ".json")JudgePath 的查找顺序
配置文件用相对路径时,框架就是这个逻辑:先看进程工作目录,再看可执行文件目录,最后看调用栈目录。这解释了为什么示例都要求在对应子目录里 go run .。
net — 网络工具
cnet.LocalIPV4() // 本机内网 IP,带缓存
cnet.GetIPV4(addr) // 从 net.Addr 取 IP
cnet.ExtractClientIP(httpRequest) // 解析 X-Forwarded-For / X-Real-IPmapstructure — map 转 struct
import cherryMapStructure "github.com/cherry-game/cherry/extend/mapstructure"
var cfg MyConfig
err := cherryMapStructure.Decode(inputMap, &cfg)框架自身不用它解析配置
容易误解的一点:profile 和 data-config 组件的配置解析走的是 jsoniter,不是这个包。
mapstructure 在这里是一个通用工具,用在你需要把 map[string]interface{}(比如 JSON 反序列化结果、Redis 取出的数据)转成 struct 的场景。配表读取就是典型用法,见 data-config。
utils — 通用判断与 panic 捕获
cutils.IsEmpty(v); cutils.IsNil(v)
cutils.IsLetterUpper(s); cutils.IsNumeric(s); cutils.UcFirst(s)
// 捕获 panic,避免一个组件拖垮整个进程
cutils.Try(func() {
risky()
}, func(errString string) {
clog.Errorf("panic: %s", errString)
})其他
| 包 | 用途 |
|---|---|
base58 | Base58 编解码 |
compress | zlib 压缩/解压,IsCompressed 可识别 zlib/gzip 魔数 |
gob | 基于标准库 encoding/gob 的多值编解码 |
json | ToJson(v) 与从文件读 map |
nuid | 生成短唯一串 |
reflect | 反射辅助,GetFuncInfo 用于消息分发 |
json / compress | 序列化与压缩的底层支撑 |