UUIDs with the uuid Package
UUIDs with the uuid Package
Every desk ticket needs an ID that will not collide with the next one. For years that meant importing github.com/google/uuid (or a cousin). Go 1.27 ships a first-party uuid package: RFC 9562 generation and parsing, cryptographically strong randomness, and no third-party module pin. Prefer it for new code on 1.27+.
Mental model
- A UUID is
[16]byte. Values are comparable with==. uuid.New()is the boring default. Today it is the same asuuid.NewV4()(122 bits of random data).uuid.NewV7()embeds a Unix timestamp in the high 48 bits plus random bits. Within one process with a forward clock, later V7 IDsCompareas greater — useful for DB indexes that like time order.Parseaccepts dashed, undashed, braced, andurn:uuid:forms.MustParseis the same for constants and tests; it panics on bad input.Nil()is all zeros.Max()is all0xff. Neither is Gonil.Stringis lowercase hex-and-dash.MarshalText/UnmarshalTextmakeuuid.UUIDJSON-friendly as a string.
Worked examples
Case 1: Assign a ticket ID with New
Save as new_ticket.go. New returns a non-nil UUID; the hex digits change every run, so we assert shape instead of a fixed string.
// new_ticket.go
package main
import (
"fmt"
"uuid"
)
type Ticket struct {
ID uuid.UUID
Title string
}
func main() {
t := Ticket{
ID: uuid.New(),
Title: "Replace keyboard",
}
s := t.ID.String()
fmt.Printf("title=%s\n", t.Title)
fmt.Printf("id length=%d\n", len(s))
fmt.Printf("nil? %v\n", t.ID == uuid.Nil())
fmt.Printf("dash positions ok? %v\n", s[8] == '-' && s[13] == '-' && s[18] == '-' && s[23] == '-')
}Run:
go run new_ticket.goOutput:
title=Replace keyboard
id length=36
nil? false
dash positions ok? true
Case 2: Parse and MustParse
Save as parse_ids.go. Untrusted strings go through Parse. Fixed fixtures in tests use MustParse.
// parse_ids.go
package main
import (
"fmt"
"uuid"
)
func main() {
raw := "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
id, err := uuid.Parse(raw)
if err != nil {
fmt.Println("parse:", err)
return
}
fmt.Println(id.String())
compact := "f81d4fae7dec11d0a76500a0c91e6bf6"
id2, err := uuid.Parse(compact)
if err != nil {
fmt.Println("compact:", err)
return
}
fmt.Printf("same? %v\n", id == id2)
constFixture := uuid.MustParse("00000000-0000-4000-8000-000000000001")
fmt.Println(constFixture)
}Run:
go run parse_ids.goOutput:
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
same? true
00000000-0000-4000-8000-000000000001
Case 3: Time-ordered IDs with NewV7
Save as v7_order.go. Create three V7 IDs a few milliseconds apart. Compare uses the RFC big-endian byte order, so creation order sorts cleanly when the clock moves forward.
// v7_order.go
package main
import (
"fmt"
"slices"
"time"
"uuid"
)
func main() {
ids := make([]uuid.UUID, 0, 3)
for range 3 {
ids = append(ids, uuid.NewV7())
time.Sleep(2 * time.Millisecond)
}
fmt.Println("compare consecutive:")
for i := 0; i < len(ids)-1; i++ {
fmt.Printf(" %d vs %d → %d\n", i, i+1, ids[i].Compare(ids[i+1]))
}
shuffled := []uuid.UUID{ids[2], ids[0], ids[1]}
slices.SortFunc(shuffled, func(a, b uuid.UUID) int {
return a.Compare(b)
})
sorted := shuffled[0].Compare(ids[0]) == 0 &&
shuffled[1].Compare(ids[1]) == 0 &&
shuffled[2].Compare(ids[2]) == 0
fmt.Printf("sort restored creation order? %v\n", sorted)
}Run:
go run v7_order.goOutput:
compare consecutive:
0 vs 1 → -1
1 vs 2 → -1
sort restored creation order? true
If the system clock steps backward, NewV7 may stop producing strictly increasing values until time catches up. Do not treat V7 as a global Lamport clock across machines without reading the RFC.
Case 4: JSON round-trip on an order
Save as order_json.go. uuid.UUID implements text marshaling, so encoding/json stores it as a string.
// order_json.go
package main
import (
"encoding/json"
"fmt"
"uuid"
)
type Order struct {
ID uuid.UUID `json:"id"`
Item string `json:"item"`
Cents int `json:"cents"`
}
func main() {
o := Order{
ID: uuid.MustParse("0199a1b2-c3d4-7000-8000-111111111111"),
Item: "monitor",
Cents: 34900,
}
raw, err := json.Marshal(o)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(raw))
var back Order
if err := json.Unmarshal(raw, &back); err != nil {
fmt.Println(err)
return
}
fmt.Printf("round-trip ok? %v item=%s\n", back.ID == o.ID, back.Item)
}Run:
go run order_json.goOutput:
{"id":"0199a1b2-c3d4-7000-8000-111111111111","item":"monitor","cents":34900}
round-trip ok? true item=monitor
The trap
Save as mustparse_trap.go. Two easy mistakes: treating the zero UUID as “no value” without naming it, and calling MustParse on user input.
// mustparse_trap.go
package main
import (
"fmt"
"uuid"
)
func main() {
var zero uuid.UUID
fmt.Printf("zero == Nil()? %v\n", zero == uuid.Nil())
fmt.Printf("Nil: %s\n", uuid.Nil())
fmt.Printf("Max: %s\n", uuid.Max())
raw := "desk-ticket-7"
id, err := uuid.Parse(raw)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
} else {
fmt.Println(id)
}
defer func() {
if r := recover(); r != nil {
fmt.Println("MustParse panicked (expected):", r)
}
}()
_ = uuid.MustParse(raw)
}Run:
go run mustparse_trap.goOutput:
zero == Nil()? true
Nil: 00000000-0000-0000-0000-000000000000
Max: ffffffff-ffff-ffff-ffff-ffffffffffff
Parse error: invalid uuid
MustParse panicked (expected): invalid uuid
The zero value is Nil(). That is fine for “unset” if your API documents it. It is not Go nil, and it is a valid UUID string on the wire — clients will see sixteen zeros. Prefer Parse at trust boundaries; reserve MustParse for literals you control.
The boring rule
- Import
"uuid"on Go 1.27+. Do not addgithub.com/google/uuidfor new modules unless you need an API the stdlib does not provide. - Use
uuid.New()for opaque IDs (tickets, request correlation, row keys that do not need time order). - Use
uuid.NewV7()when lexicographic / time-ish order helps indexes — and document the clock caveat. - Use
uuid.Parsefor anything from a client, file, or queue. Useuuid.MustParseonly for constants and tests. - Compare with
==for equality; useCompare(orslices.SortFunc) when order matters. - Remember
Nil()marshals as a normal string. If “missing” must be absent from JSON, use a pointer or a separateomitzerostrategy you design on purpose.
Try this
- In
new_ticket.go, switchuuid.New()touuid.NewV4()and confirm the shape checks still pass. - In
parse_ids.go, also parse{f81d4fae-7dec-11d0-a765-00a0c91e6bf6}andurn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6. Are all three equal? - Generate ten
NewV7()values with no sleep between them. Do they stillComparenon-decreasing? What does the package guarantee when calls share the same millisecond? - Change
Order.IDto*uuid.UUIDwithjson:"id,omitzero"and marshal an order with a nil ID. Decide whether the desk API should omit the field or sendNil().