diff --git a/go.mod b/go.mod index d128035..9126208 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,6 @@ toolchain go1.24.1 require ( github.com/gobwas/glob v0.2.3 github.com/google/uuid v1.6.0 - github.com/gosimple/slug v1.15.0 - github.com/sony/sonyflake v1.2.1 github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 @@ -17,7 +15,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gosimple/unidecode v1.0.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect diff --git a/go.sum b/go.sum index be69b08..dd852ef 100644 --- a/go.sum +++ b/go.sum @@ -7,10 +7,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= -github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= -github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= -github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -24,8 +20,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48= -github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= diff --git a/id/README.md b/id/README.md new file mode 100644 index 0000000..cb9e64b --- /dev/null +++ b/id/README.md @@ -0,0 +1,28 @@ +# ID生成器 + +## 订单ID + +- 电商平台:202506041234567890(时间戳 + 随机数,19-20 位)。 +- 支付系统:PAY20250604123456789(业务前缀 + 时间戳 + 序号)。 +- 微信支付:1589123456789012345(类似 Snowflake 的纯数字 ID)。 +- 美团订单:202506041234567890123(时间戳 + 商户 ID + 随机数)。 + +## UUID + +| 特性 | GUID/UUID | KSUID | ShortUUID | XID | Snowflake | +|-------|--------------|------------|-----------|----------|----------------| +| 长度 | 36/32字符(不含-) | 27字符 | 22字符 | 20字符 | 19(数字位数) | +| 有序性 | 无序(UUIDv4) | 严格时序 | 无序 | 趋势有序 | 严格时序 | +| 时间精度 | 无(UUIDv4) | 毫秒级 | 无 | 秒级 | 毫秒级 | +| 分布式安全 | 高(随机数) | 高 | 高 | 高 | 高(需配置WorkerID) | +| 性能 | 中等 | 中等 | 较低(编码开销) | 极高 | 极高 | +| 时钟依赖 | 无 | 有(需处理时钟回拨) | 无 | 有(但影响较小) | 强依赖(需严格同步) | +| 适用场景 | 跨系统兼容 | 时序索引 | 短ID、URL | 高并发、短ID | 分布式时序ID | + +## 选择建议 + +- **GUID/UUID**: 适用于需要跨系统兼容的场景,特别是当不需要有序性时。 +- **KSUID**: 适合需要严格时序的应用,如事件日志、时间序列数据。 +- **ShortUUID**: 当需要短ID且不关心有序性时的理想选择,适用于URL、短链接等。 +- **XID**: 高并发场景下的短ID选择,适合需要一定有序性的应用。 +- **Snowflake**: 适合分布式系统,特别是需要严格时序和高性能的场景,如大规模分布式应用。 diff --git a/id/go.mod b/id/go.mod new file mode 100644 index 0000000..12a79d8 --- /dev/null +++ b/id/go.mod @@ -0,0 +1,27 @@ +module github.com/tx7do/go-utils/id + +go 1.23.0 + +toolchain go1.23.2 + +require ( + github.com/bwmarrin/snowflake v0.3.0 + github.com/google/uuid v1.6.0 + github.com/lithammer/shortuuid/v4 v4.2.0 + github.com/rs/xid v1.6.0 + github.com/segmentio/ksuid v1.0.4 + github.com/stretchr/testify v1.10.0 + github.com/tx7do/go-utils v1.1.27 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sony/sonyflake v1.2.1 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/tx7do/go-utils => ../ diff --git a/id/go.sum b/id/go.sum new file mode 100644 index 0000000..24c711c --- /dev/null +++ b/id/go.sum @@ -0,0 +1,32 @@ +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= +github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48= +github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/id/order_id.go b/id/order_id.go new file mode 100644 index 0000000..8c02320 --- /dev/null +++ b/id/order_id.go @@ -0,0 +1,84 @@ +package id + +import ( + "fmt" + "math/rand" + "strings" + "sync/atomic" + "time" + + "github.com/tx7do/go-utils/trans" +) + +type idCounter uint32 + +func (c *idCounter) Increase() uint32 { + cur := *c + atomic.AddUint32((*uint32)(c), 1) + atomic.CompareAndSwapUint32((*uint32)(c), 1000, 0) + return uint32(cur) +} + +var orderIdIndex idCounter + +// GenerateOrderIdWithRandom 生成20位订单号,前缀 + 时间戳 + 随机数 +func GenerateOrderIdWithRandom(prefix string, tm *time.Time) string { + // 前缀 + 时间戳(14位) + 随机数(4位) + + if tm == nil { + tm = trans.Time(time.Now()) + } + + timestamp := tm.Format("20060102150405") + + randNum := rand.Intn(10000) // 生成0-9999之间的随机数 + + return fmt.Sprintf("%s%s%d", prefix, timestamp, randNum) +} + +// GenerateOrderIdWithIncreaseIndex 生成20位订单号,前缀+时间+自增长索引 +func GenerateOrderIdWithIncreaseIndex(prefix string, tm *time.Time) string { + if tm == nil { + tm = trans.Time(time.Now()) + } + + timestamp := tm.Format("20060102150405") + + index := orderIdIndex.Increase() + + return fmt.Sprintf("%s%s%d", prefix, timestamp, index) +} + +// GenerateOrderIdWithTenantId 带商户ID的订单ID生成器:202506041234567890123 +func GenerateOrderIdWithTenantId(tenantID string) string { + // 时间戳(14位) + 商户ID(固定 5 位) + 随机数(4位) + + // 时间戳部分(精确到毫秒) + now := time.Now() + timestamp := now.Format("20060102150405") + + // 商户ID部分(截取或补零到5位) + tenantPart := tenantID + if len(tenantPart) > 5 { + tenantPart = tenantPart[:5] + } else { + tenantPart = fmt.Sprintf("%-5s", tenantPart) + tenantPart = strings.ReplaceAll(tenantPart, " ", "0") + } + + // 随机数部分(4位) + n := rand.Int31n(10000) + randomPart := fmt.Sprintf("%04d", n) + + return timestamp + tenantPart + randomPart +} + +func GenerateOrderIdWithPrefixSonyflake(prefix string) string { + id, _ := NewSonyflakeID() + return fmt.Sprintf("%s%d", prefix, id) +} + +func GenerateOrderIdWithPrefixSnowflake(workerId int64, prefix string) string { + id, _ := NewSnowflakeID(workerId) + return fmt.Sprintf("%s%d", prefix, id) +} diff --git a/id/order_id_test.go b/id/order_id_test.go new file mode 100644 index 0000000..701722a --- /dev/null +++ b/id/order_id_test.go @@ -0,0 +1,158 @@ +package id + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateOrderIdWithRandom(t *testing.T) { + prefix := "PT" + + // 测试生成的订单号是否包含前缀 + orderID := GenerateOrderIdWithRandom(prefix, nil) + assert.Contains(t, orderID, prefix, "订单号应包含前缀") + t.Logf("GenerateOrderIdWithRandom: %s", orderID) + + // 测试生成的订单号长度是否正确 + assert.Equal(t, len(prefix)+14+4, len(orderID), "订单号长度应为前缀+时间戳+随机数") +} + +func TestGenerateOrderIdWithIndex(t *testing.T) { + prefix := "PT" + + tm := time.Now() + + fmt.Println(GenerateOrderIdWithIncreaseIndex(prefix, &(tm))) + + ids := make(map[string]bool) + count := 100 + for i := 0; i < count; i++ { + ids[GenerateOrderIdWithIncreaseIndex(prefix, &(tm))] = true + } + assert.Equal(t, count, len(ids)) +} + +func TestGenerateOrderIdWithIndexThread(t *testing.T) { + tm := time.Now() + + var wg sync.WaitGroup + var ids sync.Map + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + for i := 0; i < 100; i++ { + id := GenerateOrderIdWithIncreaseIndex("PT", &(tm)) + ids.Store(id, true) + } + wg.Done() + }() + } + wg.Wait() + + aLen := 0 + ids.Range(func(k, v interface{}) bool { + aLen++ + return true + }) + assert.Equal(t, 1000, aLen) +} + +func TestGenerateOrderIdWithTenantId(t *testing.T) { + tenantID := "M9876" + orderID := GenerateOrderIdWithTenantId(tenantID) + + t.Logf(orderID) + + // 验证订单号长度是否正确 + assert.Equal(t, 14+5+4, len(orderID)) + + // 验证时间戳部分是否正确 + timestamp := time.Now().Format("20060102150405") + assert.Contains(t, orderID, timestamp) + t.Logf("timestamp %d", len(timestamp)) + + // 验证商户ID部分是否正确 + assert.Contains(t, orderID, tenantID) + + // 验证随机数部分是否为4位数字 + randomPart := orderID[len(orderID)-4:] + assert.Regexp(t, `^\d{4}$`, randomPart) +} + +func TestGenerateOrderIdWithTenantIdCollision(t *testing.T) { + tenantID := "M9876" + count := 1000 // 生成订单号的数量 + ids := make(map[string]bool) + + for i := 0; i < count; i++ { + orderID := GenerateOrderIdWithTenantId(tenantID) + if ids[orderID] { + t.Errorf("碰撞的订单号: %s", orderID) + } + ids[orderID] = true + } + + t.Logf("生成了 %d 个订单号,没有发生碰撞", count) +} + +func TestGenerateOrderIdWithPrefixSonyflake(t *testing.T) { + prefix := "ORD" + orderID := GenerateOrderIdWithPrefixSonyflake(prefix) + t.Logf("order id with prefix sonyflake: %s [%d]", orderID, len(orderID)) + + // 验证订单号是否包含前缀 + assert.Contains(t, orderID, prefix, "订单号应包含前缀") + + // 验证订单号是否为有效的数字字符串 + assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字") +} + +func TestGenerateOrderIdWithPrefixSonyflakeCollision(t *testing.T) { + prefix := "ORD" + count := 100000 // 生成订单号的数量 + ids := make(map[string]bool) + + for i := 0; i < count; i++ { + orderID := GenerateOrderIdWithPrefixSonyflake(prefix) + if ids[orderID] { + t.Errorf("碰撞的订单号: %s", orderID) + } + ids[orderID] = true + } + + t.Logf("生成了 %d 个订单号,没有发生碰撞", count) +} + +func TestGenerateOrderIdWithPrefixSnowflake(t *testing.T) { + workerId := int64(1) // 假设使用的 workerId + prefix := "ORD" + orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix) + t.Logf("order id with prefix snowflake: %s [%d]", orderID, len(orderID)) + + // 验证订单号是否包含前缀 + assert.Contains(t, orderID, prefix, "订单号应包含前缀") + + // 验证订单号是否为有效的数字字符串 + assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字") +} + +func TestGenerateOrderIdWithPrefixSnowflakeCollision(t *testing.T) { + workerId := int64(1) // 假设使用的 workerId + prefix := "ORD" + count := 1000000 // 生成订单号的数量 + ids := make(map[string]bool) + + for i := 0; i < count; i++ { + orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix) + if ids[orderID] { + t.Errorf("碰撞的订单号: %s", orderID) + } + ids[orderID] = true + } + + t.Logf("生成了 %d 个订单号,没有发生碰撞", count) +} diff --git a/id/snowflake.go b/id/snowflake.go new file mode 100644 index 0000000..32007e8 --- /dev/null +++ b/id/snowflake.go @@ -0,0 +1,61 @@ +package id + +import ( + "errors" + "sync" + + "github.com/bwmarrin/snowflake" +) + +var snowflakeNodeMap = sync.Map{} + +type SnowflakeNode struct { + workerId int64 + node *snowflake.Node + sync.Mutex +} + +func NewSnowflakeNode(workerId int64) (*SnowflakeNode, error) { + node, err := snowflake.NewNode(workerId) + return &SnowflakeNode{ + workerId: workerId, + node: node, + Mutex: sync.Mutex{}, + }, err +} + +func (sfNode *SnowflakeNode) Generate() int64 { + sfNode.Lock() + defer sfNode.Unlock() + return sfNode.node.Generate().Int64() +} + +func (sfNode *SnowflakeNode) GenerateString() string { + sfNode.Lock() + defer sfNode.Unlock() + return sfNode.node.Generate().String() +} + +func NewSnowflakeID(workerId int64) (int64, error) { + // 64 位 ID = 41 位时间戳 + 10 位工作节点 ID + 12 位序列号 + + var node *SnowflakeNode + var err error + find, ok := snowflakeNodeMap.Load(workerId) + if ok { + node = find.(*SnowflakeNode) + } else { + node, err = NewSnowflakeNode(workerId) + if err != nil { + //log.Println(err) + return 0, err + } + snowflakeNodeMap.Store(workerId, node) + } + if node == nil { + //log.Println("snowflake node is nil") + return 0, errors.New("snowflake node is nil") + } + + return node.Generate(), nil +} diff --git a/id/sonyflake.go b/id/sonyflake.go new file mode 100644 index 0000000..18a675c --- /dev/null +++ b/id/sonyflake.go @@ -0,0 +1,25 @@ +package id + +import ( + "sync" + + "github.com/sony/sonyflake" +) + +var ( + sf *sonyflake.Sonyflake + sfMu sync.Mutex +) + +func NewSonyflakeID() (uint64, error) { + // 64 位 ID = 39 位时间戳 + 8 位机器 ID + 16 位序列号 + + sfMu.Lock() + defer sfMu.Unlock() + + if sf == nil { + sf = sonyflake.NewSonyflake(sonyflake.Settings{}) + } + + return sf.NextID() +} diff --git a/id/uuid.go b/id/uuid.go new file mode 100644 index 0000000..527d675 --- /dev/null +++ b/id/uuid.go @@ -0,0 +1,36 @@ +package id + +import ( + "strings" + + "github.com/google/uuid" + "github.com/lithammer/shortuuid/v4" + "github.com/rs/xid" + "github.com/segmentio/ksuid" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func NewGUIDv4(withHyphen bool) string { + id := uuid.NewString() + if !withHyphen { + id = strings.ReplaceAll(id, "-", "") + } + return id +} + +func NewShortUUID() string { + return shortuuid.New() +} + +func NewKSUID() string { + return ksuid.New().String() +} + +func NewXID() string { + return xid.New().String() +} + +func NewMongoObjectID() string { + objID := primitive.NewObjectID() + return objID.String() +} diff --git a/id/uuid_test.go b/id/uuid_test.go new file mode 100644 index 0000000..8fad5a1 --- /dev/null +++ b/id/uuid_test.go @@ -0,0 +1,266 @@ +package id + +import ( + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewGUIDv4(t *testing.T) { + // 测试带有连字符的 GUID + withHyphen := NewGUIDv4(true) + assert.NotEmpty(t, withHyphen) + assert.Equal(t, 4, strings.Count(withHyphen, "-"), "GUID 应包含 4 个连字符") + + // 测试不带连字符的 GUID + withoutHyphen := NewGUIDv4(false) + assert.NotEmpty(t, withoutHyphen) + assert.Equal(t, 0, strings.Count(withoutHyphen, "-"), "GUID 不应包含连字符") + + // 验证 GUID 的长度 + assert.Equal(t, 36, len(withHyphen), "带连字符的 GUID 长度应为 36") + assert.Equal(t, 32, len(withoutHyphen), "不带连字符的 GUID 长度应为 32") +} + +func TestNewGUIDv4CollisionRate(t *testing.T) { + const ( + testCount = 100000 // 测试生成的GUID数量 + withHyphen = true // 是否带连字符 + ) + + ids := make(map[string]struct{}) + for i := 0; i < testCount; i++ { + id := NewGUIDv4(withHyphen) + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %s 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个GUID,无碰撞。", testCount) +} + +func TestNewShortUUID(t *testing.T) { + // 测试生成的 ShortUUID 是否非空 + id := NewShortUUID() + assert.NotEmpty(t, id, "生成的 ShortUUID 应该非空") + + // 测试生成的 ShortUUID 的长度是否符合预期 + assert.True(t, len(id) > 0, "生成的 ShortUUID 长度应该大于 0") +} + +func TestNewShortUUIDCollisionRate(t *testing.T) { + const testCount = 100000 // 测试生成的ShortUUID数量 + + ids := make(map[string]struct{}) + for i := 0; i < testCount; i++ { + id := NewShortUUID() + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %s 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个ShortUUID,无碰撞。", testCount) +} + +func TestNewKSUID(t *testing.T) { + // 测试生成的 KSUID 是否非空 + id := NewKSUID() + assert.NotEmpty(t, id, "生成的 KSUID 应该非空") + + // 测试生成的 KSUID 的长度是否符合预期 + assert.Equal(t, 27, len(id), "生成的 KSUID 长度应该为 27") +} + +func TestNewKSUIDCollisionRate(t *testing.T) { + const testCount = 100000 // 测试生成的KSUID数量 + + ids := make(map[string]struct{}) + for i := 0; i < testCount; i++ { + id := NewKSUID() + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %s 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个KSUID,无碰撞。", testCount) +} + +func TestNewXID(t *testing.T) { + // 测试生成的 XID 是否非空 + id := NewXID() + assert.NotEmpty(t, id, "生成的 XID 应该非空") + + // 测试生成的 XID 的长度是否符合预期 + assert.Equal(t, 20, len(id), "生成的 XID 长度应该为 20") +} + +func TestNewXIDCollisionRate(t *testing.T) { + const testCount = 100000 // 测试生成的XID数量 + + ids := make(map[string]struct{}) + for i := 0; i < testCount; i++ { + id := NewXID() + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %s 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个XID,无碰撞。", testCount) +} + +func TestNewSnowflakeID(t *testing.T) { + tests := []struct { + workerId int64 + expectErr bool + }{ + {0, false}, // 有效的 workerId + {31, false}, // 有效的 workerId + {32, false}, // 有效的 workerId + + {-1, true}, // 无效的 workerId + } + + for _, tt := range tests { + id, err := NewSnowflakeID(tt.workerId) + if (err != nil) != tt.expectErr { + t.Errorf("NewSnowflakeID(%d) 错误状态不符合预期: %v", tt.workerId, err) + } + if err == nil && id <= 0 { + t.Errorf("NewSnowflakeID(%d) 生成的ID无效: %d", tt.workerId, id) + } + t.Logf("NewSnowflakeID(%d) ID: %d", tt.workerId, id) + } +} + +func TestNewSnowflakeIDCollisionRate(t *testing.T) { + const ( + workerId = 0 + testCount = 100000 // 测试生成的ID数量 + ) + + ids := make(map[int64]struct{}) + for i := 0; i < testCount; i++ { + id, err := NewSnowflakeID(workerId) + if err != nil { + t.Errorf("生成ID时出现错误: %v", err) + continue + } + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %d 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个Snowflake ID,无碰撞。", testCount) +} + +func TestConcurrentNewSnowflakeIDCollisionRate(t *testing.T) { + const ( + workerId = 0 // Snowflake 工作节点 ID + testCount = 100000 // 测试生成的 ID 数量 + workerCount = 10 // 并发工作线程数 + ) + + var mu sync.Mutex + ids := make(map[int64]struct{}) + var wg sync.WaitGroup + + for w := 0; w < workerCount; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < testCount/workerCount; i++ { + id, err := NewSnowflakeID(workerId) + if err != nil { + t.Errorf("生成 Snowflake ID 时出现错误: %v", err) + continue + } + mu.Lock() + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %d 已存在", id) + } + ids[id] = struct{}{} + mu.Unlock() + } + }() + } + + wg.Wait() + t.Logf("生成了 %d 个 Snowflake ID,无碰撞。", testCount) +} + +func TestNewSonyflakeID(t *testing.T) { + // 测试生成的 Sonyflake ID 是否有效 + id, err := NewSonyflakeID() + t.Logf("sonyflake id: %v", id) + assert.NoError(t, err, "生成 Sonyflake ID 时不应出现错误") + assert.True(t, id > 0, "生成的 Sonyflake ID 应该是正数") +} + +func TestNewSonyflakeIDCollisionRate(t *testing.T) { + const testCount = 100000 // 测试生成的 Sonyflake ID 数量 + + ids := make(map[uint64]struct{}) + for i := 0; i < testCount; i++ { + id, err := NewSonyflakeID() + if err != nil { + t.Errorf("生成 Sonyflake ID 时出现错误: %v", err) + continue + } + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %d 已存在", id) + } + ids[id] = struct{}{} + } + + t.Logf("生成了 %d 个 Sonyflake ID,无碰撞。", testCount) +} + +func TestConcurrentNewSonyflakeIDCollisionRate(t *testing.T) { + const ( + testCount = 100000 // 测试生成的 Sonyflake ID 数量 + workerCount = 10 // 并发工作线程数 + ) + + var mu sync.Mutex + ids := make(map[uint64]struct{}) + var wg sync.WaitGroup + + for w := 0; w < workerCount; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < testCount/workerCount; i++ { + id, err := NewSonyflakeID() + if err != nil { + t.Errorf("生成 Sonyflake ID 时出现错误: %v", err) + continue + } + mu.Lock() + if _, exists := ids[id]; exists { + t.Errorf("碰撞发生: %d 已存在", id) + } + ids[id] = struct{}{} + mu.Unlock() + } + }() + } + + wg.Wait() + t.Logf("生成了 %d 个 Sonyflake ID,无碰撞。", testCount) +} + +func TestNewMongoObjectID(t *testing.T) { + // 测试生成的 ObjectID 是否非空 + id := NewMongoObjectID() + assert.NotEmpty(t, id, "生成的 Mongo ObjectID 应该非空") + + // 测试生成的 ObjectID 的长度是否符合预期 + assert.Equal(t, 36, len(id), "生成的 Mongo ObjectID 长度应该为 36") +} diff --git a/order_id/id.go b/order_id/id.go deleted file mode 100644 index 3aa7da8..0000000 --- a/order_id/id.go +++ /dev/null @@ -1,47 +0,0 @@ -package order_id - -import ( - "fmt" - "math/rand" - "sync/atomic" - "time" - - "github.com/tx7do/go-utils/trans" -) - -type idCounter uint32 - -func (c *idCounter) Increase() uint32 { - cur := *c - atomic.AddUint32((*uint32)(c), 1) - atomic.CompareAndSwapUint32((*uint32)(c), 1000, 0) - return uint32(cur) -} - -var orderIdIndex idCounter - -// GenerateOrderIdWithRandom 生成20位订单号,前缀+时间+随机数 -func GenerateOrderIdWithRandom(prefix string, split string, tm *time.Time) string { - if tm == nil { - tm = trans.Time(time.Now()) - } - - index := rand.Intn(1000) - - return fmt.Sprintf("%s%s%.4d%s%.2d%s%.2d%s%.2d%s%.2d%s%.2d%s%.4d", prefix, split, - tm.Year(), split, tm.Month(), split, tm.Day(), split, - tm.Hour(), split, tm.Minute(), split, tm.Second(), split, index) -} - -// GenerateOrderIdWithIncreaseIndex 生成20位订单号,前缀+时间+自增长索引 -func GenerateOrderIdWithIncreaseIndex(prefix string, split string, tm *time.Time) string { - if tm == nil { - tm = trans.Time(time.Now()) - } - - index := orderIdIndex.Increase() - - return fmt.Sprintf("%s%s%.4d%s%.2d%s%.2d%s%.2d%s%.2d%s%.2d%s%.4d", prefix, split, - tm.Year(), split, tm.Month(), split, tm.Day(), split, - tm.Hour(), split, tm.Minute(), split, tm.Second(), split, index) -} diff --git a/order_id/id_test.go b/order_id/id_test.go deleted file mode 100644 index 53798b0..0000000 --- a/order_id/id_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package order_id - -import ( - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/tx7do/go-utils/trans" -) - -func TestGenerateOrderIdWithRandom(t *testing.T) { - fmt.Println(GenerateOrderIdWithRandom("PT", "-", trans.Time(time.Now()))) - - tm := time.Now() - var ids map[string]bool - ids = make(map[string]bool) - count := 100 - for i := 0; i < count; i++ { - ids[GenerateOrderIdWithRandom("PT", "", trans.Time(tm))] = true - } - assert.Equal(t, count, len(ids)) -} - -func TestGenerateOrderIdWithIndex(t *testing.T) { - tm := time.Now() - - fmt.Println(GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm))) - - ids := make(map[string]bool) - count := 100 - for i := 0; i < count; i++ { - ids[GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm))] = true - } - assert.Equal(t, count, len(ids)) -} - -func TestGenerateOrderIdWithIndexThread(t *testing.T) { - tm := time.Now() - - var wg sync.WaitGroup - var ids sync.Map - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - for i := 0; i < 100; i++ { - id := GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm)) - ids.Store(id, true) - } - wg.Done() - }() - } - wg.Wait() - - aLen := 0 - ids.Range(func(k, v interface{}) bool { - aLen++ - return true - }) - assert.Equal(t, 1000, aLen) -} diff --git a/slug/go.mod b/slug/go.mod new file mode 100644 index 0000000..86f55ce --- /dev/null +++ b/slug/go.mod @@ -0,0 +1,22 @@ +module github.com/tx7do/go-utils/slug + +go 1.23.0 + +toolchain go1.23.2 + +require ( + github.com/gosimple/slug v1.15.0 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/tx7do/go-utils => ../ diff --git a/slug/go.sum b/slug/go.sum new file mode 100644 index 0000000..e2fc4bc --- /dev/null +++ b/slug/go.sum @@ -0,0 +1,27 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sonyflake/sonyflake.go b/sonyflake/sonyflake.go deleted file mode 100644 index 7cc6561..0000000 --- a/sonyflake/sonyflake.go +++ /dev/null @@ -1,50 +0,0 @@ -package sonyflake - -import ( - "github.com/sony/sonyflake" - "time" -) - -var ( - sf *sonyflake.Sonyflake -) - -func startTime() time.Time { - return time.Now() -} - -func getMachineID() (uint16, error) { - return 0, nil -} - -func checkMachineID(uint16) bool { - return false -} - -// InitSonyflake 初始化Sonyflake节点单体 -func InitSonyflake() { - settings := sonyflake.Settings{ - /*StartTime: startTime(), - MachineID: getMachineID, - CheckMachineID: checkMachineID,*/ - } - sf = sonyflake.NewSonyflake(settings) - if sf == nil { - panic("sonyflake not created") - } -} - -// GenerateSonyflake 生成 Sonyflake ID -func GenerateSonyflake() uint64 { - if sf == nil { - InitSonyflake() - } - if sf == nil { - return 0 - } - id, err := sf.NextID() - if err != nil { - return 0 - } - return id -} diff --git a/sonyflake/sonyflake_test.go b/sonyflake/sonyflake_test.go deleted file mode 100644 index a883772..0000000 --- a/sonyflake/sonyflake_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package sonyflake - -import ( - "fmt" - "testing" - "time" -) - -func init() { - InitSonyflake() -} - -func TestGenerateSonyflake(t *testing.T) { - for i := 0; i < 100; i++ { - id := GenerateSonyflake() - fmt.Println(id) - } -} - -func TestGenerateTime(t *testing.T) { - // 秒 - fmt.Printf("时间戳(秒):%v;\n", time.Now().Unix()) - fmt.Printf("时间戳(纳秒转换为秒):%v;\n", time.Now().UnixNano()/1e9) - - // 毫秒 - fmt.Printf("时间戳(毫秒):%v;\n", time.Now().UnixMilli()) - fmt.Printf("时间戳(纳秒转换为毫秒):%v;\n", time.Now().UnixNano()/1e6) - - // 微秒 - fmt.Printf("时间戳(微秒):%v;\n", time.Now().UnixMicro()) - fmt.Printf("时间戳(纳秒转换为微秒):%v;\n", time.Now().UnixNano()/1e3) - - // 纳秒 - fmt.Printf("时间戳(纳秒):%v;\n", time.Now().UnixNano()) -} diff --git a/tag.bat b/tag.bat index e1983c4..f6aaba5 100644 --- a/tag.bat +++ b/tag.bat @@ -1,10 +1,12 @@ -git tag v1.1.27 +git tag v1.1.28 git tag bank_card/v1.1.5 git tag geoip/v1.1.5 git tag translator/v1.1.2 git tag copierutil/v0.0.5 git tag jwtutil/v0.0.2 +git tag id/v0.0.1 +git tag slug/v0.0.1 git tag entgo/v1.1.30 git tag gorm/v1.1.6 diff --git a/trans/trans.go b/trans/trans.go index f20c5e7..ece3028 100644 --- a/trans/trans.go +++ b/trans/trans.go @@ -1,6 +1,10 @@ package trans -import "time" +import ( + "time" + + "github.com/google/uuid" +) func String(a string) *string { return &a @@ -527,3 +531,28 @@ func MapValues[TKey mapKeyValueType, TValue mapKeyValueType](source map[TKey]TVa } return target } + +func ToUuidPtr(str *string) *uuid.UUID { + var id *uuid.UUID + if str != nil { + _id, err := uuid.Parse(*str) + if err != nil { + return nil + } + id = &_id + } + return id +} + +func ToUuid(str string) uuid.UUID { + id, _ := uuid.Parse(str) + return id +} + +func ToStringPtr(id *uuid.UUID) *string { + var strUUID *string + if id != nil { + strUUID = Ptr(id.String()) + } + return strUUID +} diff --git a/trans/trans_test.go b/trans/trans_test.go index c075321..341a7bf 100644 --- a/trans/trans_test.go +++ b/trans/trans_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -167,3 +168,62 @@ func Test_Trans(t *testing.T) { assert.Equal(t, time.Now(), tmVal) assert.Equal(t, time.Now(), TimeValue(nil)) } + +func TestUUID(t *testing.T) { + t.Run("ToUuidPtr_NilString", func(t *testing.T) { + var str *string + result := ToUuidPtr(str) + if result != nil { + t.Errorf("expected nil, got %v", result) + } + }) + + t.Run("ToUuidPtr_ValidString", func(t *testing.T) { + str := "550e8400-e29b-41d4-a716-446655440000" + result := ToUuidPtr(&str) + if result == nil || result.String() != str { + t.Errorf("expected %v, got %v", str, result) + } + }) + + t.Run("ToUuidPtr_InvalidString", func(t *testing.T) { + str := "invalid-uuid" + result := ToUuidPtr(&str) + if result != nil { + t.Errorf("expected nil, got %v", result) + } + }) + + t.Run("ToUuid_ValidString", func(t *testing.T) { + str := "550e8400-e29b-41d4-a716-446655440000" + result := ToUuid(str) + if result.String() != str { + t.Errorf("expected %v, got %v", str, result) + } + }) + + t.Run("ToUuid_InvalidString", func(t *testing.T) { + str := "invalid-uuid" + result := ToUuid(str) + if result.String() == str { + t.Errorf("expected invalid UUID, got %v", result) + } + }) + + t.Run("ToStringPtr_NilUUID", func(t *testing.T) { + var id *uuid.UUID + result := ToStringPtr(id) + if result != nil { + t.Errorf("expected nil, got %v", result) + } + }) + + t.Run("ToStringPtr_ValidUUID", func(t *testing.T) { + id := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") + result := ToStringPtr(&id) + expected := "550e8400-e29b-41d4-a716-446655440000" + if result == nil || *result != expected { + t.Errorf("expected %v, got %v", expected, result) + } + }) +} diff --git a/uuid/uuid.go b/uuid/uuid.go deleted file mode 100644 index 1efd4b8..0000000 --- a/uuid/uuid.go +++ /dev/null @@ -1,32 +0,0 @@ -package uuid - -import ( - "github.com/google/uuid" - - "github.com/tx7do/go-utils/trans" -) - -func ToUuidPtr(str *string) *uuid.UUID { - var id *uuid.UUID - if str != nil { - _id, err := uuid.Parse(*str) - if err != nil { - return nil - } - id = &_id - } - return id -} - -func ToUuid(str string) uuid.UUID { - id, _ := uuid.Parse(str) - return id -} - -func ToStringPtr(id *uuid.UUID) *string { - var strUUID *string - if id != nil { - strUUID = trans.String(id.String()) - } - return strUUID -} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go deleted file mode 100644 index e01edef..0000000 --- a/uuid/uuid_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package uuid - -import ( - "testing" - - "github.com/google/uuid" -) - -func TestUUID(t *testing.T) { - t.Run("ToUuidPtr_NilString", func(t *testing.T) { - var str *string - result := ToUuidPtr(str) - if result != nil { - t.Errorf("expected nil, got %v", result) - } - }) - - t.Run("ToUuidPtr_ValidString", func(t *testing.T) { - str := "550e8400-e29b-41d4-a716-446655440000" - result := ToUuidPtr(&str) - if result == nil || result.String() != str { - t.Errorf("expected %v, got %v", str, result) - } - }) - - t.Run("ToUuidPtr_InvalidString", func(t *testing.T) { - str := "invalid-uuid" - result := ToUuidPtr(&str) - if result != nil { - t.Errorf("expected nil, got %v", result) - } - }) - - t.Run("ToUuid_ValidString", func(t *testing.T) { - str := "550e8400-e29b-41d4-a716-446655440000" - result := ToUuid(str) - if result.String() != str { - t.Errorf("expected %v, got %v", str, result) - } - }) - - t.Run("ToUuid_InvalidString", func(t *testing.T) { - str := "invalid-uuid" - result := ToUuid(str) - if result.String() == str { - t.Errorf("expected invalid UUID, got %v", result) - } - }) - - t.Run("ToStringPtr_NilUUID", func(t *testing.T) { - var id *uuid.UUID - result := ToStringPtr(id) - if result != nil { - t.Errorf("expected nil, got %v", result) - } - }) - - t.Run("ToStringPtr_ValidUUID", func(t *testing.T) { - id := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") - result := ToStringPtr(&id) - expected := "550e8400-e29b-41d4-a716-446655440000" - if result == nil || *result != expected { - t.Errorf("expected %v, got %v", expected, result) - } - }) -}