Skip to content

Commit 97e1dc6

Browse files
committed
Refactoring, restructuring, new tests, sequential guids
1 parent 0250d8b commit 97e1dc6

File tree

6 files changed

+891
-491
lines changed

6 files changed

+891
-491
lines changed

README.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ gxOQRIVR4B_uGHD6OP76XA
3535
Zo_hpnDxkOsAWLk1tIS6DA
3636
```
3737
---
38-
## Guid is ~10x faster than `github.com/google/uuid`
38+
## Guid is ~10x faster than `github.com/google/uuid` 🔥
3939

4040
* `guid.New()` is 6~10 ns
4141
* `guid.NewString()` is 40~60 ns
@@ -47,14 +47,78 @@ Zo_hpnDxkOsAWLk1tIS6DA
4747
| Function | Description |
4848
|---|---|
4949
| `guid.New()` | Generate a new Guid |
50-
| `guid.NewString()` | Generate a new Guid as Base64Url string |
50+
| `guid.NewString()` | Generate a new Guid as q Base64Url string |
5151
| `guid.Parse(s)` | Parse Base64Url string to Guid |
5252
| `guid.ParseBytes(b)` | Parse Base64Url bytes to Guid |
5353
| `guid.FromBytes(b)` | Parse 16-byte slice to Guid |
54-
| `guid.Read()` 🔥 | Faster alternative to `crypto/rand` |
54+
| `guid.Read()` 🔥 | Faster alternative to `crypto/rand` |
5555
| `guid.Nil` | The zero-value Guid |
5656

57-
## Benchmarks
57+
## Sequential Guids 🔥
58+
`guid` includes two special types `GuidPG` and `GuidSS` optimized for use as database primary keys (PostgreSQL and SQL Server). Their time-ordered composition helps prevent index fragmentation and improves `INSERT` performance compared to fully random Guids. Note that sequential sorting is only across `time.Now()` timestamp presision.
59+
60+
* **`guid.NewPG()`**: Generates a `GuidPG`, which is sortable in **PostgreSQL**. It is structured as `[8-byte timestamp][8 random bytes]`.
61+
* **`guid.NewSS()`**: Generates a `GuidSS`, which is sortable in **SQL Server**. It is structured as `[8 random bytes][8-byte SQL Server-ordered timestamp]`.
62+
* `.Timestamp()` on `GuidPG`/`GuidSS` returns Guid creation time as UTC `time.Time`.
63+
64+
Both `GuidPG` and `GuidSS` are nearly as fast as `guid.New()`. They can be used as a standard `Guid` and support the same interfaces.
65+
66+
***
67+
68+
### Sequential Guid Example:
69+
70+
```go
71+
fmt.Printf("%s\t %s\t\t\t\t%s\t %s\n",
72+
"gpg.String()", "hex(gpg)", "gss.String()", "hex(gss)")
73+
for range 10 {
74+
gpg := guid.NewPG()
75+
gss := guid.NewSS()
76+
fmt.Println(&gpg, hex.EncodeToString(gpg.Guid[:]), &gss, hex.EncodeToString(gss.Guid[:]))
77+
}
78+
79+
gpg := guid.NewPG()
80+
gss := guid.NewSS()
81+
fmt.Println(gpg.Timestamp()) // time.Time
82+
fmt.Println(gss.Timestamp()) // time.Time
83+
```
84+
```
85+
gpg.String() hex(gpg) gss.String() hex(gss)
86+
GFEU88wgQvDlahOowSGTKA 185114f3cc2042f0e56a13a8c1219328 9SurLKL6ti2l0BhRFPPMKA f52bab2ca2fab62da5d0185114f3cc28
87+
GFEU88wopdChlFba89-4yg 185114f3cc28a5d0a19456daf3dfb8ca yTRE6Rr1gISl0BhRFPPMKA c93444e91af58084a5d0185114f3cc28
88+
GFEU88ww9fA01GntVDQ_4w 185114f3cc30f5f034d469ed54343fe3 8SaILyee6q718BhRFPPMMA f126882f279eeaaef5f0185114f3cc30
89+
GFEU88ww9fASNFzZQJpv7Q 185114f3cc30f5f012345cd9409a6fed xZ3KYLzqJ0f18BhRFPPMMA c59dca60bcea2747f5f0185114f3cc30
90+
GFEU88ww9fAHgWvjAmkQJw 185114f3cc30f5f007816be302691027 yEif2kTQBcD18BhRFPPMMA c8489fda44d005c0f5f0185114f3cc30
91+
GFEU88ww9fD4_Vm3PG5Vuw 185114f3cc30f5f0f8fd59b73c6e55bb SRKgSiCc-gL18BhRFPPMMA 4912a04a209cfa02f5f0185114f3cc30
92+
GFEU88ww9fDzO_One7T6BA 185114f3cc30f5f0f33bf3a77bb4fa04 rGr2czgQcmr18BhRFPPMMA ac6af6733810726af5f0185114f3cc30
93+
GFEU88w5PqQAifEi5tqoWQ 185114f3cc393ea40089f122e6daa859 5YYbiI3p7P4-pBhRFPPMOQ e5861b888de9ecfe3ea4185114f3cc39
94+
GFEU88w5PqSFkX4bmxSvMQ 185114f3cc393ea485917e1b9b14af31 PqUPeiyessU-pBhRFPPMOQ 3ea50f7a2c9eb2c53ea4185114f3cc39
95+
GFEU88w5PqTsYX0kcZzL6Q 185114f3cc393ea4ec617d24719ccbe9 yFIlRwKZJNo-pBhRFPPMOQ c8522547029924da3ea4185114f3cc39
96+
2025-07-11 03:32:47.3597457 +0000 UTC
97+
2025-07-11 03:32:47.3597457 +0000 UTC
98+
```
99+
100+
## Interoperability with `google/uuid` 🔥
101+
* If you must keep using `google/uuid`, use `guid` to increase performance by **2~4x**:
102+
```go
103+
// do this before using google/uuid
104+
uuid.SetRand(guid.Reader);
105+
```
106+
107+
## uuid Benchmarks with and without `guid.Reader`
108+
109+
| Benchmark Name | Time per Op | Bytes per Op | Allocs per Op |
110+
|---|---|---|---|
111+
| Benchmark_uuid_New_x10-8 | 3031 ns/op | 160 B/op | 10 allocs/op |
112+
| Benchmark_uuid_New_**guidRand**_x10-8 🔥 | 862.0 ns/op | 160 B/op | 10 allocs/op |
113+
| Benchmark_uuid_New_RandPool_x10-8 | 747.6 ns/op | 0 B/op | 0 allocs/op |
114+
| Benchmark_uuid_New_RandPool_**guidRand**_x10-8 🔥 | 516.8 ns/op | 0 B/op | 0 allocs/op |
115+
| Benchmark_uuid_New_Parallel_x10-8 | 1230 ns/op | 160 B/op | 10 allocs/op |
116+
| Benchmark_uuid_New_Parallel_**guidRand**_x10-8 🔥 | 510.0 ns/op | 160 B/op | 10 allocs/op |
117+
| Benchmark_uuid_New_Parallel_RandPool_x10-8 | 1430 ns/op | 0 B/op | 0 allocs/op |
118+
| Benchmark_uuid_New_Parallel_RandPool_**guidRand**_x10-8 🔥 | 1185 ns/op | 0 B/op | 0 allocs/op |
119+
120+
121+
## Guid Benchmarks
58122
```
59123
go test -bench=.* -benchtime=4s
60124
goos: windows
@@ -67,7 +131,7 @@ cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
67131
| guid_New_x10-8 | 203.4 ns/op | 0 B/op | 0 allocs/op | |
68132
| guid_NewString_x10-8 | 582.4 ns/op | 240 B/op | 10 allocs/op | |
69133
| guid_String_x10-8 | 388.9 ns/op | 240 B/op | 10 allocs/op | |
70-
| guid_New_Parallel_x10-8 | 62.45 ns/op | 0 B/op | 0 allocs/op | |
134+
| guid_New_Parallel_x10-8 🔥 | 62.45 ns/op | 0 B/op | 0 allocs/op | |
71135
| guid_NewString_Parallel_x10-8 | 374.2 ns/op | 240 B/op | 10 allocs/op | |
72136

