UUIDs with the uuid Package

Updated

September 13, 2026

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 as uuid.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 IDs Compare as greater — useful for DB indexes that like time order.
  • Parse accepts dashed, undashed, braced, and urn:uuid: forms. MustParse is the same for constants and tests; it panics on bad input.
  • Nil() is all zeros. Max() is all 0xff. Neither is Go nil.
  • String is lowercase hex-and-dash. MarshalText / UnmarshalText make uuid.UUID JSON-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.go

Output:

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.go

Output:

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.go

Output:

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.go

Output:

{"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.go

Output:

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 add github.com/google/uuid for 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.Parse for anything from a client, file, or queue. Use uuid.MustParse only for constants and tests.
  • Compare with == for equality; use Compare (or slices.SortFunc) when order matters.
  • Remember Nil() marshals as a normal string. If “missing” must be absent from JSON, use a pointer or a separate omitzero strategy you design on purpose.

Try this

  1. In new_ticket.go, switch uuid.New() to uuid.NewV4() and confirm the shape checks still pass.
  2. In parse_ids.go, also parse {f81d4fae-7dec-11d0-a765-00a0c91e6bf6} and urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6. Are all three equal?
  3. Generate ten NewV7() values with no sleep between them. Do they still Compare non-decreasing? What does the package guarantee when calls share the same millisecond?
  4. Change Order.ID to *uuid.UUID with json:"id,omitzero" and marshal an order with a nil ID. Decide whether the desk API should omit the field or send Nil().