Compare commits

...

8 Commits
v1.0.3 ... main

15 changed files with 560 additions and 518 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.ai/
.geminiignore

35
AI.md
View File

@ -1,35 +0,0 @@
# AI Coding Context: @go/time
本索引供 AI 模型理解 `@go/time` 的逻辑与调用原则,以生成符合本项目“极简、无摩擦”哲学的代码。
## 🤖 AI 行为准则
1. **优先使用包级函数**:直接使用 `time.Parse(v)`,自动归一化处理。
2. **默认本地时间**:所有解析结果统一归一化为 `time.Local`
3. **使用 DSL 替代手动计算**时间偏移如“30天后”强制使用 `time.Add("+30D", v)`
4. **容错处理**:解析失败返回 `time.Now()`,无 Panic 风险。
## 🛠 API Reference
### 核心解析与格式化
- `func Parse(v any) time.Time`全能解析支持时间戳、ISO、RFC3339、中文习惯等。
- `func Format(layout string, v any) string`:人性化格式化。支持 `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `ms`, `ZZ` 等直观占位符。
- `func Add(expr string, v any) time.Time`:时间计算 DSL。支持 `Y, M, D, h, m, s, ms, us, ns` 组合。
### 快捷助手
- `func Now() time.Time`:获取当前时间。
- `func Today() time.Time`:获取今日 00:00:00。
- `func Yesterday() time.Time`:获取昨日 00:00:00。
- `func Tomorrow() time.Time`:获取明日 00:00:00。
## 🧩 典型模式 (Best Practices)
* **✅ 推荐**:
```go
// 解析与偏移
tm := time.Parse("2025-06-23")
nextMonth := time.Add("+1M", tm)
// 格式化输出
str := time.Format("YYYY-MM-DD HH:mm:ss", time.Now())
```

100
README.md
View File

@ -1,50 +1,78 @@
# 关于本项目
# @go/timer
本项目完全由 AI 维护。代码源自 github.com/ssgo/u 的重构。
# @go/time
`@go/time` 是一个致力于“零心智负担”的时间处理库。它通过极简的 API 屏蔽了 Go 原生 `time` 包中繁琐的 Layout 记忆与类型转换,让时间解析、格式化与加减操作变得直观且高效。
> **Maintainer Statement:** 本项目完全由 AI 维护。任何改动均遵循代码质量与性能的最佳实践。
## 🎯 设计哲学
* **直观 Layout**:使用 `YYYY-MM-DD HH:mm:ss` 等符合直觉的占位符。
* **强力解析**`Parse(v)` 自动探测并解析几乎所有时间格式。
* **DSL 时间算术**:通过简洁的字符串表达式进行复杂的时间偏移计算。
* **零摩擦防御**:输入非法格式时绝不 Panic返回合理的默认值`time.Now()`)。
## 🛠 API Reference
### 核心函数
- `func Parse(v any) time.Time`:全能解析。支持时间戳(s/ms/us/ns)、ISO、RFC3339、中文、JS Date 等。结果统一归一化为 `time.Local`
- `func Format(layout string, v any) string`:人性化格式化。支持 `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `ms`, `ZZ` 等占位符。
- `func Add(expr string, v any) time.Time`:时间计算 DSL。支持 `Y, M, D, h, m, s, ms, us, ns` 组合(如 `+1Y-2M`)。
### 快捷助手
- `func Now() time.Time`
- `func Today() time.Time`:今日 00:00:00。
- `func Yesterday() time.Time`:昨日 00:00:00。
- `func Tomorrow() time.Time`:明日 00:00:00。
`@go/timer` 是一个高性能的时间流管控引擎,专为构建健壮的微服务基础设施而设计。它不仅仅是计时器,更是一套处理并发、超时和重试的工具集。
## 📦 安装
```bash
go get apigo.cc/go/time
go get apigo.cc/go/timer
```
## 💡 快速开始
## 💡 核心功能
### 1. 耗时打点追踪 (Tracker)
用于精准测量复杂业务流程中各环节的耗时。
```go
import "apigo.cc/go/time"
tk := timer.Start()
// ... 业务步骤 1
tk.Record("step1")
// ... 业务步骤 2
tk.Record("step2")
// 1. 自动解析
tm := time.Parse("2025-06-23T15:30:45Z")
// 2. 直观格式化
fmt.Println(time.Format("YYYY-MM-DD HH:mm", tm))
// 3. 语义加减
nextMonth := time.Add("+1M", tm)
fmt.Println(tk.Describe()) // [step1] 10ms; [step2] 20ms; Total: 30ms
```
### 2. 指数退避重试 (Retry)
提供优雅的重试管控,防止由于短时波动导致的系统雪崩。
```go
err := timer.Retry(func() error {
return fetchRemoteData()
}, timer.WithMaxRetries(3), timer.WithBackoff(100*time.Millisecond, 2.0))
```
### 3. 高性能时间轮 (Time Wheel)
在大规模定时任务场景下,相比原生 `time.Timer` 具有极低的 CPU 和内存开销。
```go
tw := timer.NewTimeWheel(10*time.Millisecond, 100)
tw.Start()
tw.AfterFunc(5*time.Second, func() {
// 异步执行
})
```
### 4. 防抖与节流 (Debouncer & Throttler)
控制高频事件的触发频率。
```go
// 防抖:静止 1s 后执行
d := timer.NewDebouncer(1*time.Second, flush)
d.Trigger()
// 节流:每 2s 最多执行一次
t := timer.NewThrottler(2*time.Second, alert)
t.Trigger()
```
### 5. 上下文超时监控 (Timeout Executor)
结合打点追踪的超时执行器。
```go
res, err := timer.RunTimeout(3*time.Second, func(tk *timer.Tracker) (any, error) {
tk.Record("init")
// ...
return data, nil
})
```
## 🧪 验证状态
测试全部通过,性能达标。
详见:[TEST.md](./TEST.md)