73137
### Alternative library benchmarks:

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/sdrapkin/guid
22

33
go 1.24
4+
5+
require golang.org/x/sys v0.34.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
2+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

guid.go

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"math/bits"
1213
"sync"
14+
"time"
1315
"unsafe"
16+
17+
"golang.org/x/sys/cpu"
1418
)
1519

1620
//==============================================
@@ -45,6 +49,8 @@ var (
4549
ErrInvalidBase64UrlGuidEncoding = errors.New("invalid Base64Url Guid encoding (invalid characters, or length != 22)")
4650
// ErrInvalidGuidSlice is returned when a byte slice cannot represent a valid Guid (length < 16 bytes).
4751
ErrInvalidGuidSlice = errors.New("invalid Guid slice (length < 16 bytes)")
52+
// ErrBufferTooSmallBase64Url is returned when a destination slice is too small to receive the text-encoded Guid.
53+
ErrBufferTooSmallBase64Url = fmt.Errorf("buffer is too small (length < %d bytes)", GuidBase64UrlByteSize)
4854
)
4955

5056
//==============================================
@@ -66,6 +72,19 @@ var (
6672

6773
// Guid is a 16-byte (128-bit) cryptographically random value.
6874
type Guid [GuidByteSize]byte
75+
76+
// GuidPG is a 16-byte (128-bit) PostgreSQL sortable Guid formed as [8-byte time.Now() timestamp][8 random bytes]
77+
// GuidPG is optimized for use as a PostgreSQL index key.
78+
type GuidPG struct {
79+
Guid // embedded
80+
}
81+
82+
// GuidSS is a 16-byte (128-bit) SQL Server sortable Guid formed as [8 random bytes][8 bytes of SQL Server ordered time.Now() timestamp]
83+
// GuidSS is optimized for use as a SQL Server index or clustered key.
84+
type GuidSS struct {
85+
Guid // embedded
86+
}
87+
6988
type reader struct{}
7089

7190
//==============================================
@@ -74,9 +93,9 @@ type reader struct{}
7493

7594
// guidCache holds a 4096-byte buffer and a byte index for Guid allocation.
7695
type guidCache struct {
77-
buffer [guidCacheByteSize]byte
96+
buffer []byte //buffer [guidCacheByteSize]byte
7897
index uint8
79-
_ [63]byte // pad ensures each index is on its own cache line
98+
_ [32]byte // pad ensures each index is on its own cache line
8099

81100
}
82101

@@ -109,7 +128,7 @@ func (guid *Guid) UnmarshalText(data []byte) error {
109128
// MarshalText implements encoding.TextMarshaler.
110129
func (guid *Guid) MarshalText() ([]byte, error) {
111130
buffer := make([]byte, GuidBase64UrlByteSize)
112-
guid.EncodeBase64URL(buffer)
131+
guid.encodeBase64URL(buffer)
113132
return buffer, nil
114133
}
115134

@@ -158,7 +177,16 @@ func (guid *Guid) String() string {
158177
}
159178

160179
// EncodeBase64URL encodes the Guid into the provided dst as Base64Url.
161-
func (guid *Guid) EncodeBase64URL(dst []byte) {
180+
func (guid *Guid) EncodeBase64URL(dst []byte) error {
181+
if len(dst) < GuidBase64UrlByteSize {
182+
return ErrBufferTooSmallBase64Url
183+
}
184+
guid.encodeBase64URL(dst)
185+
return nil
186+
}
187+
188+
// private - panics on undersized buffer
189+
func (guid *Guid) encodeBase64URL(dst []byte) {
162190
const lengthMod3 = 1 // 16 % 3 = 1
163191
const limit = GuidByteSize - lengthMod3 // 15 bytes can be processed in groups of 3 bytes, leaving 1 byte at the end.
164192

@@ -190,7 +218,7 @@ func (guid *Guid) EncodeBase64URL(dst []byte) {
190218
// It always fills b entirely, and returns len(b) and nil error.
191219
// guid.Read() is up to 7x faster than crypto/rand.Read() for small slices.
192220
// if b is > 512 bytes, it simply calls crypto/rand.Read().
193-
func (r *reader) Read(b []byte) (n int, err error) {
221+
func (r reader) Read(b []byte) (n int, err error) {
194222
const MaxBytesToFillViaGuids = 512
195223
n = len(b)
196224

@@ -206,7 +234,7 @@ func (r *reader) Read(b []byte) (n int, err error) {
206234

207235
if guidCacheRef.index == 0 {
208236
// Refill buffer if index wraps (Go 1.24+: cryptoRand.Read is guaranteed to succeed)
209-
cryptoRand.Read(guidCacheRef.buffer[:])
237+
cryptoRand.Read(guidCacheRef.buffer)
210238
}
211239

212240
n1 := copy(b, guidCacheRef.buffer[int(guidCacheRef.index)*GuidByteSize:])
@@ -219,8 +247,8 @@ func (r *reader) Read(b []byte) (n int, err error) {
219247
}
220248

221249
// Case 2: Need to refill for remainder
222-
cryptoRand.Read(guidCacheRef.buffer[:])
223-
n2 := copy(b[n1:], guidCacheRef.buffer[:])
250+
cryptoRand.Read(guidCacheRef.buffer)
251+
n2 := copy(b[n1:], guidCacheRef.buffer)
224252

225253
if n1+n2 != n {
226254
panic("guid: internal panic in reader.Read(); should never happen")
@@ -230,6 +258,35 @@ func (r *reader) Read(b []byte) (n int, err error) {
230258
return
231259
} //func (r *reader) Read
232260

261+
//==============================================
262+
// GuidPG Extension Methods
263+
//==============================================
264+
265+
// Timestamp extracts the timestamp from the PostgreSQL Guid.
266+
// The timestamp is stored in the first 8 bytes as nanoseconds since Unix epoch.
267+
// Returns the time.Time representation of when the Guid was created.
268+
func (g *GuidPG) Timestamp() time.Time {
269+
timestamp := *(*int64)(unsafe.Pointer(&g.Guid[0])) // Extract timestamp from first 8 bytes
270+
271+
if !cpu.IsBigEndian {
272+
timestamp = int64(bits.ReverseBytes64(uint64(timestamp)))
273+
}
274+
return time.Unix(0, timestamp).UTC()
275+
}
276+
277+
//==============================================
278+
// GuidSS Extension Methods
279+
//==============================================
280+
281+
// Timestamp extracts the timestamp from the SQL Server Guid.
282+
// The timestamp is stored in the last 8 bytes using SQL Server's Guid ordering rules.
283+
// Returns the time.Time representation of when the Guid was created.
284+
func (g *GuidSS) Timestamp() time.Time {
285+
encoded := *(*uint64)(unsafe.Pointer(&g.Guid[8])) // Extract timestamp from last 8 bytes (SQL Server format)
286+
timestamp := int64(bits.RotateLeft64(bits.ReverseBytes64(encoded), 16))
287+
return time.Unix(0, timestamp).UTC()
288+
}
289+
233290
//==============================================
234291
// Standalone Functions
235292
//==============================================
@@ -240,7 +297,7 @@ func New() (g Guid) {
240297

241298
if guidCacheRef.index == 0 {
242299
// Refill buffer if index wraps (Go 1.24+: cryptoRand.Read is guaranteed to succeed)
243-
cryptoRand.Read(guidCacheRef.buffer[:])
300+
cryptoRand.Read(guidCacheRef.buffer)
244301
}
245302

246303
// Extract GUID at current index
@@ -252,6 +309,34 @@ func New() (g Guid) {
252309
return
253310
}
254311

312+
// NewPG generates a new PostgreSQL sortable Guid as [8-byte time.Now() timestamp][8 random bytes]
313+
func NewPG() GuidPG {
314+
return newPG(time.Now().UnixNano())
315+
}
316+
317+
func newPG(ts int64) (gpg GuidPG) {
318+
gpg.Guid = New()
319+
if !cpu.IsBigEndian {
320+
ts = int64(bits.ReverseBytes64(uint64(ts)))
321+
}
322+
*(*uint64)(unsafe.Pointer(&gpg.Guid[0])) = uint64(ts)
323+
return
324+
}
325+
326+
// NewSS generates a new SQL Server sortable Guid as [8 random bytes][8 bytes of SQL Server ordered time.Now() timestamp]
327+
func NewSS() GuidSS {
328+
return newSS(time.Now().UnixNano())
329+
}
330+
331+
func newSS(ts int64) (gss GuidSS) {
332+
// based on Microsoft SqlGuid.cs
333+
// https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Data/System/Data/SQLTypes/SQLGuid.cs
334+
gss.Guid = New()
335+
// we don't worry about big-endian, because SQL Server does not run on big-endian
336+
*(*uint64)(unsafe.Pointer(&gss.Guid[8])) = bits.ReverseBytes64(bits.RotateLeft64(uint64(ts), -16))
337+
return
338+
}
339+
255340
// NewString generates a new cryptographically secure Guid, and returns it as a Base64Url string.
256341
// NewString is equivalent to "g := guid.New(); return g.String();".
257342
func NewString() string {
@@ -357,7 +442,7 @@ func Read(b []byte) (n int, err error) {
357442
// guidCachePool is a sync.Pool that holds guidCache instances.
358443
var guidCachePool = sync.Pool{
359444
New: func() any {
360-
return &guidCache{}
445+
return &guidCache{buffer: make([]byte, guidCacheByteSize)}
361446
},
362447
}
363448

0 commit comments

Comments
 (0)