Skip to content

扩展包 ​

extend/ 下是框架自带的工具库。它们不依赖框架主体,可以单独使用。这里按实用程度分组介绍,重点包给出完整 API。

包名前缀统一为 cherryXxx,import 时建议起短别名。

时间 — extend/time ​

import ctime "github.com/cherry-game/cherry/extend/time"

CherryTime 内嵌 time.Time,增加了时区/时间偏移支持和大量便捷方法。

构造 ​

go
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()

时区与时间偏移 ​

go
ctime.SetOffsetLocation("Asia/Shanghai") // 设置全局时区,默认 "Local"
ctime.AddOffsetTime(8 * time.Hour)       // 全局加偏移
ctime.SubOffsetTime(8 * time.Hour)

全局状态

时区与偏移是进程级全局变量。所有 ctime.Now() 都受它影响。跨服对齐时间时通常在这里设一次,不要在中途反复改。

常用方法与格式 ​

go
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 系统的定时器就建立在它之上。

go
// 创建(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 接口自定义:

go
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"
go
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 必须是数字的原因。

go
// 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 秒。

go
// 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"
)
go
// 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(多生产者单消费者)队列:

go
q := cqueue.NewQueue()
q.Push(item)        // 可多 goroutine
item := q.Pop()     // 只能单 goroutine
q.Empty()

常用工具包 ​

crypto — 哈希与编解码 ​

go
ccrypto.MD5("abc")
ccrypto.MD5WithBytes(b)
ccrypto.CRC32("abc")
ccrypto.Base64Encode("abc")
ccrypto.Base64Decode(s)       // (string, error)
ccrypto.Base64DecodeBytes(s)  // ([]byte, error)

string — 字符串与类型转换 ​

go
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 — 切片操作 ​

go
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 ​

go
// 泛型,可选并发安全
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 ​

go
cregex.IsMatchString(`^\d+$`, "123")   // bool,编译结果会被缓存
cregex.MatchString(pattern, src)
cregex.ReplaceString(`\d`, "x", src)
cregex.Split(pattern, src)
cregex.Validate(pattern)               // 校验表达式合法性

file — 路径与文件 ​

go
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 — 网络工具 ​

go
cnet.LocalIPV4()                    // 本机内网 IP,带缓存
cnet.GetIPV4(addr)                  // 从 net.Addr 取 IP
cnet.ExtractClientIP(httpRequest)   // 解析 X-Forwarded-For / X-Real-IP

mapstructure — map 转 struct ​

go
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 捕获 ​

go
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)
})

其他 ​

包用途
base58Base58 编解码
compresszlib 压缩/解压,IsCompressed 可识别 zlib/gzip 魔数
gob基于标准库 encoding/gob 的多值编解码
jsonToJson(v) 与从文件读 map
nuid生成短唯一串
reflect反射辅助,GetFuncInfo 用于消息分发
json / compress序列化与压缩的底层支撑