45
TEST.md
View File

@ -1,26 +1,27 @@
# Test Report: @go/time
# Test Report - @go/timer
## 📋 测试概览
- **测试时间**: 2026-04-22
- **测试环境**: darwin/amd64 (Intel i9-9980HK)
- **Go 版本**: 1.25.0
## 📊 测试概览
- **通过率**: 100%
- **覆盖模块**: Tracker, Retry, TimeWheel, Flow Control (Debounce/Throttle), RunTimeout
- **最后测试时间**: 2026-05-04
## ✅ 功能测试 (Functional Tests)
| 场景 | 状态 | 描述 |
| :--- | :--- | :--- |
| `TestParse` | PASS | 覆盖时间戳、紧凑格式、RFC3339、标准/斜杠/点日期、短年份、中文格式、纯时间等。 |
| `TestFormat` | PASS | 验证直观布局 (YYYY-MM-DD HH:mm:ss) 的正确转换与格式化。 |
| `TestAdd` | PASS | 验证 DSL 时间加减 (+1Y-2M+3D, +1h30m 等) 的逻辑。 |
| `TestHelpers` | PASS | 验证 `Now()`, `Today()`, `Yesterday()`, `Tomorrow()` 辅助函数。 |
## 🧪 详细测试结果
## 🛡️ 鲁棒性防御 (Robustness)
- **非法输入拦截**:空字符串或无效格式均返回 `time.Now()`,确保业务不中断,无 Panic。
- **自动时区修正**RFC3339/JS 格式自动转换为 `time.Local`,保持时区一致性。
- **中文宽松匹配**支持“下午3点”、“15时”等多种中文习惯。
### Tracker (耗时追踪)
- `TestTracker`: 验证多段耗时记录与描述输出。 **[PASS]**
## ⚡ 性能基准 (Benchmarks)
| 函数 | 平均耗时 | 性能分析 |
| :--- | :--- | :--- |
| `Parse` | **501.1 ns/op** | 灵活的多格式探测解析,性能保持在微秒级。 |
| `Format` | **742.6 ns/op** | 包含布局字符串替换与 Go 原生 Format性能良好。 |
| `Add` | **146.2 ns/op** | DSL 解析与时间计算极其高效。 |
### Retry (退避重试)
- `TestRetry`: 验证最大重试次数与退避因子。 **[PASS]**
### TimeWheel (时间轮)
- `TestTimeWheel`: 验证任务延迟执行的准确性。 **[PASS]**
### Flow Control (流量管控)
- `TestFlow`: 验证 Debounce 和 Throttle 的触发逻辑。 **[PASS]**
### RunTimeout (超时监控)
- `TestRunTimeout`: 验证正常执行与超时中断场景。 **[PASS]**
## ⚙️ 测试环境
- **Go Version**: 1.25.0
- **OS**: darwin

