go.mod から github.com/google/uuid の行が消せます。Go 1.27 で標準ライブラリに uuid パッケージが入り、v4 と v7 の生成もパースも標準だけで完結する。ただし API は google/uuid のサブセットなので、import 行を書き換えるだけではビルドが通りません。
計測環境は go1.27rc2(darwin/arm64、Apple M5 Pro)、比較対象は google/uuid v1.6.0。
標準 uuid が公開しているのは7つの関数と5つのメソッド
go doc uuid の出力がパッケージのほぼ全体です。
$ go1.27rc2 doc uuid
package uuid // import "uuid"
type UUID [16]byte
func Max() UUID
func MustParse(s string) UUID
func New() UUID
func NewV4() UUID
func NewV7() UUID
func Nil() UUID
func Parse(s string) (UUID, error)
これに String / Compare / MarshalText / AppendText / UnmarshalText の5メソッドが付きます。型は type UUID [16]byte の配列。== で直接比較でき、map のキーにもそのまま置けます。
New は NewV4 の別名
パッケージドキュメントには “The New function returns a new UUID generated using an algorithm suitable for most purposes” とあり、実装は NewV4() を呼ぶだけ。将来 New の中身が v7 に差し替わる余地を残した書き方です。バージョンを固定したいなら NewV4() か NewV7() を明示的に呼びます。
Nil と Max は変数ではなく関数
google/uuid の uuid.Nil はパッケージ変数でしたが、標準では uuid.Nil() という関数。書き換え漏れがそのままコンパイルエラーになるので、移行の項で扱います。Max は RFC 9562 Section 5.10 で定義された ffffffff-ffff-ffff-ffff-ffffffffffff を返します。
Parse が受け付ける4つの表記
ハイフンあり、波括弧つき、URN 形式、ハイフンなしの4つ。16進の英字は大文字小文字どちらでも通ります。
for _, in := range []string{
"f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"{f81d4fae-7dec-11d0-a765-00a0c91e6bf6}",
"urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"F81D4FAE7DEC11D0A76500A0C91E6BF6",
"f81d4fae-7dec-11d0-a765-00a0c91e6bf", // 1文字足りない
} {
u, err := uuid.Parse(in)
fmt.Printf("%v / %v\n", u, err)
}
f81d4fae-7dec-11d0-a765-00a0c91e6bf6 / <nil> (上4件はすべて同じ値)
...
00000000-0000-0000-0000-000000000000 / invalid uuid
エラーは errInvalid = errors.New("invalid uuid") の1種類だけ。どの文字が不正だったかは返らないので、入力値のログ出しは呼び出し側の仕事です。
NewV7 は google/uuid の2.9倍速く、アロケーションが0
生成とパースを b.Loop で回した結果。-benchtime 3s で計測しています。
| 処理 | 標準 uuid (1.27rc2) | google/uuid v1.6.0 | 差 |
|---|---|---|---|
| v4 生成 | 164.6 ns/op 0 B/op, 0 allocs | 172.7 ns/op 16 B/op, 1 alloc | -4.7% |
| v7 生成 | 71.12 ns/op 0 B/op, 0 allocs | 207.2 ns/op 16 B/op, 1 alloc | -65.7% |
| Parse | 14.84 ns/op 0 allocs | 11.06 ns/op 0 allocs | +34.2% |
v7 が v4 より速いのは乱数を半分しか使わないから
標準実装のソースを並べると理由が一行で分かります。
// src/uuid/uuid.go
func NewV4() UUID {
var u UUID
rand.Read(u[:]) // 16バイト全部を乱数で埋める
u.setVersion(4)
u.setVariant(0b10)
return u
}
func NewV7() UUID {
// ...タイムスタンプ計算は省略
var u UUID
binary.BigEndian.PutUint64(u[0:8], hibits)
rand.Read(u[8:]) // 後半8バイトだけ乱数
u.setVersion(7)
u.setVariant(0b10)
return u
}
v4 は crypto/rand から16バイト、v7 は8バイト。164.6 ns と 71.12 ns の差はほぼこの読み取り量で説明がつきます。v7 は sync.Mutex のロックを取るのに、それでも v4 の半分以下に収まっている。
Parse だけは google/uuid が速い
標準の Parse は UnmarshalText に委譲する実装で、4つの表記を判定する分岐が入ります。ハイフンあり形式に最適化された google/uuid に 3.78 ns 負ける。どちらも0アロケーションなので、1リクエストあたり数回のパースなら誤差です。
10万件を連続生成して順序が逆転した回数
v7 の売りは生成順にソートされること。10万件を生成して直前の値と Compare し、逆転を数えました。
v7 生成 100000 件 / 順序が逆転した件数: 0
v4 生成 100000 件 / 順序が逆転した件数: 49955
v7 は生成順のままソート済みか: true
v4 は約半分が逆転。乱数なので当然の結果です。
単調性の刻みは1/4096ミリ秒
標準の NewV7 は RFC 9562 が任意で許している12ビットのサブミリ秒フラクションを rand_a 領域に入れています。1ミリ秒を4096分割した粒度。それを超える速度で採番したときは、直前の値に +1 して単調性を維持します。
} else if timestamp <= v7lastTimestamp {
// This timestamp is the same as a previously-generated UUID.
// To preserve the property that we generate UUIDs in order,
// use a timestamp 1/4096 millisecond later than the most recently
// generated UUID.
timestamp = v7lastTimestamp + 1
}
この補正が入ると、埋め込みタイムスタンプが実時間より先に進みます。5万件をループで生成して測った結果。
50000 件の生成にかかった実時間: 10.326ms
先頭UUIDの埋め込み時刻 : 20:10:02.210
末尾UUIDの埋め込み時刻 : 20:10:02.223
埋め込み時刻の進み幅 : 13ms
実時間との差(先送り量) : 2.674ms
13ミリ秒ぶんの時刻が刻まれ、実時間より 2.67ミリ秒 未来にずれました。v7 のタイムスタンプを監査ログの時刻として扱うなら、この誤差が乗ります。
database/sql は uuid.UUID を内部でハードコードして扱う
google/uuid は driver.Valuer と sql.Scanner を実装していました。標準の UUID 型はどうか。インターフェース充足をコンパイル時にチェックします。
var (
_ driver.Valuer = uuid.UUID{}
_ encoding.BinaryMarshaler = uuid.UUID{}
)
./nilcheck.go:15:31: cannot use uuid.UUID{} (value of array type uuid.UUID)
as driver.Valuer value: uuid.UUID does not implement driver.Valuer (missing method Value)
./nilcheck.go:16:31: cannot use uuid.UUID{} (value of array type uuid.UUID)
as encoding.BinaryMarshaler value: uuid.UUID does not implement encoding.BinaryMarshaler
(missing method MarshalBinary)
どちらも未実装。ここでラッパー型に Value() と Scan() を生やしたくなります。自分は先にラッパーを書いてしまい、後から不要と分かって消しました。
Valuer 未実装でも INSERT が通る
SQLite(modernc.org/sqlite v1.55.0)にそのまま渡した結果。
db.Exec(`CREATE TABLE orders (id TEXT PRIMARY KEY, amount INTEGER)`)
want := uuid.NewV7()
_, err := db.Exec(`INSERT INTO orders VALUES (?, ?)`, want, 1200)
var got uuid.UUID
err = db.QueryRow(`SELECT id FROM orders`).Scan(&got)
INSERT (uuid.UUID 直接): err=<nil>
SELECT (*uuid.UUID 直接): err=<nil>
got=019fc74e-aef2-7083-ab63-8dcbb4e07348 / 一致=true
往復とも通ります。database/sql/driver の defaultConverter.ConvertValue に uuid.UUID の分岐があるためです。
// src/database/sql/driver/types.go
func (defaultConverter) ConvertValue(v any) (Value, error) {
if IsValue(v) {
return v, nil
}
switch vr := v.(type) {
case Valuer:
// ...
case decimalDecompose:
return vr, nil
case uuid.UUID:
return vr.String(), nil
}
// ...
case uuid.UUID という型スイッチが1本入っている。標準ライブラリが自前の型を名指しで分岐しています。素の [16]byte はこの分岐に落ちません。
DefaultParameterConverter: value="019fc74e-540c-7808-99dc-8847cfcd0249" type=string err=<nil>
素の [16]byte : value=<nil> type=<nil> err=unsupported type main.plain, a array
Scan 側は TEXT と16バイトの両方を受ける
読み取りは database/sql/convert.go 側に *uuid.UUID の分岐があります。driver から返る値が string なら uuid.Parse、[]byte なら長さで判定する。
// src/database/sql/convert.go
case *uuid.UUID:
if len(s) == len(*d) {
copy((*d)[:], s) // 16バイトならそのままコピー
return nil
}
var u uuid.UUID
err := u.UnmarshalText(s) // それ以外はテキストとして解釈
PostgreSQL の uuid 型や MySQL の BINARY(16) のようにバイナリで返るカラムと、VARCHAR(36) のようにテキストで返るカラム。どちらも追加のコードなしで *uuid.UUID に入ります。SQLite の BLOB カラムに3パターン流し込んで確かめました。
- 16バイトのバイナリ: 成功。
copyでそのまま入る - 36バイトのテキスト: 成功。
UnmarshalText経由でパースされる - 不正な長さ(
"broken"の6バイト): エラー
sql: Scan error on column index 0, name "id":
converting driver.Value type []byte ("broken") to a UUID: invalid uuid
16バイト経路の判定は len(s) == len(*d) という長さ比較だけで、中身は検証されません。ちょうど16バイトの非UUID文字列を BLOB カラムに入れて *uuid.UUID で受けると、こうなります。
入れた値の長さ: 16
Scan err: <nil>
入った UUID: 61626364-6566-6768-696a-6b6c6d6e6f70
バージョン部: 6
"abcdefghijklmnop" がそのまま16バイトのUUIDとして入り、バージョン部が6という RFC 9562 に存在しない値になった。エラーは返りません。UUID 以外の値が混ざりうるカラムなら、いったん string で受けて uuid.Parse のエラーを見ます。
この挙動はリリースノートに書かれていない
Go 1.27 Release Notes の “New uuid package” セクションは “The new uuid package generates and parses UUIDs.” の一文だけ。“Minor changes to the library” の database/sql 項目にあるのは ConvertAssign と RowsColumnScanner の追加で、uuid への言及はありません。pgx のようにネイティブインターフェースを持つドライバはこの変換経路を通らないため、Rows.Scan と ConvertValue を経由する構成に限った話になります。
google/uuid から移すとビルドが止まる6箇所
import パスの書き換えだけでは済みません。google/uuid でよく使う API を並べてビルドすると、この6つで止まります。
_ = uuid.NewString()
_, _ = uuid.NewRandom()
_ = uuid.New().Version()
_ = uuid.New().Variant()
_, _ = uuid.New().MarshalBinary()
_ = uuid.NewSHA1(uuid.Nil, []byte("x"))
./missing.go:6:11: undefined: uuid.NewString
./missing.go:8:14: undefined: uuid.NewRandom
./missing.go:9:17: uuid.New().Version undefined (type uuid.UUID has no field or method Version)
./missing.go:10:17: uuid.New().Variant undefined (type uuid.UUID has no field or method Variant)
./missing.go:11:20: uuid.New().MarshalBinary undefined (type uuid.UUID has no field or method MarshalBinary)
./missing.go:12:11: undefined: uuid.NewSHA1
Nil の比較は型エラーになる
google/uuid では uuid.Nil がパッケージ変数だったため、if u == uuid.Nil がイディオムでした。標準では関数なので、書き換えないと比較そのものが成立しません。
invalid operation: u == uuid.Nil (mismatched types uuid.UUID and func() uuid.UUID)
正しくは u == uuid.Nil()、あるいは u == (uuid.UUID{})。配列型なのでゼロ値との比較で足ります。
v1・v3・v5 は入っていない
標準にあるのは v4 と v7 だけ。名前空間ベースの v5(NewSHA1)や MAC アドレスを使う v1 が要るなら、google/uuid を残す判断になります。Version() と Variant() のアクセサも意図的に省かれました。バージョン判定が要るなら7バイト目の上位4ビットを自分で読みます。
version := u[6] >> 4 // 4 または 7
型変換は無条件に通る
両者とも実体は [16]byte。段階的に移行するなら型変換で橋渡しできます。
g := googleuuid.New()
s := uuid.UUID(g) // 変換だけで済む
fmt.Println(g.String() == s.String())
true
v7 は生成時刻を外部に見せる
先頭48ビットは Unix ミリ秒そのもの。UUID を受け取った側がレコードの作成時刻を復元できます。
func v7TimeOf(u uuid.UUID) time.Time {
var b [8]byte
copy(b[2:], u[:6])
return time.UnixMilli(int64(binary.BigEndian.Uint64(b[:])))
}
019fc74d-fe17-74c4-9a77-256589b4e342 から復元した生成時刻: 2026-08-03T20:06:45.143+09:00
注文番号を v7 で採番して URL に載せると、その注文が入った時刻がミリ秒精度で第三者に読めます。
| NewV4 | NewV7 | |
|---|---|---|
| ランダム性 | 122ビット | 62ビット以上 |
| 生成時刻 | 復元不可 | ミリ秒精度で復元可 |
| ソート順 | ランダム(10万件中49,955件が逆転) | 生成順(10万件で逆転0) |
| 生成コスト | 164.6 ns | 71.12 ns |
| 向く用途 | セッションID、公開URLのID | 主キー、イベントID |
まとめ
- 標準 uuid の API は7関数と5メソッド。v4 と v7 のみで、v1・v3・v5 と
Version()/Variant()は無い - NewV7 は 71.12 ns/op・0アロケーション。google/uuid v1.6.0 の
207.2 ns/opから65.7%削減。乱数の読み取りが8バイトで済むため v4 よりも速い - Parse は google/uuid のほうが 3.78 ns 速い。標準は4表記を判定する分岐を持つ
driver.Valuerもsql.Scannerも未実装。ただしdatabase/sql/driverとdatabase/sqlの両方にuuid.UUIDの型スイッチがあるため、ラッパー型なしで往復できる。バイナリ16バイトのカラムも対応済み- 16バイト経路は長さしか見ないので、UUID でない16バイト列がエラーなしで通る。混在しうるカラムは
stringで受けてuuid.Parseにかける - 移行でビルドが止まるのは
NewString/NewRandom/NewSHA1/Version/Variant/MarshalBinaryの6つと、変数から関数に変わったNil - v7 のタイムスタンプは高速採番時に実時間より先へずれる。5万件を10.3ミリ秒で生成したとき2.67ミリ秒の先送りが発生した
Go 1.27 は2026年8月にリリース予定で、現時点の最新安定版は Go 1.26.5。この記事の検証は go1.27rc2 で行いました。go install golang.org/dl/go1.27rc2@latest と go1.27rc2 download で既存の Go と並列に入ります。

