Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c368fb2b7a | ||
|
|
e50d4bc5ad | ||
|
|
44dffdef26 | ||
|
|
1e58a7c339 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
go.sum
|
.ai/
|
||||||
|
|
||||||
|
.geminiignore
|
||||||
|
|||||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,12 +0,0 @@
|
|||||||
# CHANGELOG
|
|
||||||
|
|
||||||
## [v1.0.5] - 2026-05-04
|
|
||||||
- **重构**: 核心时间解析与操作逻辑迁移至 `apigo.cc/go/cast` 模块。
|
|
||||||
- **优化**: 现已支持更强大的多格式解析(包括紧凑格式、中文日期、多级时间戳)及高性能 DSL 计算。
|
|
||||||
- **增强**: `TimeZone` 增加 `Now()` 方法。
|
|
||||||
|
|
||||||
## [v1.0.4] - 2026-05-01
|
|
||||||
- **新增**: 引入 `TimeZone` 上下文,支持 explicit 时区控制,兼容原有包级 API。
|
|
||||||
- **新增**: 引入 `Timer` 计时工具,支持 `Start`, `Record`, `Pause`, `Resume` 及统计描述。
|
|
||||||
- **优化**: 扩展 `DescribeDuration` 方法,支持直观的时长自然语言格式化。
|
|
||||||
- **重构**: 内部逻辑重构至 `TimeZone` 结构体,提升架构扩展性。
|
|
||||||
118
README.md
118
README.md
@ -1,68 +1,78 @@
|
|||||||
# 关于本项目
|
# @go/timer
|
||||||
|
|
||||||
本项目完全由 AI 维护。代码源自 github.com/ssgo/u 的重构。
|
> **Maintainer Statement:** 本项目完全由 AI 维护。任何改动均遵循代码质量与性能的最佳实践。
|
||||||
|
|
||||||
# @go/time
|
|
||||||
|
|
||||||
`@go/time` 是一个致力于“零心智负担”的时间处理库。它通过极简的 API 屏蔽了 Go 原生 `time` 包中繁琐的 Layout 记忆与类型转换,让时间解析、格式化与加减操作变得直观且高效。
|
|
||||||
|
|
||||||
## 🎯 设计哲学
|
## 🎯 设计哲学
|
||||||
|
|
||||||
* **开箱即用**:所有核心功能均提供包级函数,默认操作本地时区,无需初始化。
|
`@go/timer` 是一个高性能的时间流管控引擎,专为构建健壮的微服务基础设施而设计。它不仅仅是计时器,更是一套处理并发、超时和重试的工具集。
|
||||||
* **强力解析**:`Parse(v)` 自动探测并解析几乎所有时间格式。
|
|
||||||
* **DSL 时间算术**:通过简洁的字符串表达式进行复杂的时间偏移计算。
|
|
||||||
* **时区上下文**:对于特定时区需求,通过 `time.New(loc)` 轻松扩展。
|
|
||||||
* **零摩擦防御**:输入非法格式时绝不 Panic,返回合理的默认值(`time.Now()`)。
|
|
||||||
|
|
||||||
## 🛠 API Reference
|
|
||||||
|
|
||||||
### 核心函数 (默认操作本地时区)
|
|
||||||
|
|
||||||
- `func Parse(v any) time.Time`:全能解析。结果归一化为 `time.Local`。
|
|
||||||
- `func Format(layout string, v any) string`:人性化格式化。
|
|
||||||
- `func Add(expr string, v any) time.Time`:时间计算 DSL。
|
|
||||||
- `func Now() time.Time`:获取当前本地时间。
|
|
||||||
|
|
||||||
### 时区上下文扩展
|
|
||||||
|
|
||||||
当需要处理特定时区(如 UTC 或特定区域)时:
|
|
||||||
|
|
||||||
- `func New(loc *time.Location) *TimeZone`:创建一个时区上下文对象。
|
|
||||||
- `func (tz *TimeZone) Parse(v any) time.Time`:在指定时区上下文内解析。
|
|
||||||
- `func (tz *TimeZone) Format(layout string, v any) string`:在指定时区下格式化。
|
|
||||||
- `func (tz *TimeZone) Add(expr string, v any) time.Time`:在指定时区下计算偏移。
|
|
||||||
|
|
||||||
### 计时器工具 (Timer)
|
|
||||||
|
|
||||||
- `func Start() *Timer`:开始计时。
|
|
||||||
- `func (t *Timer) Record(label string) time.Duration`:记录一段耗时。
|
|
||||||
- `func (t *Timer) Pause(label string)`:暂停计时。
|
|
||||||
- `func (t *Timer) Resume()`:恢复计时。
|
|
||||||
- `func (t *Timer) Stop() time.Duration`:结束计时。
|
|
||||||
- `func (t *Timer) Describe() string`:输出统计描述。
|
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get apigo.cc/go/time
|
go get apigo.cc/go/timer
|
||||||
```
|
```
|
||||||
|
|
||||||
## 💡 快速开始
|
## 💡 核心功能
|
||||||
|
|
||||||
|
### 1. 耗时打点追踪 (Tracker)
|
||||||
|
用于精准测量复杂业务流程中各环节的耗时。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "apigo.cc/go/time"
|
tk := timer.Start()
|
||||||
|
// ... 业务步骤 1
|
||||||
|
tk.Record("step1")
|
||||||
|
// ... 业务步骤 2
|
||||||
|
tk.Record("step2")
|
||||||
|
|
||||||
// 1. 简单场景:直接使用(默认本地时区)
|
fmt.Println(tk.Describe()) // [step1] 10ms; [step2] 20ms; Total: 30ms
|
||||||
tm := time.Parse("2026-05-01 10:00:00")
|
|
||||||
nextMonth := time.Add("+1M", tm)
|
|
||||||
|
|
||||||
// 2. 复杂场景:特定时区处理
|
|
||||||
bj := time.New(time.LoadLocation("Asia/Shanghai"))
|
|
||||||
tmInBJ := bj.Parse("2026-05-01 10:00:00")
|
|
||||||
|
|
||||||
// 3. 性能计时
|
|
||||||
t := time.Start()
|
|
||||||
// ... 业务逻辑
|
|
||||||
t.Record("Step1")
|
|
||||||
fmt.Println(t.Describe())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|||||||
47
TEST.md
47
TEST.md
@ -1,28 +1,27 @@
|
|||||||
# Test Report: @go/time
|
# Test Report - @go/timer
|
||||||
|
|
||||||
## 📋 测试概览
|
## 📊 测试概览
|
||||||
- **测试时间**: 2026-05-01
|
- **通过率**: 100%
|
||||||
- **测试环境**: darwin/amd64 (Intel i9-9980HK)
|
- **覆盖模块**: Tracker, Retry, TimeWheel, Flow Control (Debounce/Throttle), RunTimeout
|
||||||
- **Go 版本**: 1.25.0
|
- **最后测试时间**: 2026-05-04
|
||||||
|
|
||||||
## ✅ 功能测试 (Functional Tests)
|
## 🧪 详细测试结果
|
||||||
| 场景 | 状态 | 描述 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| `TestParse` | PASS | 覆盖时间戳、紧凑格式、RFC3339、中文格式等。 |
|
|
||||||
| `TestFormat` | PASS | 验证直观布局转换。 |
|
|
||||||
| `TestAdd` | PASS | 验证 DSL 时间加减逻辑。 |
|
|
||||||
| `TestHelpers` | PASS | 验证辅助函数逻辑。 |
|
|
||||||
| `TestTimeZoneRobustness` | PASS | 验证跨时区解析、转换与异常回退逻辑。 |
|
|
||||||
| `Timer` | PASS | 验证计时器的启动、分段记录、暂停/恢复及统计输出。 |
|
|
||||||
| `DescribeDuration` | PASS | 验证时长自然语言格式化。 |
|
|
||||||
|
|
||||||
## 🛡️ 鲁棒性防御 (Robustness)
|
### Tracker (耗时追踪)
|
||||||
- **非法输入拦截**:空字符串或无效格式均返回当前时区下的 `time.Now()`。
|
- `TestTracker`: 验证多段耗时记录与描述输出。 **[PASS]**
|
||||||
- **自动时区修正**:所有输入自动归一化到指定 `TimeZone` 上下文。
|
|
||||||
|
|
||||||
## ⚡ 性能基准 (Benchmarks)
|
### Retry (退避重试)
|
||||||
| 函数 | 平均耗时 | 性能分析 |
|
- `TestRetry`: 验证最大重试次数与退避因子。 **[PASS]**
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| `Parse` | **~630 ns/op** | 灵活的多格式探测,性能稳定。 |
|
### TimeWheel (时间轮)
|
||||||
| `Format` | **~800 ns/op** | 高效布局替换。 |
|
- `TestTimeWheel`: 验证任务延迟执行的准确性。 **[PASS]**
|
||||||
| `Add` | **~150 ns/op** | 高效 DSL 计算。 |
|
|
||||||
|
### Flow Control (流量管控)
|
||||||
|
- `TestFlow`: 验证 Debounce 和 Throttle 的触发逻辑。 **[PASS]**
|
||||||
|
|
||||||
|
### RunTimeout (超时监控)
|
||||||
|
- `TestRunTimeout`: 验证正常执行与超时中断场景。 **[PASS]**
|
||||||
|
|
||||||
|
## ⚙️ 测试环境
|
||||||
|
- **Go Version**: 1.25.0
|
||||||
|
- **OS**: darwin
|
||||||
|
|||||||
@ -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
37
executor.go
Normal 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
61
flow.go
Normal 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
6
go.mod
@ -1,7 +1,3 @@
|
|||||||
module apigo.cc/go/time
|
module apigo.cc/go/timer
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require apigo.cc/go/cast v1.0.4
|
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
|
|||||||
56
retry.go
Normal file
56
retry.go
Normal 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
|
||||||
|
}
|
||||||
129
time.go
129
time.go
@ -1,129 +0,0 @@
|
|||||||
package time
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeZone 定义了特定时区上下文下的时间操作
|
|
||||||
type TimeZone struct {
|
|
||||||
*cast.TimeZone
|
|
||||||
}
|
|
||||||
|
|
||||||
// New 创建一个时区上下文
|
|
||||||
func New(loc *time.Location) *TimeZone {
|
|
||||||
return &TimeZone{TimeZone: cast.NewTimeZone(loc)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse 将任意类型转换为 time.Time。
|
|
||||||
// 转换失败返回当前时间 time.Now()。
|
|
||||||
func (tz *TimeZone) Parse(v any) time.Time {
|
|
||||||
tm := tz.ParseTime(v)
|
|
||||||
if tm.IsZero() {
|
|
||||||
return time.Now().In(tz.Location())
|
|
||||||
}
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse 将任意类型转换为 time.Time。
|
|
||||||
func Parse(v any) time.Time {
|
|
||||||
tm := cast.ParseTime(v)
|
|
||||||
if tm.IsZero() {
|
|
||||||
return time.Now().In(cast.DefaultTimeZone.Location())
|
|
||||||
}
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format 格式化时间。
|
|
||||||
func (tz *TimeZone) Format(layout string, v any) string {
|
|
||||||
return tz.FormatTime(layout, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format 格式化时间。
|
|
||||||
func Format(layout string, v any) string {
|
|
||||||
return cast.FormatTime(layout, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 时间加减 DSL。
|
|
||||||
func (tz *TimeZone) Add(expr string, v any) time.Time {
|
|
||||||
return tz.AddTime(expr, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 时间加减 DSL。
|
|
||||||
func Add(expr string, v any) time.Time {
|
|
||||||
return cast.AddTime(expr, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer 计时器
|
|
||||||
type Timer struct {
|
|
||||||
start time.Time
|
|
||||||
last time.Time
|
|
||||||
laps []Lap
|
|
||||||
pause time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lap struct {
|
|
||||||
Label string
|
|
||||||
Duration time.Duration
|
|
||||||
At time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start 开始计时
|
|
||||||
func Start() *Timer {
|
|
||||||
now := time.Now()
|
|
||||||
return &Timer{start: now, last: now}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record 记录一段耗时 (Lap),并返回段耗时
|
|
||||||
func (t *Timer) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause 暂停记录
|
|
||||||
func (t *Timer) Pause(label string) {
|
|
||||||
now := time.Now()
|
|
||||||
d := now.Sub(t.last)
|
|
||||||
t.laps = append(t.laps, Lap{Label: label, Duration: d, At: now})
|
|
||||||
t.last = now // 暂时标记下一次开始点
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume 从暂停中恢复
|
|
||||||
func (t *Timer) Resume() {
|
|
||||||
t.last = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop 结束计时,返回总持续时间
|
|
||||||
func (t *Timer) Stop() time.Duration {
|
|
||||||
return time.Since(t.start)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summarize 返回所有记录段
|
|
||||||
func (t *Timer) Summarize() []Lap {
|
|
||||||
return t.laps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Describe 返回格式化后的统计字符串
|
|
||||||
func (t *Timer) Describe() string {
|
|
||||||
var sb strings.Builder
|
|
||||||
total := t.Stop()
|
|
||||||
for _, lap := range t.laps {
|
|
||||||
sb.WriteString(fmt.Sprintf("[%s] %v; ", lap.Label, lap.Duration))
|
|
||||||
}
|
|
||||||
sb.WriteString(fmt.Sprintf("Total: %v", total))
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DescribeDuration 将时长转化为自然语言描述
|
|
||||||
func DescribeDuration(d time.Duration) string {
|
|
||||||
return cast.DescribeDuration(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now 获取当前时间
|
|
||||||
func Now() time.Time { return cast.Now() }
|
|
||||||
99
time_test.go
99
time_test.go
@ -1,99 +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) {
|
|
||||||
// 将测试数据转换为本地时间以符合库的 Local 归一化逻辑
|
|
||||||
refTime := time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC).Local()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
layout string
|
|
||||||
input any
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"YYYY-MM-DD HH:mm:ss", refTime, refTime.Format("2006-01-02 15:04:05")},
|
|
||||||
{"YYYY/MM/DD", "2006-01-02", "2006/01/02"},
|
|
||||||
{"hh:mm A", refTime, refTime.Format("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") }
|
|
||||||
|
|
||||||
// 通过 DSL 加减测试代替原本的辅助函数
|
|
||||||
today := utime.Parse(time.Now())
|
|
||||||
today = time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.Local)
|
|
||||||
|
|
||||||
yesterday := utime.Add("-1D", today)
|
|
||||||
if !yesterday.Equal(today.AddDate(0, 0, -1)) { t.Error("Yesterday() incorrect") }
|
|
||||||
|
|
||||||
tomorrow := utime.Add("+1D", today)
|
|
||||||
if !tomorrow.Equal(today.AddDate(0, 0, 1)) { t.Error("Tomorrow() incorrect") }
|
|
||||||
}
|
|
||||||
137
timer_test.go
Normal file
137
timer_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package time_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
utime "apigo.cc/go/time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTimeZoneRobustness(t *testing.T) {
|
|
||||||
// 定义一个特定时间: 2026-05-01 10:00:00 (UTC)
|
|
||||||
// 在上海时区 (UTC+8) 应该是 18:00:00
|
|
||||||
const input = "2026-05-01 10:00:00"
|
|
||||||
shanghai, _ := time.LoadLocation("Asia/Shanghai")
|
|
||||||
|
|
||||||
utcTZ := utime.New(time.UTC)
|
|
||||||
shTZ := utime.New(shanghai)
|
|
||||||
|
|
||||||
// 验证 1: 跨时区解析一致性
|
|
||||||
t1 := utcTZ.Parse(input)
|
|
||||||
if t1.Hour() != 10 || t1.Location() != time.UTC {
|
|
||||||
t.Errorf("UTC Parse failed: expected 10:00 UTC, got %02d:00 %s", t1.Hour(), t1.Location())
|
|
||||||
}
|
|
||||||
|
|
||||||
t2 := shTZ.Parse(input)
|
|
||||||
// 解析为上海时区,意味着输入被视为上海时间的 10:00
|
|
||||||
if t2.Hour() != 10 || t2.Location() != shanghai {
|
|
||||||
t.Errorf("Shanghai Parse failed: expected 10:00 Shanghai, got %02d:00 %s", t2.Hour(), t2.Location())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 2: 转换能力 (Shift/Convert 逻辑)
|
|
||||||
// 期望:将 UTC 的 10:00 搬移到上海时区,应该是 18:00
|
|
||||||
// 我们的逻辑: shTZ.Parse(t1) 实际上就是将 t1 搬移到了上海时区
|
|
||||||
t3 := shTZ.Parse(t1)
|
|
||||||
if t3.Hour() != 18 || t3.Location() != shanghai {
|
|
||||||
t.Errorf("Time Shift failed: expected 18:00 Shanghai, got %02d:00 %s", t3.Hour(), t3.Location())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 3: 异常鲁棒性 (无效输入)
|
|
||||||
// 设计哲学: 失败回退到 Now()
|
|
||||||
invalid := "not-a-time"
|
|
||||||
t4 := shTZ.Parse(invalid)
|
|
||||||
if t4.Location() != shanghai {
|
|
||||||
t.Errorf("Invalid input failed to use context timezone: expected %s, got %s", shanghai, t4.Location())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
tracker.go
Normal file
62
tracker.go
Normal 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
116
wheel.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user