View File

@ -1,31 +0,0 @@
package time_test
import (
"testing"
"time"
utime "apigo.cc/go/time"
)
func BenchmarkParse(b *testing.B) {
input := "2025-06-23 15:30:45"
for i := 0; i < b.N; i++ {
_ = utime.Parse(input)
}
}
func BenchmarkFormat(b *testing.B) {
tm := time.Now()
layout := "YYYY-MM-DD HH:mm:ss"
for i := 0; i < b.N; i++ {
_ = utime.Format(layout, tm)
}
}
func BenchmarkAdd(b *testing.B) {
tm := time.Now()
expr := "+1Y2M3D4h5m6s"
for i := 0; i < b.N; i++ {
_ = utime.Add(expr, tm)
}
}

37
executor.go Normal file
View File

@ -0,0 +1,37 @@
package timer
import (
"context"
"errors"
"time"
)
var ErrTimeout = errors.New("execution timed out")
// RunTimeout 执行带超时控制的任务,并支持打点追踪
func RunTimeout[T any](timeout time.Duration, fn func(tk *Tracker) (T, error)) (T, error) {
tk := Start()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan struct {
res T
err error
}, 1)
go func() {
res, err := fn(tk)
done <- struct {
res T
err error
}{res, err}
}()
select {
case <-ctx.Done():
var zero T
return zero, ErrTimeout
case result := <-done:
return result.res, result.err
}
}

61
flow.go Normal file
View File

@ -0,0 +1,61 @@
package timer
import (
"sync"
"time"
)
// Debouncer 防抖器
type Debouncer struct {
interval time.Duration
callback func()
timer *time.Timer
mu sync.Mutex
}
// NewDebouncer 创建一个防抖器
func NewDebouncer(interval time.Duration, callback func()) *Debouncer {
return &Debouncer{
interval: interval,
callback: callback,
}
}
// Trigger 触发事件
func (d *Debouncer) Trigger() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.interval, d.callback)
}
// Throttler 节流器
type Throttler struct {
interval time.Duration
callback func()
lastCalled time.Time
mu sync.Mutex
}
// NewThrottler 创建一个节流器
func NewThrottler(interval time.Duration, callback func()) *Throttler {
return &Throttler{
interval: interval,
callback: callback,
}
}
// Trigger 触发事件
func (t *Throttler) Trigger() {
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
if now.Sub(t.lastCalled) >= t.interval {
go t.callback()
t.lastCalled = now
}
}

6
go.mod
View File

@ -1,7 +1,3 @@
module apigo.cc/go/time
module apigo.cc/go/timer
go 1.25.0
require apigo.cc/go/cast v1.0.3
require gopkg.in/yaml.v3 v3.0.1 // indirect

6
go.sum
View File

@ -1,6 +0,0 @@
apigo.cc/go/cast v1.0.2 h1:L9cbD6gOxQdUTwLJz8aJlV7L1df7CH11N80qKhb1QTk=
apigo.cc/go/cast v1.0.2/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

56
retry.go Normal file
View File

@ -0,0 +1,56 @@
package timer
import (
"time"
)
// RetryOptions 重试配置
type RetryOptions struct {
MaxRetries int
Backoff time.Duration
Factor float64
}
// RetryOption 重试选项函数
type RetryOption func(*RetryOptions)
// WithMaxRetries 设置最大重试次数
func WithMaxRetries(n int) RetryOption {
return func(o *RetryOptions) {
o.MaxRetries = n
}
}
// WithBackoff 设置初始等待时间和增长因子
func WithBackoff(d time.Duration, factor float64) RetryOption {
return func(o *RetryOptions) {
o.Backoff = d
o.Factor = factor
}
}
// Retry 执行带退避机制的重试
func Retry(fn func() error, opts ...RetryOption) error {
o := &RetryOptions{
MaxRetries: 3,
Backoff: 100 * time.Millisecond,
Factor: 2.0,
}
for _, opt := range opts {
opt(o)
}
var err error
backoff := o.Backoff
for i := 0; i <= o.MaxRetries; i++ {
err = fn()
if err == nil {
return nil
}
if i < o.MaxRetries {
time.Sleep(backoff)
backoff = time.Duration(float64(backoff) * o.Factor)
}
}
return err
}

