Go 1.27 มี package uuid ใน Standard Library: เลือก V4 หรือ V7 ให้ตรงงาน
UUID เป็นของชิ้นเล็กที่อยู่เกือบทุกระบบ แต่ที่ผ่านมาแต่ละทีม Go มักต้องเลือก dependency และตกลง API กันเอง Go 1.27 เพิ่ม package uuid เข้า standard library ทำให้การสร้าง, parse, เปรียบเทียบ และแปลงข้อความมีฐานกลางที่มากับ toolchain
ข่าวสำคัญไม่ใช่แค่ “ลด dependency ได้หนึ่งตัว” ครับ แต่คือเราควรหยุดถามว่า UUID แบบไหนดังที่สุด แล้วถามว่า ID นี้เดินทางและถูกจัดเก็บอย่างไร เพราะ V4 กับ V7 มีคุณสมบัติคนละแบบ
Release notes ของ Go 1.27 ระบุว่า package ใหม่รองรับ UUID ตาม RFC 9562 และ เอกสาร uuid มี constructor สำหรับ V4 และ V7 โดยตรง
WHY: ID ที่ไม่ซ้ำยังไม่พอสำหรับฐานข้อมูล
UUID V4 ใช้ความสุ่มเป็นหลัก เหมือนแจกบัตรคิวโดยหยิบเลขจากหมวกใบใหญ่มาก โอกาสชนต่ำมากและเดาเลขถัดไปไม่ได้ แต่เมื่อใช้เป็น index ของฐานข้อมูล ค่าที่กระจายทั่วช่วงอาจทำให้การ insert กระโดดไปหลายหน้า
UUID V7 วาง timestamp ระดับมิลลิวินาทีไว้ด้านหน้า แล้วเติมส่วนสุ่มตามหลัง เหมือนบัตรพัสดุที่เรียงชั้นตามเวลารับของก่อน จึงรักษาความเป็น UUID พร้อม locality ที่เป็นมิตรกับ index มากขึ้น เอกสาร Go ระบุว่า NewV7 สร้างค่าแบบเพิ่มขึ้นตามลำดับสำหรับการเรียกใน process เดียวกัน ตราบใดที่นาฬิกาไม่ถอยหลัง
ดังนั้นกติกาคร่าว ๆ คือ:
- ใช้ V4 เมื่ออยากได้ identifier สุ่มและไม่ต้องการให้เวลาสร้างปรากฏจากโครงสร้าง ID
- พิจารณา V7 เมื่อ ID เป็น primary key ที่ถูก insert ต่อเนื่องและการเรียงตามเวลามีประโยชน์
- อย่าใช้ลำดับของ V7 แทน business timestamp เพราะ clock skew, การนำเข้าข้อมูลย้อนหลัง และหลาย process ยังเกิดขึ้นได้
HOW: สร้าง เปรียบเทียบ และ parse
ตัวอย่างต่อไปนี้มี test สองเรื่อง: V7 ที่สร้างติดกันควรเรียงตามลำดับ และ UUID V4 ต้องผ่านการแปลงเป็นข้อความแล้ว parse กลับมาได้ค่าเดิม
package uuiddemo
import (
"testing"
"uuid"
)
func TestV7IncreasesWithinOneProcess(t *testing.T) {
first := uuid.NewV7()
second := uuid.NewV7()
if first.Compare(second) >= 0 {
t.Fatalf("first UUID %s must sort before second UUID %s", first, second)
}
}
func TestV4TextRoundTrip(t *testing.T) {
original := uuid.NewV4()
parsed, err := uuid.Parse(original.String())
if err != nil {
t.Fatalf("parse generated UUID: %v", err)
}
if parsed != original {
t.Fatalf("round trip = %s, want %s", parsed, original)
}
}
ชนิด uuid.UUID เป็น array 16 ไบต์และเปรียบเทียบด้วย == ได้ จึงเหมาะเป็น key ของ map และลดการถือ ID เป็น string ระหว่าง logic ภายใน ส่วนตอนผ่าน HTTP/JSON หรือฐานข้อมูลค่อยแปลงตาม boundary ที่ชัดเจน
สิ่งที่ต้องระวังคือฟังก์ชัน uuid.New() ปัจจุบันสร้าง V4 แต่ชื่อสั้นไม่ได้บอกเจตนาของระบบ พี่โชว์ชอบเขียน NewV4() หรือ NewV7() ตรง ๆ ใน domain code เพราะ reviewer เห็น policy ได้ทันที และถ้าวันหนึ่งค่าเริ่มต้นหรือความต้องการเปลี่ยน จุดตัดสินใจก็หาเจอง่าย
ย้ายจาก third-party package อย่างไร
อย่าค้นหาและแทน import อย่างเดียว API อาจต่างกันทั้งรูปแบบ error, การยอมรับข้อความ, zero value และการ marshal ให้ทำ adapter เล็กที่ขอบ domain ก่อน เช่น NewOrderID และ ParseOrderID แล้วเพิ่ม test จาก input ที่ระบบเคยรับจริง
เช็ก schema ฐานข้อมูลด้วยว่าเก็บ UUID เป็น native type, 16-byte binary หรือข้อความ ถ้าเปลี่ยน representation พร้อมเปลี่ยน library จะทำให้หาสาเหตุยาก แยกสองเรื่องเป็นคนละ migration จะย้อนกลับและเปรียบเทียบได้ง่ายกว่า
RFC 9562 ยังเตือนเรื่องความเป็นส่วนตัวและความสามารถในการเดาข้อมูลจาก UUID แต่ละรุ่น V7 เปิดเผยช่วงเวลาสร้างโดยประมาณ จึงไม่ควรใช้เป็นความลับหรือ authorization token ไม่ว่าโอกาสชนจะต่ำแค่ไหน ID บอกว่า “ของชิ้นไหน” ไม่ได้บอกว่า “ใครมีสิทธิ์เปิด”
Recap
package uuid ใหม่ช่วยให้ Go มีฐานกลางสำหรับงานทั่วไป แต่การเลือกรุ่นยังเป็นงานออกแบบ V4 เหมาะกับความสุ่ม V7 เหมาะกับลำดับเวลาและ index locality ส่วนทั้งคู่ไม่ใช่ token ความลับ
เลือก constructor ให้สื่อเจตนา เก็บค่าแบบ 16 ไบต์ภายในเมื่อเหมาะสม และย้ายผ่าน adapter พร้อม test อย่าปล่อยให้การลด dependency กลายเป็นการเปลี่ยน contract แบบเงียบ ๆ ครับ