├── .github ├── .dependabot.yml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE.md ├── README.md ├── bench ├── README.md ├── benchmark_test.go ├── go.mod └── go.sum ├── cmd └── uuid │ └── main.go ├── encoding.go ├── encoding_test.go ├── example └── example.go ├── go.mod ├── go.sum ├── uuid.go └── uuid_test.go /.github/.dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: ".github/workflows" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: stable 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: goreleaser 3 | 4 | on: 5 | push: 6 | # run only against tags 7 | tags: 8 | - "*" 9 | 10 | permissions: 11 | contents: write 12 | # packages: write 13 | # issues: write 14 | # id-token: write 15 | 16 | jobs: 17 | goreleaser: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: stable 28 | # More assembly might be required: Docker logins, GPG, etc. 29 | # It all depends on your needs. 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v6 32 | with: 33 | # either 'goreleaser' (default) or 'goreleaser-pro' 34 | distribution: goreleaser 35 | # 'latest', 'nightly', or a semver 36 | version: "~> v2" 37 | args: release --clean 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 41 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,intellij+all,visualstudiocode,go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,intellij+all,visualstudiocode,go 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### Intellij+all ### 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # AWS User-specific 39 | .idea/**/aws.xml 40 | 41 | # Generated files 42 | .idea/**/contentModel.xml 43 | 44 | # Sensitive or high-churn files 45 | .idea/**/dataSources/ 46 | .idea/**/dataSources.ids 47 | .idea/**/dataSources.local.xml 48 | .idea/**/sqlDataSources.xml 49 | .idea/**/dynamic.xml 50 | .idea/**/uiDesigner.xml 51 | .idea/**/dbnavigator.xml 52 | 53 | # Gradle 54 | .idea/**/gradle.xml 55 | .idea/**/libraries 56 | 57 | # Gradle and Maven with auto-import 58 | # When using Gradle or Maven with auto-import, you should exclude module files, 59 | # since they will be recreated, and may cause churn. Uncomment if using 60 | # auto-import. 61 | # .idea/artifacts 62 | # .idea/compiler.xml 63 | # .idea/jarRepositories.xml 64 | # .idea/modules.xml 65 | # .idea/*.iml 66 | # .idea/modules 67 | # *.iml 68 | # *.ipr 69 | 70 | # CMake 71 | cmake-build-*/ 72 | 73 | # Mongo Explorer plugin 74 | .idea/**/mongoSettings.xml 75 | 76 | # File-based project format 77 | *.iws 78 | 79 | # IntelliJ 80 | out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # SonarLint plugin 92 | .idea/sonarlint/ 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | # Editor-based Rest Client 101 | .idea/httpRequests 102 | 103 | # Android studio 3.1+ serialized cache file 104 | .idea/caches/build_file_checksums.ser 105 | 106 | ### Intellij+all Patch ### 107 | # Ignore everything but code style settings and run configurations 108 | # that are supposed to be shared within teams. 109 | 110 | .idea/* 111 | 112 | !.idea/codeStyles 113 | !.idea/runConfigurations 114 | 115 | ### Linux ### 116 | *~ 117 | 118 | # temporary files which can be created if a process still has a handle open of a deleted file 119 | .fuse_hidden* 120 | 121 | # KDE directory preferences 122 | .directory 123 | 124 | # Linux trash folder which might appear on any partition or disk 125 | .Trash-* 126 | 127 | # .nfs files are created when an open file is removed but is still being accessed 128 | .nfs* 129 | 130 | ### macOS ### 131 | # General 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | 136 | # Icon must end with two \r 137 | Icon 138 | 139 | 140 | # Thumbnails 141 | ._* 142 | 143 | # Files that might appear in the root of a volume 144 | .DocumentRevisions-V100 145 | .fseventsd 146 | .Spotlight-V100 147 | .TemporaryItems 148 | .Trashes 149 | .VolumeIcon.icns 150 | .com.apple.timemachine.donotpresent 151 | 152 | # Directories potentially created on remote AFP share 153 | .AppleDB 154 | .AppleDesktop 155 | Network Trash Folder 156 | Temporary Items 157 | .apdisk 158 | 159 | ### macOS Patch ### 160 | # iCloud generated files 161 | *.icloud 162 | 163 | ### VisualStudioCode ### 164 | .vscode/* 165 | !.vscode/settings.json 166 | !.vscode/tasks.json 167 | !.vscode/launch.json 168 | !.vscode/extensions.json 169 | !.vscode/*.code-snippets 170 | 171 | # Local History for Visual Studio Code 172 | .history/ 173 | 174 | # Built Visual Studio Code Extensions 175 | *.vsix 176 | 177 | ### VisualStudioCode Patch ### 178 | # Ignore all local history of files 179 | .history 180 | .ionide 181 | 182 | ### Windows ### 183 | # Windows thumbnail cache files 184 | Thumbs.db 185 | Thumbs.db:encryptable 186 | ehthumbs.db 187 | ehthumbs_vista.db 188 | 189 | # Dump file 190 | *.stackdump 191 | 192 | # Folder config file 193 | [Dd]esktop.ini 194 | 195 | # Recycle Bin used on file shares 196 | $RECYCLE.BIN/ 197 | 198 | # Windows Installer files 199 | *.cab 200 | *.msi 201 | *.msix 202 | *.msm 203 | *.msp 204 | 205 | # Windows shortcuts 206 | *.lnk 207 | 208 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,intellij+all,visualstudiocode,go 209 | dist/ 210 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | 8 | builds: 9 | - id: "uuid" 10 | main: ./cmd/uuid/main.go 11 | binary: uuid 12 | env: 13 | - CGO_ENABLED=0 14 | goos: 15 | - linux 16 | - darwin 17 | - windows 18 | archives: 19 | - format: binary 20 | 21 | changelog: 22 | sort: asc 23 | filters: 24 | exclude: 25 | - "^docs:" 26 | - "^test:" 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Cole MacKenzie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-uuid 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/cmackenzie1/go-uuid.svg)](https://pkg.go.dev/github.com/cmackenzie1/go-uuid) 4 | ![go workflow](https://github.com/cmackenzie1/go-uuid/actions/workflows/go.yml/badge.svg) 5 | 6 | A simple, stdlib only, go module for generating version 4 (random) and version 7 (time-based) UUIDs (**U**niversally **U**nique **ID**entifiers). This library is based on the [RFC9562](https://www.rfc-editor.org/rfc/rfc9562.html) specification. 7 | 8 | ## Installation 9 | 10 | ### CLI 11 | 12 | ```bash 13 | # Using go install 14 | go install github.com/cmackenzie1/go-uuid/cmd/uuid@latest 15 | ``` 16 | 17 | Or download the binary from [Releases](https://github.com/cmackenzie1/go-uuid/releases) 18 | 19 | ### Package 20 | 21 | ```bash 22 | go get github.com/cmackenzie1/go-uuid 23 | ``` 24 | 25 | ## Supported versions 26 | 27 | | Version | Variant | Details | 28 | | ----------- | ------- | ----------------------------------------------------------------------------------- | 29 | | `Version 4` | `10` | Pure random as defined in [RFC9562](https://www.rfc-editor.org/rfc/rfc9562.html). | 30 | | `Version 7` | `10` | Time-sortable as defined in [RFC9562](https://www.rfc-editor.org/rfc/rfc9562.html). | 31 | 32 | ## Usage 33 | 34 | ```go 35 | // example/example.go 36 | package main 37 | 38 | import ( 39 | "fmt" 40 | 41 | "github.com/cmackenzie1/go-uuid" 42 | ) 43 | 44 | func main() { 45 | v4, err := uuid.NewV4() 46 | if err != nil { 47 | panic(err) 48 | } 49 | fmt.Printf("UUIDv4: %s\n", v4) // c07526de-40e5-418f-93d1-73ba20d2ac2c 50 | 51 | v7, _ := uuid.NewV7() 52 | if err != nil { 53 | panic(err) 54 | } 55 | fmt.Printf("UUIDv7: %s\n", v7) // 0185e1af-a3c1-704f-80f5-6fd2f8387f09 56 | } 57 | 58 | ``` 59 | 60 | ## FAQ 61 | 62 | ### What are the benefits of this library over X? 63 | 64 | - A single library with no external dependencies for multiple types of UUIDs. 65 | - `UUID` type is defined as a fixed-size, `[16]byte`, array which can be used as a map key (instead of the 36 byte 66 | string representation). Over 2x space savings for memory! 67 | - Limited API. As per RFC9562, UUIDs (while containing embedded information), should be treated as opaque 68 | values. There is no temptation to build dependencies on the embedded information if you can't easily access it. 😉 69 | 70 | ### When should I use UUIDv7 over UUIDv4? 71 | 72 | > Non-time-ordered UUID versions such as UUIDv4 have poor database index locality. This means that new 73 | > values created in succession are not close to each other in the index and thus require inserts to be performed at 74 | > random locations. The negative performance effects of which on common structures used for this (B-tree and its 75 | > variants) can be dramatic. [1] 76 | 77 | **tl;dr**: if you intend to use the UUID as a database key, use UUIDv7. If you require 78 | purely random IDs, use UUIDv4. 79 | 80 | ## Contributing 81 | 82 | Pull requests are welcome. For major changes, please open an issue first 83 | to discuss what you would like to change. 84 | 85 | Please make sure to update tests as appropriate. 86 | 87 | ## License 88 | 89 | [MIT](./LICENSE.md) 90 | 91 | [1]: https://www.rfc-editor.org/rfc/rfc9562.html#section-2.1 92 | -------------------------------------------------------------------------------- /bench/README.md: -------------------------------------------------------------------------------- 1 | # go-uuid Benchmarks 2 | 3 | This directory contains benchmarks for the go-uuid package, comparing it to `google/uuid` and `gofrs/uuid`. 4 | This package does not claim to be the fastest, or the best, so these benchmarks are provided to give you an idea of how 5 | it compares to other packages. 6 | 7 | For the benchmarks, we will be comparing generating a UUID and serializing it to a string. 8 | 9 | ## Running Benchmarks 10 | 11 | To run the benchmarks, execute the following command: 12 | 13 | ```bash 14 | go test -benchmem -bench=. 15 | ``` 16 | 17 | ## Results 18 | 19 | The results of the benchmarks are as follows: 20 | 21 | ```bash 22 | $ go test -benchmem -bench=. 23 | goos: darwin 24 | goarch: arm64 25 | pkg: github.com/cmackenzie1/go-uuid/bench 26 | cpu: Apple M2 27 | BenchmarkNewV4-8 3954818 292.8 ns/op 64 B/op 2 allocs/op 28 | BenchmarkGoogleV4-8 4062295 292.6 ns/op 64 B/op 2 allocs/op 29 | BenchmarkGofrsV4-8 4227584 282.5 ns/op 64 B/op 2 allocs/op 30 | BenchmarkNewV7-8 7648292 157.6 ns/op 64 B/op 2 allocs/op 31 | BenchmarkGoogleV7-8 3550208 335.3 ns/op 64 B/op 2 allocs/op 32 | BenchmarkGofrsV7-8 7195696 167.5 ns/op 64 B/op 2 allocs/op 33 | PASS 34 | ok github.com/cmackenzie1/go-uuid/bench 8.897s 35 | ``` 36 | -------------------------------------------------------------------------------- /bench/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cmackenzie1/go-uuid" 7 | gofrs "github.com/gofrs/uuid/v5" 8 | google "github.com/google/uuid" 9 | ) 10 | 11 | // Version 4 UUID benchmarks 12 | 13 | func BenchmarkNewV4(b *testing.B) { 14 | for i := 0; i < b.N; i++ { 15 | a, _ := uuid.NewV4() 16 | _ = a.String() // prevent compiler optimization 17 | } 18 | } 19 | 20 | func BenchmarkGoogleV4(b *testing.B) { 21 | for i := 0; i < b.N; i++ { 22 | a, _ := google.NewRandom() 23 | _ = a.String() // prevent compiler optimization 24 | } 25 | } 26 | 27 | func BenchmarkGofrsV4(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | a, _ := gofrs.NewV4() 30 | _ = a.String() // prevent compiler optimization 31 | } 32 | } 33 | 34 | // Version 7 UUID benchmarks 35 | 36 | func BenchmarkNewV7(b *testing.B) { 37 | for i := 0; i < b.N; i++ { 38 | a, _ := uuid.NewV7() 39 | _ = a.String() // prevent compiler optimization 40 | } 41 | } 42 | 43 | func BenchmarkGoogleV7(b *testing.B) { 44 | for i := 0; i < b.N; i++ { 45 | a, _ := google.NewV7() 46 | _ = a.String() // prevent compiler optimization 47 | } 48 | } 49 | 50 | func BenchmarkGofrsV7(b *testing.B) { 51 | for i := 0; i < b.N; i++ { 52 | a, _ := gofrs.NewV7() 53 | _ = a.String() // prevent compiler optimization 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bench/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cmackenzie1/go-uuid/bench 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/cmackenzie1/go-uuid v1.1.3 7 | github.com/gofrs/uuid/v5 v5.3.0 8 | github.com/google/uuid v1.6.0 9 | ) 10 | 11 | replace github.com/cmackenzie1/go-uuid => ../ 12 | -------------------------------------------------------------------------------- /bench/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= 2 | github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | -------------------------------------------------------------------------------- /cmd/uuid/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/cmackenzie1/go-uuid" 10 | ) 11 | 12 | func run(version int, count int, uppercase bool) error { 13 | for i := 0; i < count; i++ { 14 | var id uuid.UUID 15 | var err error 16 | if version == 4 { 17 | id, err = uuid.NewV4() 18 | } else { 19 | id, err = uuid.NewV7() 20 | } 21 | if err != nil { 22 | return err 23 | } 24 | if uppercase { 25 | fmt.Println(strings.ToUpper(id.String())) 26 | } else { 27 | fmt.Println(id) 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func main() { 34 | v := flag.Int("v", 4, "UUID version to generate.Supported versions are 4 and 7.") 35 | n := flag.Int("c", 1, "Number of UUIDs to generate.") 36 | u := flag.Bool("u", false, "Print UUIDs in uppercase.") 37 | flag.Parse() 38 | 39 | if *v != 4 && *v != 7 { 40 | fmt.Printf("Unsupported UUID version: %d\n", *v) 41 | os.Exit(1) 42 | } 43 | 44 | if *n < 1 { 45 | fmt.Printf("Number of UUIDs to generate must be greater than 0.\n") 46 | os.Exit(1) 47 | } 48 | 49 | if err := run(*v, *n, *u); err != nil { 50 | fmt.Println(err) 51 | os.Exit(1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | func (uuid UUID) MarshalText() ([]byte, error) { 4 | return []byte(uuid.String()), nil 5 | } 6 | 7 | func (uuid *UUID) UnmarshalText(text []byte) error { 8 | if len(text) == 0 { 9 | *uuid = UUID{} 10 | return nil 11 | } 12 | u, err := Parse(string(text)) 13 | if err != nil { 14 | return err 15 | } 16 | *uuid = u 17 | return err 18 | } 19 | -------------------------------------------------------------------------------- /encoding_test.go: -------------------------------------------------------------------------------- 1 | package uuid_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/cmackenzie1/go-uuid" 9 | ) 10 | 11 | type exampleJSON struct { 12 | ID uuid.UUID `json:"id"` 13 | } 14 | 15 | func TestNewV4_MarshalJSON(t *testing.T) { 16 | tests := map[string]struct { 17 | uuid string 18 | want string 19 | }{ 20 | "valid": { 21 | uuid: "b5ae3fb7-9cf5-4220-b040-069badaa0092", 22 | want: "{\"id\":\"b5ae3fb7-9cf5-4220-b040-069badaa0092\"}", 23 | }, 24 | "invalid": { 25 | uuid: "b5ae3fb7-9cf5-b040", 26 | want: "{\"id\":\"00000000-0000-0000-0000-000000000000\"}", 27 | }, 28 | "nil": { 29 | uuid: "00000000-0000-0000-0000-000000000000", 30 | want: "{\"id\":\"00000000-0000-0000-0000-000000000000\"}", 31 | }, 32 | } 33 | for name, tt := range tests { 34 | t.Run(name, func(t *testing.T) { 35 | u, _ := uuid.Parse(tt.uuid) 36 | ex := exampleJSON{ID: u} 37 | got, err := json.Marshal(ex) 38 | if err != nil { 39 | t.Errorf("json.Marshal() failed: %v", err) 40 | return 41 | } 42 | if !bytes.Equal(got, []byte(tt.want)) { 43 | t.Errorf("got = %s, wanted = %s", got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestNewV4_UnmarshalJSON(t *testing.T) { 50 | tests := map[string]struct { 51 | uuid string 52 | want func() exampleJSON 53 | wantErr bool 54 | }{ 55 | "valid": { 56 | uuid: "{\"id\":\"b5ae3fb7-9cf5-4220-b040-069badaa0092\"}", 57 | want: func() exampleJSON { 58 | u, _ := uuid.Parse("b5ae3fb7-9cf5-4220-b040-069badaa0092") 59 | return exampleJSON{ID: u} 60 | }, 61 | }, 62 | "invalid": { 63 | uuid: "{\"id\":\"b5ae3fb7-9cf5\"}", 64 | want: func() exampleJSON { 65 | return exampleJSON{ID: uuid.Nil} 66 | }, 67 | wantErr: true, 68 | }, 69 | "nil": { 70 | uuid: "{\"id\":\"\"}", 71 | want: func() exampleJSON { 72 | return exampleJSON{ID: uuid.Nil} 73 | }, 74 | }, 75 | "null": { 76 | uuid: "{\"id\":null}", 77 | want: func() exampleJSON { 78 | return exampleJSON{ID: uuid.Nil} 79 | }, 80 | }, 81 | } 82 | 83 | for name, tt := range tests { 84 | t.Run(name, func(t *testing.T) { 85 | got := exampleJSON{} 86 | err := json.Unmarshal([]byte(tt.uuid), &got) 87 | if (err != nil) != tt.wantErr { 88 | t.Errorf("json.Unmarshal() failed: %v", err) 89 | return 90 | } 91 | if got.ID != tt.want().ID { 92 | t.Errorf("got = %s, wanted = %s", got, tt.want()) 93 | return 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cmackenzie1/go-uuid" 7 | ) 8 | 9 | func main() { 10 | v4, err := uuid.NewV4() 11 | if err != nil { 12 | panic(err) 13 | } 14 | fmt.Printf("UUIDv4: %s\n", v4) // c07526de-40e5-418f-93d1-73ba20d2ac2c 15 | 16 | v7, err := uuid.NewV7() 17 | if err != nil { 18 | panic(err) 19 | } 20 | fmt.Printf("UUIDv7: %s\n", v7) // 0185e1af-a3c1-704f-80f5-6fd2f8387f09 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cmackenzie1/go-uuid 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmackenzie1/go-uuid/57110dcf18284d3373f036da20f58f38477ab98e/go.sum -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | errInvalidLength = errors.New("invalid length") 15 | errInvalidFormat = errors.New("invalid format") 16 | errUnsupportedVersion = errors.New("unsupported version") 17 | errUnsupportedVariant = errors.New("unsupported variant") 18 | ) 19 | 20 | // UUID is a 128 bit (16 byte) value defined by RFC9562. 21 | type UUID [16]byte 22 | 23 | // Nil represents the zero-value UUID 24 | var Nil UUID 25 | 26 | // NewV4 returns a Version 4 UUID as defined in [RFC9562]. Random bits 27 | // are generated using [crypto/rand]. 28 | // 29 | // 0 1 2 3 30 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 31 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 32 | // | random_a | 33 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 34 | // | random_a | ver | random_b | 35 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 36 | // |var| random_c | 37 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 38 | // | random_c | 39 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | // 41 | // [RFC9562]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7 42 | func NewV4() (UUID, error) { 43 | var uuid UUID 44 | 45 | // fill entire uuid with secure, random bytes 46 | _, err := io.ReadFull(rand.Reader, uuid[:]) 47 | if err != nil { 48 | return UUID{}, err 49 | } 50 | 51 | // Set version and variant bits 52 | uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 [0100] 53 | uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is [10] 54 | return uuid, nil 55 | } 56 | 57 | // NewV7 returns a Version 7 UUID as defined in [RFC9562]. 58 | // Random bits are generated using [crypto/rand]. 59 | // 60 | // This function employs method 3 (Replace Leftmost Random Bits with Increased Clock Precision) 61 | // to increase the clock precision of the UUID. This helps support scenarios where 62 | // several UUIDs are generated within the same millisecond. 63 | // 64 | // 0 1 2 3 65 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 66 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 67 | // | unix_ts_ms | 68 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 69 | // | unix_ts_ms | ver | rand_a | 70 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 71 | // |var| rand_b | 72 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 73 | // | rand_b | 74 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 75 | // 76 | // [RFC9562]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7 77 | func NewV7() (UUID, error) { 78 | var uuid UUID 79 | 80 | t := time.Now() 81 | ms := t.UnixMilli() 82 | 83 | // Extract each byte from the 48-bit timestamp 84 | uuid[0] = byte(ms >> 40) // Most significant byte 85 | uuid[1] = byte(ms >> 32) 86 | uuid[2] = byte(ms >> 24) 87 | uuid[3] = byte(ms >> 16) 88 | uuid[4] = byte(ms >> 8) 89 | uuid[5] = byte(ms) // Least significant byte 90 | 91 | // Calculate sub-millisecond precision for rand_a (12 bits) 92 | ns := t.UnixNano() 93 | 94 | // Calculate sub-millisecond precision by: 95 | // 1. Taking nanoseconds modulo 1 million to get just the sub-millisecond portion 96 | // 2. Multiply by 4096 (2^12) to scale to 12 bits of precision 97 | // 3. Divide by 1 million to normalize back to a 12-bit fraction 98 | // This provides monotonically increasing values within the same millisecond 99 | subMs := ((ns % 1_000_000) * (1 << 12)) / 1_000_000 100 | 101 | // Fill the increased clock precision into "rand_a" bits 102 | uuid[6] = byte(subMs >> 8) 103 | uuid[7] = byte(subMs) 104 | 105 | // Fill the rest with random data 106 | _, err := io.ReadFull(rand.Reader, uuid[8:]) 107 | if err != nil { 108 | return UUID{}, err 109 | } 110 | 111 | // Set the version and variant bits 112 | uuid[6] = (uuid[6] & 0x0f) | 0x70 // Version 7 [0111] 113 | uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant [10] 114 | 115 | return uuid, nil 116 | } 117 | 118 | // String returns the UUID in "hex-and-dash" string format. 119 | // 120 | // 56c450b3-255d-4a2a-a761-cd1b1bf028e2 121 | func (uuid UUID) String() string { 122 | var buf [36]byte 123 | encodeHex(buf[:], uuid) 124 | return string(buf[:]) 125 | } 126 | 127 | // URN returns the UUID in "urn:uuid" string format. 128 | // 129 | // urn:uuid:56c450b3-255d-4a2a-a761-cd1b1bf028e2 130 | func (uuid UUID) URN() string { 131 | var buf [36]byte 132 | encodeHex(buf[:], uuid) 133 | return "urn:uuid:" + string(buf[:]) 134 | } 135 | 136 | func encodeHex(dst []byte, uuid UUID) { 137 | hex.Encode(dst, uuid[:4]) 138 | dst[8] = '-' 139 | hex.Encode(dst[9:13], uuid[4:6]) 140 | dst[13] = '-' 141 | hex.Encode(dst[14:18], uuid[6:8]) 142 | dst[18] = '-' 143 | hex.Encode(dst[19:23], uuid[8:10]) 144 | dst[23] = '-' 145 | hex.Encode(dst[24:], uuid[10:]) 146 | } 147 | 148 | // Parse a UUID string with or without the dashes. 149 | // 150 | // u, err := Parse("56c450b3255d4a2aa761cd1b1bf028e2") // no dashes 151 | // u, err := Parse("56c450b3-255d-4a2a-a761-cd1b1bf028e2") // with dashes 152 | // u, err := Parse("urn:uuid:56c450b3-255d-4a2a-a761-cd1b1bf028e2") // urn uuid prefix 153 | func Parse(s string) (UUID, error) { 154 | var x string 155 | switch len(s) { 156 | case 32: // uuid: "9178e496ba5c4c108b1513a1c70550d0", len: 32 157 | x = s[:8] + s[8:13] + s[13:18] + s[18:23] + s[23:] 158 | case 36: // uuid: "9178e496-ba5c-4c10-8b15-13a1c70550d0", len: 36 159 | if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { 160 | return UUID{}, errInvalidFormat 161 | } 162 | x = s[:8] + s[9:13] + s[14:18] + s[19:23] + s[24:] 163 | case 45: // uuid: "urn:uuid:9178e496-ba5c-4c10-8b15-13a1c70550d0" 164 | if !strings.HasPrefix(s, "urn:uuid:") || s[17] != '-' || s[22] != '-' || s[27] != '-' || s[32] != '-' { 165 | return UUID{}, errInvalidFormat 166 | } 167 | x = s[9:17] + s[18:22] + s[23:27] + s[28:32] + s[33:] 168 | default: 169 | return UUID{}, errInvalidLength 170 | } 171 | 172 | b, err := hex.DecodeString(x) 173 | if err != nil { 174 | return UUID{}, err 175 | } 176 | if len(b) != 16 { 177 | return UUID{}, errInvalidLength 178 | } 179 | 180 | switch { // assert version 181 | case (b[6] & 0xf0) == 0x40: // version 4 182 | case (b[6] & 0xf0) == 0x70: // version 7 183 | default: 184 | return UUID{}, fmt.Errorf("%v: %d", errUnsupportedVersion, b[6]&0xf0>>4) 185 | } 186 | 187 | if (b[8] & 0xc0) != 0x80 { // assert variant 188 | return UUID{}, errUnsupportedVariant 189 | } 190 | 191 | var uuid UUID 192 | copy(uuid[:], b) 193 | return uuid, nil 194 | } 195 | -------------------------------------------------------------------------------- /uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | stringUUIDRegex = regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") 10 | urnUUIDRegex = regexp.MustCompile("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") 11 | ) 12 | 13 | func TestParse(t *testing.T) { 14 | tests := map[string]struct { 15 | uuid string 16 | want string 17 | wantErr bool 18 | }{ 19 | "with dashes": {uuid: "53bfe550-4165-4f81-a8e7-c2609579ccc0", want: "53bfe550-4165-4f81-a8e7-c2609579ccc0"}, 20 | "no dashes": {uuid: "53bfe55041654f81a8e7c2609579ccc0", want: "53bfe550-4165-4f81-a8e7-c2609579ccc0"}, 21 | "urn:uuid prefix": {uuid: "urn:uuid:53bfe550-4165-4f81-a8e7-c2609579ccc0", want: "53bfe550-4165-4f81-a8e7-c2609579ccc0"}, 22 | "uppercase": {uuid: "53BFE550-4165-4F81-A8E7-C2609579CCC0", want: "53bfe550-4165-4f81-a8e7-c2609579ccc0"}, 23 | "mixed case": {uuid: "53bfe550-4165-4f81-A8E7-C2609579CCC0", want: "53bfe550-4165-4f81-a8e7-c2609579ccc0"}, 24 | "invalid urn prefix": {uuid: "abc:1234:53bfe550-4165-4f81-a8e7-c2609579ccc0", want: "00000000-0000-0000-0000-000000000000", wantErr: true}, 25 | } 26 | for name, tt := range tests { 27 | t.Run(name, func(t *testing.T) { 28 | uuid, err := Parse(tt.uuid) 29 | if (err != nil) != tt.wantErr { 30 | t.Errorf("Parse() wantErr = %t, gotErr = %v", tt.wantErr, err) 31 | return 32 | } 33 | if uuid.String() != tt.want { 34 | t.Errorf("want = %s, got = %s", tt.want, uuid.String()) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestUUID_String(t *testing.T) { 41 | tests := map[string]struct { 42 | new func() (UUID, error) 43 | }{ 44 | "nil": {new: func() (UUID, error) { 45 | return Nil, nil 46 | }}, 47 | "version 4": {new: NewV4}, 48 | "version 7": {new: NewV7}, 49 | } 50 | 51 | for name, tt := range tests { 52 | t.Run(name, func(t *testing.T) { 53 | u, _ := tt.new() 54 | if !stringUUIDRegex.MatchString(u.String()) { 55 | t.Errorf("UUID.String(): did not match string regex") 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestUUID_URN(t *testing.T) { 62 | tests := map[string]struct { 63 | new func() (UUID, error) 64 | }{ 65 | "nil": {new: func() (UUID, error) { 66 | return Nil, nil 67 | }}, 68 | "version 4": {new: NewV4}, 69 | "version 7": {new: NewV7}, 70 | } 71 | 72 | for name, tt := range tests { 73 | t.Run(name, func(t *testing.T) { 74 | u, _ := tt.new() 75 | if !urnUUIDRegex.MatchString(u.URN()) { 76 | t.Errorf("UUID.URN(): did not match string regex") 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestUUID_Duplicates(t *testing.T) { 83 | var iterations int = 1e6 // 1 million 84 | set := make(map[UUID]struct{}, iterations) 85 | tests := map[string]struct { 86 | new func() (UUID, error) 87 | }{ 88 | "version 4": {new: NewV4}, 89 | "version 7": {new: NewV7}, 90 | } 91 | for name, tt := range tests { 92 | t.Run(name, func(t *testing.T) { 93 | for i := 0; i < iterations; i++ { 94 | u, _ := tt.new() 95 | if _, ok := set[u]; ok { 96 | t.Errorf("iter %d: duplicate UUID detected!", i) 97 | } else { 98 | set[u] = struct{}{} 99 | } 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func TestPrint(t *testing.T) { 106 | u, _ := NewV4() 107 | t.Logf("v4: %s %v", u, u[:]) 108 | 109 | u, _ = NewV7() 110 | t.Logf("v7: %s %v", u, u[:]) 111 | } 112 | --------------------------------------------------------------------------------