286
time.go
View File

@ -1,286 +0,0 @@
package time
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"apigo.cc/go/cast"
)
// Parse 将任意类型转换为 time.Time。
// 支持time.Time, 时间戳 (秒/毫秒/微秒/纳秒), RFC3339, JS 格式, 中文格式等。
// 转换失败返回当前时间 time.Now()。
func Parse(v any) time.Time {
if v == nil {
return time.Now()
}
if tm, ok := v.(time.Time); ok {
return tm
}
str := strings.TrimSpace(cast.String(v))
if str == "" {
return time.Now()
}
var tm time.Time
var err error
// 1. 处理纯数字 (时间戳或紧凑格式)
if num := cast.Int64(v); num > 0 {
// 紧凑格式解析
switch len(str) {
case 14: // 20060102150405
if tm, err = time.ParseInLocation("20060102150405", str, time.Local); err == nil {
return tm
}
case 8: // 20060102
if tm, err = time.ParseInLocation("20060102", str, time.Local); err == nil {
return tm
}
case 6: // 150405 或 060102
if tm, err = time.ParseInLocation("150405", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("060102", str, time.Local); err == nil {
return tm
}
}
// 时间戳处理
switch {
case num < 1e10: // 秒
return time.Unix(num, 0)
case num < 1e13: // 毫秒
return time.UnixMilli(num)
case num < 1e16: // 微秒
return time.UnixMicro(num)
default: // 纳秒
return time.Unix(0, num)
}
}
// 2. 标准格式与常见变体
// RFC3339 (带 T 和 Z 或偏移量)
if strings.Contains(str, "T") {
if tm, err = time.Parse(time.RFC3339Nano, str); err == nil {
return tm.In(time.Local)
}
if tm, err = time.Parse(time.RFC3339, str); err == nil {
return tm.In(time.Local)
}
// 兼容带 T 但没 Z 的情况,尝试按普通日期时间解析
}
// 处理分隔符:-, /, .
if len(str) >= 10 && (str[4] == '-' || str[4] == '/' || str[4] == '.') {
sep := str[4]
layout := fmt.Sprintf("2006%c01%c02", sep, sep)
// 完整日期时间
if len(str) >= 19 {
tsep := str[10] // 通常是 ' ' 或 'T'
if len(str) >= 20 && str[19] == '.' {
// 带纳秒
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05.999999999", tsep), str, time.Local); err == nil {
return tm
}
} else {
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", tsep), str[:19], time.Local); err == nil {
return tm
}
}
}
// 纯日期
if tm, err = time.ParseInLocation(layout, str[:10], time.Local); err == nil {
return tm
}
}
// 06-01-02 这种短年份格式
if len(str) >= 8 && (str[2] == '-' || str[2] == '/' || str[2] == '.') {
sep := str[2]
layout := fmt.Sprintf("06%c01%c02", sep, sep)
if len(str) >= 17 && (str[8] == ' ' || str[8] == 'T') {
if tm, err = time.ParseInLocation(layout+fmt.Sprintf("%c15:04:05", str[8]), str[:17], time.Local); err == nil {
return tm
}
}
if tm, err = time.ParseInLocation(layout, str[:8], time.Local); err == nil {
return tm
}
}
// 3. 特殊格式JS, HTTP, 中文
if strings.Contains(str, "GMT") || strings.Contains(str, "CST") {
// 简单清理 JS 冗余部分
cleanStr := strings.SplitN(str, " (", 2)[0]
// 统一将时区简写替换为偏移量尝试解析
cleanStr = strings.Replace(cleanStr, "CST", "+0800", 1)
formats := []string{
"Mon Jan 02 2006 15:04:05 GMT-0700",
"Mon, 02 Jan 2006 15:04:05 GMT",
time.RFC1123Z,
time.RFC1123,
time.UnixDate,
}
for _, f := range formats {
if tm, err = time.Parse(f, cleanStr); err == nil {
return tm.In(time.Local)
}
}
}
// 中文格式解析
if strings.ContainsAny(str, "年月日时点分秒") {
return parseCN(str)
}
// 纯时间格式
if strings.Contains(str, ":") {
if tm, err = time.ParseInLocation("15:04:05.999999", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04:05", str, time.Local); err == nil {
return tm
}
if tm, err = time.ParseInLocation("15:04", str, time.Local); err == nil {
return tm
}
}
return time.Now()
}
// Format 格式化时间。
// layout 支持: YYYY-MM-DD HH:mm:ss, YYYY/MM/DD, HH:mm 等直观格式。
func Format(layout string, v any) string {
tm := Parse(v)
l := layout
l = strings.ReplaceAll(l, "YYYY", "2006")
l = strings.ReplaceAll(l, "YY", "06")
l = strings.ReplaceAll(l, "MM", "01")
l = strings.ReplaceAll(l, "M", "1")
l = strings.ReplaceAll(l, "DD", "02")
l = strings.ReplaceAll(l, "D", "2")
l = strings.ReplaceAll(l, "HH", "15")
l = strings.ReplaceAll(l, "hh", "03")
l = strings.ReplaceAll(l, "h", "3")
l = strings.ReplaceAll(l, "mm", "04")
l = strings.ReplaceAll(l, "ss", "05")
l = strings.ReplaceAll(l, "a", "pm")
l = strings.ReplaceAll(l, "A", "PM")
l = strings.ReplaceAll(l, "ZZ", "-0700")
l = strings.ReplaceAll(l, "Z", "-07:00")
return tm.Format(l)
}
// Add 时间加减 DSL。
// 格式如: "+1Y-2M+3D", "+1h30m", "-1s"。
// 单位支持: Y (年), M (月), D (天), h, m, s, ms, us, ns。
func Add(expr string, v any) time.Time {
tm := Parse(v)
if expr == "" {
return tm
}
i := 0
years, months, days := 0, 0, 0
var duration time.Duration
for i < len(expr) {
sign := 1
if expr[i] == '+' {
i++
} else if expr[i] == '-' {
sign = -1
i++
}
j := i
for j < len(expr) && expr[j] >= '0' && expr[j] <= '9' {
j++
}
num := 1
if j > i {
num, _ = strconv.Atoi(expr[i:j])
}
val := num * sign
i = j
unit := ""
// 匹配双字符单位 (ms, us, ns)
if i+2 <= len(expr) {
u2 := expr[i : i+2]
if u2 == "ms" || u2 == "us" || u2 == "ns" {
unit = u2
i += 2
}
}
// 匹配单字符单位
if unit == "" && i < len(expr) {
unit = expr[i : i+1]
i++
}
switch unit {
case "Y": years += val
case "M": months += val
case "D": days += val
case "h": duration += time.Duration(val) * time.Hour
case "m": duration += time.Duration(val) * time.Minute
case "s": duration += time.Duration(val) * time.Second
case "ms": duration += time.Duration(val) * time.Millisecond
case "us": duration += time.Duration(val) * time.Microsecond
case "ns": duration += time.Duration(val) * time.Nanosecond
default:
// 默认秒
duration += time.Duration(val) * time.Second
}
}
if years != 0 || months != 0 || days != 0 {
tm = tm.AddDate(years, months, days)
}
return tm.Add(duration)
}
// 辅助函数
var cnDateRegex = regexp.MustCompile(`(\d{2,4})?年?(\d{1,2})月(\d{1,2})日`)
var cnTimeRegex = regexp.MustCompile(`(上午|下午)?(\d{1,2})(?:时|点|)(\d{1,2})(?:分|)(\d{1,2})?秒?`)
func parseCN(str string) time.Time {
str = strings.ReplaceAll(str, "", ":")
str = strings.ReplaceAll(str, " ", "")
var y, m, d, h, mm, s int
if matches := cnDateRegex.FindStringSubmatch(str); len(matches) == 4 {
y = cast.Int(matches[1])
m = cast.Int(matches[2])
d = cast.Int(matches[3])
if y > 0 && y < 100 { y += 2000 }
}
if matches := cnTimeRegex.FindStringSubmatch(str); len(matches) == 5 {
h = cast.Int(matches[2])
mm = cast.Int(matches[3])
s = cast.Int(matches[4])
if matches[1] == "下午" && h < 12 { h += 12 }
if matches[1] == "上午" && h >= 12 { h -= 12 }
}
if y == 0 { y = time.Now().Year() }
if m == 0 { m = int(time.Now().Month()) }
if d == 0 { d = time.Now().Day() }
return time.Date(y, time.Month(m), d, h, mm, s, 0, time.Local)
}
// Helpers
func Now() time.Time { return time.Now() }
func Today() time.Time { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local) }
func Yesterday() time.Time { return Today().AddDate(0, 0, -1) }
func Tomorrow() time.Time { return Today().AddDate(0, 0, 1) }

