yuid

A small, unique, URL-safe ID generator written in Go.
git clone git://git.lair.cx/yuid
Log | Files | Refs | README | LICENSE

commit 0b7579712a60d124264891ac9d3edf10d0af5598
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Fri, 25 Aug 2023 08:20:44 +0900

First Commit

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
A.gitignore | 25+++++++++++++++++++++++++
ALICENSE | 20++++++++++++++++++++
AREADME | 12++++++++++++
Agenerator.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agenerator_test.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 3+++
Aid.go | 11+++++++++++
Amathrand.go | 24++++++++++++++++++++++++
Amathrand_test.go | 12++++++++++++
9 files changed, 288 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,25 @@ +### Go.AllowList template +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. +# +# Recommended: Go.AllowList.gitignore + +# Ignore everything +* + +# But not these files... +!/.gitignore + +!*.go +!go.sum +!go.mod + +!README +!LICENSE + +# !Makefile + +# ...even if they are in subdirectories +!*/ + diff --git a/LICENSE b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Yongbin Kim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +\ No newline at end of file diff --git a/README b/README @@ -0,0 +1,12 @@ +A small, unique, URL-safe ID generator written in Go. + +## Usage + +```go +import "go.lair.cx/yuid" + +generator := yuid.NewGenerator() + +var id yuid.ID +generator.Next(&id) +``` diff --git a/generator.go b/generator.go @@ -0,0 +1,113 @@ +package yuid + +import ( + "io" + "sync" + "time" +) + +const ( + Characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" // Exactly 64 characters +) + +type Generator struct { + sync.Mutex + Source io.Reader + Now func() int64 + sequence uint8 + timestamp int64 +} + +type GeneratorOption func(*Generator) + +func NewGenerator(opts ...GeneratorOption) *Generator { + g := Generator{ + Source: InsecureSource{}, + Now: Now, + } + + for _, opt := range opts { + opt(&g) + } + + return &g +} + +func WithSource(source io.Reader) GeneratorOption { + return func(g *Generator) { + g.Source = source + } +} + +func WithNowFunc(now func() int64) GeneratorOption { + return func(g *Generator) { + g.Now = now + } +} + +func (g *Generator) Next(id *ID) error { + var ts int64 + var seq uint8 + + now := g.Now() + + g.Lock() + + if now > g.timestamp { + g.timestamp = now + g.sequence = 0 + } else if g.sequence == 255 { + g.timestamp++ + g.sequence = 0 + } else { + g.sequence++ + } + + ts = g.timestamp + seq = g.sequence + + _, err := io.ReadFull(g.Source, id[12:]) + + g.Unlock() + + if err != nil { + return err + } + + putHeader(id, ts, seq) + setBody(id) + + return nil +} + +func putHeader(id *ID, ts int64, seq uint8) { + id[0] = Characters[(ts>>58)&63] + id[1] = Characters[(ts>>52)&63] + id[2] = Characters[(ts>>46)&63] + id[3] = Characters[(ts>>40)&63] + id[4] = Characters[(ts>>34)&63] + id[5] = Characters[(ts>>28)&63] + id[6] = Characters[(ts>>22)&63] + id[7] = Characters[(ts>>16)&63] + id[8] = Characters[(ts>>10)&63] + id[9] = Characters[(ts>>4)&63] + id[10] = Characters[uint8((ts&15)<<2)|(seq>>6)] + id[11] = Characters[seq&63] +} + +func setBody(id *ID) { + for i := 12; i < IDSize; i++ { + id[i] = Characters[id[i]&63] + } +} + +func (g *Generator) MustNext(id *ID) { + err := g.Next(id) + if err != nil { + panic(err) + } +} + +func Now() int64 { + return time.Now().UnixMilli() +} diff --git a/generator_test.go b/generator_test.go @@ -0,0 +1,68 @@ +package yuid + +import ( + "testing" +) + +func TestGenerator_Next(t *testing.T) { + g := NewGenerator() + m := make(map[ID]struct{}) + id := ID{} + + for i := 0; i < 1000000; i++ { + err := g.Next(&id) + if err != nil { + t.Fatalf("Failed to generate ID: %v", err) + } + + if _, ok := m[id]; ok { + t.Fatalf("Duplicate ID found: %v", id) + } + + m[id] = struct{}{} + } +} + +func TestGenerator_MustNext(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Generator panicked: %v", r) + } + }() + + g := NewGenerator() + id := ID{} + + // Just calling it many times to see if any panic occurs + for i := 0; i < 1000000; i++ { + g.MustNext(&id) + } +} + +func BenchmarkGenerator_Next(b *testing.B) { + g := NewGenerator() + id := ID{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Next(&id) + } +} + +func BenchmarkPutHeader(b *testing.B) { + id := ID{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + putHeader(&id, 1234567890, 123) + } +} + +func BenchmarkSetBody(b *testing.B) { + id := ID{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + setBody(&id) + } +} diff --git a/go.mod b/go.mod @@ -0,0 +1,3 @@ +module go.lair.cx/yuid + +go 1.21.0 diff --git a/id.go b/id.go @@ -0,0 +1,11 @@ +package yuid + +const IDSize = 20 + +type ID [IDSize]byte + +//goland:noinspection GoUnusedGlobalVariable +var ( + // Nil is a nil ID. + Nil ID +) diff --git a/mathrand.go b/mathrand.go @@ -0,0 +1,24 @@ +package yuid + +import "math/rand" + +type InsecureSource struct{} + +func (InsecureSource) Read(p []byte) (n int, err error) { + var ( + val uint64 + pos int8 + ) + + for n = 0; n < len(p); n++ { + if pos == 0 { + val = rand.Uint64() + pos = 8 + } + p[n] = byte(val) + val >>= 8 + pos-- + } + + return +} diff --git a/mathrand_test.go b/mathrand_test.go @@ -0,0 +1,12 @@ +package yuid + +import "testing" + +func BenchmarkMathRand_Read(b *testing.B) { + r := InsecureSource{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = r.Read(make([]byte, 256)) + } +}