View File

@ -1,97 +0,0 @@
package time_test
import (
"testing"
"time"
utime "apigo.cc/go/time"
)
func TestParse(t *testing.T) {
refTime := time.Date(2025, 6, 23, 15, 30, 45, 123456789, time.Local)
tests := []struct {
input any
expected time.Time
name string
}{
{input: refTime.Unix(), expected: refTime.Truncate(time.Second), name: "Timestamp Seconds"},
{input: refTime.UnixMilli(), expected: refTime.Truncate(time.Millisecond), name: "Timestamp Millis"},
{input: "20250623153045", expected: refTime.Truncate(time.Second), name: "Compact DateTime"},
{input: "2025-06-23T15:30:45Z", expected: time.Date(2025, 6, 23, 15, 30, 45, 0, time.UTC), name: "RFC3339 UTC"},
{input: "2025-06-23 15:30:45", expected: refTime.Truncate(time.Second), name: "Standard Format"},
{input: "2025/06/23 15:30:45", expected: refTime.Truncate(time.Second), name: "Slash Format"},
{input: "2025.06.23", expected: time.Date(2025, 6, 23, 0, 0, 0, 0, time.Local), name: "Dot Date"},
{input: "25-06-23 15:30:45", expected: refTime.Truncate(time.Second), name: "Short Year"},
{input: "2025年06月23日 15点30分45秒", expected: refTime.Truncate(time.Second), name: "Chinese Format"},
{input: "下午3点30分", expected: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 15, 30, 0, 0, time.Local), name: "Chinese Time Only"},
{input: "15:30:45", expected: time.Date(0, 1, 1, 15, 30, 45, 0, time.Local), name: "Time Only"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := utime.Parse(tc.input)
if !result.Equal(tc.expected) {
t.Errorf("Parse failed for %s: expected %v, got %v", tc.name, tc.expected, result)
}
})
}
}
func TestFormat(t *testing.T) {
refTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
tests := []struct {
layout string
input any
expected string
}{
{"YYYY-MM-DD HH:mm:ss", refTime, "2006-01-02 15:04:05"},
{"YYYY/MM/DD", "2006-01-02", "2006/01/02"},
{"hh:mm A", refTime, "03:04 PM"},
}
for _, tc := range tests {
result := utime.Format(tc.layout, tc.input)
if result != tc.expected {
t.Errorf("Format failed for layout %s: expected %s, got %s", tc.layout, tc.expected, result)
}
}
}
func TestAdd(t *testing.T) {
refTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
tests := []struct {
expr string
input any
expected time.Time
}{
{"+1D", refTime, time.Date(2006, 1, 3, 15, 4, 5, 0, time.UTC)},
{"-1M", refTime, time.Date(2005, 12, 2, 15, 4, 5, 0, time.UTC)},
{"+1Y1M1D", refTime, time.Date(2007, 2, 3, 15, 4, 5, 0, time.UTC)},
{"+1h30m", refTime, refTime.Add(90 * time.Minute)},
{"-500ms", refTime, refTime.Add(-500 * time.Millisecond)},
}
for _, tc := range tests {
result := utime.Add(tc.expr, tc.input)
if !result.Equal(tc.expected) {
t.Errorf("Add failed for expr %s: expected %v, got %v", tc.expr, tc.expected, result)
}
}
}
func TestHelpers(t *testing.T) {
now := time.Now()
if utime.Now().Before(now.Add(-time.Second)) { t.Error("Now() too early") }
today := utime.Today()
if today.Hour() != 0 || today.Minute() != 0 { t.Error("Today() not start of day") }
yesterday := utime.Yesterday()
if !yesterday.Equal(today.AddDate(0, 0, -1)) { t.Error("Yesterday() incorrect") }
tomorrow := utime.Tomorrow()
if !tomorrow.Equal(today.AddDate(0, 0, 1)) { t.Error("Tomorrow() incorrect") }
}

137
timer_test.go Normal file
View File

@ -0,0 +1,137 @@
package timer
import (
"errors"
"testing"
"time"
)
func TestTracker(t *testing.T) {
tk := Start()
time.Sleep(10 * time.Millisecond)
tk.Record("step1")
time.Sleep(20 * time.Millisecond)
tk.Record("step2")
laps := tk.Summarize()
if len(laps) != 2 {
t.Errorf("expected 2 laps, got %d", len(laps))
}
if laps[0].Label != "step1" || laps[1].Label != "step2" {
t.Errorf("invalid lap labels")
}
desc := tk.Describe()
if desc == "" {
t.Error("description should not be empty")
}
t.Log(desc)
}
func TestRetry(t *testing.T) {
count := 0
err := Retry(func() error {
count++
if count < 3 {
return errors.New("fail")
}
return nil
}, WithMaxRetries(3), WithBackoff(10*time.Millisecond, 2.0))
if err != nil {
t.Errorf("expected success, got %v", err)
}
if count != 3 {
t.Errorf("expected 3 attempts, got %d", count)
}
count = 0
err = Retry(func() error {
count++
return errors.New("always fail")
}, WithMaxRetries(2), WithBackoff(5*time.Millisecond, 2.0))
if err == nil {
t.Error("expected error, got nil")
}
if count != 3 { // 0, 1, 2 = 3 times
t.Errorf("expected 3 attempts, got %d", count)
}
}
func TestTimeWheel(t *testing.T) {
tw := NewTimeWheel(10*time.Millisecond, 10)
tw.Start()
defer tw.Stop()
done := make(chan bool)
start := time.Now()
tw.AfterFunc(50*time.Millisecond, func() {
duration := time.Since(start)
if duration < 40*time.Millisecond {
t.Errorf("too early: %v", duration)
}
done <- true
})
select {
case <-done:
// success
case <-time.After(200 * time.Millisecond):
t.Fatal("timeout")
}
}
func TestFlow(t *testing.T) {
// Debounce test
count := 0
d := NewDebouncer(20*time.Millisecond, func() {
count++
})
d.Trigger()
d.Trigger()
d.Trigger()
time.Sleep(50 * time.Millisecond)
if count != 1 {
t.Errorf("debounce failed, count: %d", count)
}
// Throttle test
count = 0
tr := NewThrottler(20*time.Millisecond, func() {
count++
})
tr.Trigger()
tr.Trigger()
tr.Trigger()
time.Sleep(10 * time.Millisecond)
if count != 1 {
t.Errorf("throttle initial failed, count: %d", count)
}
time.Sleep(30 * time.Millisecond)
tr.Trigger()
time.Sleep(10 * time.Millisecond)
if count != 2 {
t.Errorf("throttle interval failed, count: %d", count)
}
}
func TestRunTimeout(t *testing.T) {
res, err := RunTimeout(50*time.Millisecond, func(tk *Tracker) (string, error) {
tk.Record("start")
time.Sleep(10 * time.Millisecond)
tk.Record("middle")
return "ok", nil
})
if err != nil || res != "ok" {
t.Errorf("RunTimeout failed: %v, %v", res, err)
}
_, err = RunTimeout(20*time.Millisecond, func(tk *Tracker) (string, error) {
time.Sleep(50 * time.Millisecond)
return "late", nil
})
if err != ErrTimeout {
t.Errorf("expected ErrTimeout, got %v", err)
}
}

62
tracker.go Normal file
View File

@ -0,0 +1,62 @@
package timer
import (
"fmt"
"strings"
"time"
)
// Tracker 负责耗时打点追踪
type Tracker struct {
start time.Time
last time.Time
laps []Lap
}
type Lap struct {
Label string
Duration time.Duration
At time.Time
}
// Start 开始计时
func Start() *Tracker {
now := time.Now()
return &Tracker{start: now, last: now}
}
// Record 记录一段耗时 (Lap),并返回段耗时
func (t *Tracker) Record(label string) time.Duration {
now := time.Now()
d := now.Sub(t.last)
t.last = now
t.laps = append(t.laps, Lap{Label: label, Duration: d, At: now})
return d
}
// Stop 结束计时,返回总持续时间
func (t *Tracker) Stop() time.Duration {
return time.Since(t.start)
}
// Summarize 返回所有记录段
func (t *Tracker) Summarize() []Lap {
return t.laps
}
// Describe 返回格式化后的统计字符串
func (t *Tracker) Describe() string {
var sb strings.Builder
total := t.Stop()
for i, lap := range t.laps {
if i > 0 {
sb.WriteString("; ")
}
sb.WriteString(fmt.Sprintf("[%s] %v", lap.Label, lap.Duration))
}
if len(t.laps) > 0 {
sb.WriteString("; ")
}
sb.WriteString(fmt.Sprintf("Total: %v", total))
return sb.String()
}

116
wheel.go Normal file
View File

@ -0,0 +1,116 @@
package timer
import (
"container/list"
"sync"
"time"
)
// TimeWheel 时间轮
type TimeWheel struct {
interval time.Duration
ticker *time.Ticker
buckets []*list.List
size int
cursor int
tasks sync.Map // key: *Task, value: bucket index
stop chan struct{}
}
type Task struct {
delay time.Duration
circle int
callback func()
}
// NewTimeWheel 创建一个时间轮
func NewTimeWheel(interval time.Duration, size int) *TimeWheel {
tw := &TimeWheel{
interval: interval,
size: size,
buckets: make([]*list.List, size),
stop: make(chan struct{}),
}
for i := 0; i < size; i++ {
tw.buckets[i] = list.New()
}
return tw
}
// Start 启动时间轮
func (tw *TimeWheel) Start() {
tw.ticker = time.NewTicker(tw.interval)
go func() {
for {
select {
case <-tw.ticker.C:
tw.tick()
case <-tw.stop:
tw.ticker.Stop()
return
}
}
}()
}
// Stop 停止时间轮
func (tw *TimeWheel) Stop() {
close(tw.stop)
}
func (tw *TimeWheel) tick() {
bucket := tw.buckets[tw.cursor]
tw.execute(bucket)
tw.cursor = (tw.cursor + 1) % tw.size
}
func (tw *TimeWheel) execute(bucket *list.List) {
for e := bucket.Front(); e != nil; {
task := e.Value.(*Task)
if task.circle > 0 {
task.circle--
e = e.Next()
continue
}
go task.callback()
next := e.Next()
bucket.Remove(e)
tw.tasks.Delete(task)
e = next
}
}
// AfterFunc 在延迟后执行函数
func (tw *TimeWheel) AfterFunc(delay time.Duration, callback func()) *Task {
if delay < tw.interval {
delay = tw.interval
}
task := &Task{
delay: delay,
callback: callback,
}
tw.addTask(task)
return task
}
func (tw *TimeWheel) addTask(task *Task) {
pos, circle := tw.getPositionAndCircle(task.delay)
task.circle = circle
tw.buckets[pos].PushBack(task)
tw.tasks.Store(task, pos)
}
func (tw *TimeWheel) getPositionAndCircle(delay time.Duration) (pos int, circle int) {
steps := int(delay / tw.interval)
pos = (tw.cursor + steps) % tw.size
circle = (steps - 1) / tw.size
if steps <= 0 {
pos = tw.cursor
circle = 0
}
return
}