[Go] unique package
개요
Go 1.23부터 추가된 unique 패키지는 값 정규화(interning, hash-consing)를 통해 메모리를 최적화합니다.
주요 특징:
- 값 정규화: 동일한 값은 하나의 인스턴스만 유지
- 메모리 절약: 중복 값 제거로 메모리 사용량 감소
- 비교 성능: 포인터 비교로 O(1) 성능
- 제네릭 기반: comparable 타입 모두 지원
- 동시성 안전: goroutine-safe 보장
- 전역 고유성: 프로세스 전체에서 고유한 핸들
- GC 통합: 자동 메모리 관리
기본 개념
1. Handle의 이해
import "unique"
func main() {
// Handle은 값의 전역 고유 ID
h1 := unique.Make("hello")
h2 := unique.Make("hello")
h3 := unique.Make("world")
// 같은 값은 같은 Handle
fmt.Println(h1 == h2) // true
fmt.Println(h1 == h3) // false
// 원본 값 얻기
fmt.Println(h1.Value()) // hello
}
2. 메모리 정규화
func main() {
// 같은 문자열을 여러 번 사용
s1 := "very long string that repeats many times"
s2 := "very long string that repeats many times"
s3 := "very long string that repeats many times"
// 각각 별도 메모리 공간
// 총 3배 메모리 사용
// unique 사용
h1 := unique.Make(s1)
h2 := unique.Make(s2)
h3 := unique.Make(s3)
// h1, h2, h3는 같은 내부 값 참조
// 1배 메모리만 사용
}
3. 비교 성능
import "time"
func main() {
longStr := strings.Repeat("abc", 1000)
// 일반 문자열 비교
start := time.Now()
for i := 0; i < 1000000; i++ {
_ = longStr == longStr
}
fmt.Println("String compare:", time.Since(start))
// Handle 비교 (포인터 비교)
h := unique.Make(longStr)
start = time.Now()
for i := 0; i < 1000000; i++ {
_ = h == h
}
fmt.Println("Handle compare:", time.Since(start))
// Handle이 훨씬 빠름
}
4. 타입 안전성
func main() {
type UserID string
type SessionID string
// 타입별로 별도 Handle
uid := unique.Make(UserID("user123"))
sid := unique.Make(SessionID("user123"))
// 컴파일 에러: 타입이 다름
// _ = uid == sid
// Value()도 원래 타입 유지
var u UserID = uid.Value() // OK
var s SessionID = sid.Value() // OK
}
문자열 Interning
1. 기본 문자열 풀
type StringPool struct {
pool map[unique.Handle[string]]struct{}
mu sync.RWMutex
}
func NewStringPool() *StringPool {
return &StringPool{
pool: make(map[unique.Handle[string]]struct{}),
}
}
func (p *StringPool) Intern(s string) unique.Handle[string] {
h := unique.Make(s)
p.mu.Lock()
p.pool[h] = struct{}{}
p.mu.Unlock()
return h
}
func (p *StringPool) Size() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.pool)
}
func main() {
pool := NewStringPool()
// 같은 문자열 여러 번 추가
h1 := pool.Intern("hello")
h2 := pool.Intern("hello")
h3 := pool.Intern("world")
fmt.Println(h1 == h2) // true
fmt.Println(pool.Size()) // 2 (hello, world)
}
2. 로그 레벨 Interning
type LogLevel string
const (
DEBUG LogLevel = "DEBUG"
INFO LogLevel = "INFO"
WARN LogLevel = "WARN"
ERROR LogLevel = "ERROR"
)
type Logger struct {
level unique.Handle[LogLevel]
}
func NewLogger(level LogLevel) *Logger {
return &Logger{
level: unique.Make(level),
}
}
func (l *Logger) SetLevel(level LogLevel) {
l.level = unique.Make(level)
}
func (l *Logger) IsEnabled(level LogLevel) bool {
// 포인터 비교로 빠른 체크
return unique.Make(level) == l.level
}
func main() {
logger := NewLogger(INFO)
// 빠른 레벨 체크
if logger.IsEnabled(DEBUG) {
// ...
}
}
3. HTTP 헤더 정규화
type HeaderKey string
var (
ContentType = unique.Make(HeaderKey("Content-Type"))
Authorization = unique.Make(HeaderKey("Authorization"))
UserAgent = unique.Make(HeaderKey("User-Agent"))
)
type Headers struct {
values map[unique.Handle[HeaderKey]]string
}
func NewHeaders() *Headers {
return &Headers{
values: make(map[unique.Handle[HeaderKey]]string),
}
}
func (h *Headers) Set(key HeaderKey, value string) {
h.values[unique.Make(key)] = value
}
func (h *Headers) Get(key HeaderKey) string {
return h.values[unique.Make(key)]
}
func main() {
headers := NewHeaders()
// 빠른 키 비교
headers.Set("Content-Type", "application/json")
val := headers.Get("Content-Type")
fmt.Println(val) // application/json
}
구조체 정규화
1. 설정 값 중복 제거
type Config struct {
Host string
Port int
}
type Service struct {
name string
config unique.Handle[Config]
}
func NewService(name string, cfg Config) *Service {
return &Service{
name: name,
config: unique.Make(cfg),
}
}
func main() {
// 같은 설정을 여러 서비스가 공유
cfg := Config{Host: "localhost", Port: 8080}
s1 := NewService("service1", cfg)
s2 := NewService("service2", cfg)
s3 := NewService("service3", cfg)
// s1, s2, s3는 같은 Config 인스턴스 참조
fmt.Println(s1.config == s2.config) // true
}
2. 좌표 정규화
type Point struct {
X, Y int
}
type Shape struct {
vertices []unique.Handle[Point]
}
func NewShape(points []Point) *Shape {
vertices := make([]unique.Handle[Point], len(points))
for i, p := range points {
vertices[i] = unique.Make(p)
}
return &Shape{vertices: vertices}
}
func main() {
// 여러 도형이 같은 좌표 공유
p1 := Point{0, 0}
p2 := Point{1, 0}
p3 := Point{0, 1}
triangle := NewShape([]Point{p1, p2, p3})
square := NewShape([]Point{p1, p2, Point{1, 1}, p3})
// p1을 공유하므로 메모리 절약
}
3. AST 노드 공유
type NodeType string
const (
NumberNode NodeType = "NUMBER"
StringNode NodeType = "STRING"
BoolNode NodeType = "BOOL"
)
type ASTNode struct {
Type NodeType
Value string
}
type AST struct {
nodes []unique.Handle[ASTNode]
}
func (a *AST) AddNode(node ASTNode) {
a.nodes = append(a.nodes, unique.Make(node))
}
func main() {
ast := &AST{}
// 같은 노드 여러 번 사용
trueNode := ASTNode{Type: BoolNode, Value: "true"}
ast.AddNode(trueNode)
ast.AddNode(trueNode)
ast.AddNode(trueNode)
// 실제로는 하나의 인스턴스만 저장됨
}
실전 예제
1. 캐시 키 정규화
import (
"crypto/sha256"
"encoding/hex"
"sync"
)
type CacheKey string
type Cache struct {
data map[unique.Handle[CacheKey]]interface{}
mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
data: make(map[unique.Handle[CacheKey]]interface{}),
}
}
func (c *Cache) MakeKey(parts ...string) CacheKey {
h := sha256.New()
for _, p := range parts {
h.Write([]byte(p))
}
return CacheKey(hex.EncodeToString(h.Sum(nil)))
}
func (c *Cache) Set(key CacheKey, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
// 키 정규화로 메모리 절약
c.data[unique.Make(key)] = value
}
func (c *Cache) Get(key CacheKey) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[unique.Make(key)]
return v, ok
}
func main() {
cache := NewCache()
// 같은 키가 여러 번 사용되어도 메모리 효율적
key := cache.MakeKey("user", "123", "profile")
cache.Set(key, map[string]string{"name": "Alice"})
if val, ok := cache.Get(key); ok {
fmt.Println(val)
}
}
2. 에러 메시지 중복 제거
type ErrorCode string
const (
ErrNotFound ErrorCode = "NOT_FOUND"
ErrUnauthorized ErrorCode = "UNAUTHORIZED"
ErrInternalError ErrorCode = "INTERNAL_ERROR"
)
type AppError struct {
Code unique.Handle[ErrorCode]
Message string
Details map[string]interface{}
}
func NewError(code ErrorCode, message string) *AppError {
return &AppError{
Code: unique.Make(code),
Message: message,
Details: make(map[string]interface{}),
}
}
func (e *AppError) Is(code ErrorCode) bool {
// 포인터 비교로 빠른 체크
return e.Code == unique.Make(code)
}
func (e *AppError) Error() string {
return fmt.Sprintf("%s: %s", e.Code.Value(), e.Message)
}
func main() {
err := NewError(ErrNotFound, "User not found")
if err.Is(ErrNotFound) {
fmt.Println("Handle not found")
}
// 같은 에러 코드를 여러 에러가 공유
err2 := NewError(ErrNotFound, "Product not found")
// 메모리 절약 (같은 ErrorCode Handle)
fmt.Println(err.Code == err2.Code) // true
}
3. 국제화(i18n) 키 관리
type LocaleKey string
type I18n struct {
locale string
translations map[string]map[unique.Handle[LocaleKey]]string
mu sync.RWMutex
}
func NewI18n(locale string) *I18n {
return &I18n{
locale: locale,
translations: make(map[string]map[unique.Handle[LocaleKey]]string),
}
}
func (i *I18n) AddTranslation(locale string, key LocaleKey, text string) {
i.mu.Lock()
defer i.mu.Unlock()
if i.translations[locale] == nil {
i.translations[locale] = make(map[unique.Handle[LocaleKey]]string)
}
i.translations[locale][unique.Make(key)] = text
}
func (i *I18n) Translate(key LocaleKey) string {
i.mu.RLock()
defer i.mu.RUnlock()
if text, ok := i.translations[i.locale][unique.Make(key)]; ok {
return text
}
return string(key)
}
func main() {
i18n := NewI18n("ko")
// 번역 추가
i18n.AddTranslation("ko", "greeting.hello", "안녕하세요")
i18n.AddTranslation("en", "greeting.hello", "Hello")
i18n.AddTranslation("ko", "greeting.goodbye", "안녕히 가세요")
i18n.AddTranslation("en", "greeting.goodbye", "Goodbye")
// 빠른 조회 (키 정규화)
fmt.Println(i18n.Translate("greeting.hello"))
// 안녕하세요
}
4. 메트릭 레이블 최적화
type Label struct {
Name string
Value string
}
type Metric struct {
name string
labels []unique.Handle[Label]
value float64
}
type MetricRegistry struct {
metrics map[string][]*Metric
mu sync.RWMutex
}
func NewMetricRegistry() *MetricRegistry {
return &MetricRegistry{
metrics: make(map[string][]*Metric),
}
}
func (r *MetricRegistry) Record(name string, labels []Label, value float64) {
r.mu.Lock()
defer r.mu.Unlock()
// 레이블 정규화
uniqueLabels := make([]unique.Handle[Label], len(labels))
for i, l := range labels {
uniqueLabels[i] = unique.Make(l)
}
metric := &Metric{
name: name,
labels: uniqueLabels,
value: value,
}
r.metrics[name] = append(r.metrics[name], metric)
}
func (r *MetricRegistry) Query(name string, labels []Label) []float64 {
r.mu.RLock()
defer r.mu.RUnlock()
// 레이블 정규화
uniqueLabels := make([]unique.Handle[Label], len(labels))
for i, l := range labels {
uniqueLabels[i] = unique.Make(l)
}
var values []float64
for _, m := range r.metrics[name] {
if labelsEqual(m.labels, uniqueLabels) {
values = append(values, m.value)
}
}
return values
}
func labelsEqual(a, b []unique.Handle[Label]) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func main() {
registry := NewMetricRegistry()
// 같은 레이블이 반복적으로 사용됨
labels := []Label{
{Name: "service", Value: "api"},
{Name: "env", Value: "prod"},
}
registry.Record("requests", labels, 100)
registry.Record("requests", labels, 150)
registry.Record("requests", labels, 200)
// 메모리 효율적 (레이블 정규화)
values := registry.Query("requests", labels)
fmt.Println(values) // [100 150 200]
}
5. 설정 관리 시스템
type Environment string
const (
Development Environment = "development"
Staging Environment = "staging"
Production Environment = "production"
)
type DatabaseConfig struct {
Host string
Port int
Database string
}
type AppConfig struct {
Env unique.Handle[Environment]
Database unique.Handle[DatabaseConfig]
}
type ConfigManager struct {
configs map[unique.Handle[Environment]]AppConfig
mu sync.RWMutex
}
func NewConfigManager() *ConfigManager {
return &ConfigManager{
configs: make(map[unique.Handle[Environment]]AppConfig),
}
}
func (m *ConfigManager) Set(env Environment, dbConfig DatabaseConfig) {
m.mu.Lock()
defer m.mu.Unlock()
m.configs[unique.Make(env)] = AppConfig{
Env: unique.Make(env),
Database: unique.Make(dbConfig),
}
}
func (m *ConfigManager) Get(env Environment) (AppConfig, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
cfg, ok := m.configs[unique.Make(env)]
return cfg, ok
}
func main() {
manager := NewConfigManager()
// 설정 등록
manager.Set(Development, DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "dev_db",
})
manager.Set(Production, DatabaseConfig{
Host: "prod.example.com",
Port: 5432,
Database: "prod_db",
})
// 빠른 조회
if cfg, ok := manager.Get(Development); ok {
db := cfg.Database.Value()
fmt.Printf("%s:%d/%s\n", db.Host, db.Port, db.Database)
// localhost:5432/dev_db
}
}
6. 타입 안전한 상수 집합
type HTTPMethod string
const (
GET HTTPMethod = "GET"
POST HTTPMethod = "POST"
PUT HTTPMethod = "PUT"
DELETE HTTPMethod = "DELETE"
)
var (
methodGET = unique.Make(GET)
methodPOST = unique.Make(POST)
methodPUT = unique.Make(PUT)
methodDELETE = unique.Make(DELETE)
)
type Route struct {
Method unique.Handle[HTTPMethod]
Path string
Handler func()
}
type Router struct {
routes []Route
}
func (r *Router) Add(method HTTPMethod, path string, handler func()) {
r.routes = append(r.routes, Route{
Method: unique.Make(method),
Path: path,
Handler: handler,
})
}
func (r *Router) Match(method HTTPMethod, path string) (func(), bool) {
m := unique.Make(method)
for _, route := range r.routes {
// 포인터 비교로 빠른 체크
if route.Method == m && route.Path == path {
return route.Handler, true
}
}
return nil, false
}
func main() {
router := &Router{}
router.Add(GET, "/users", func() {
fmt.Println("GET /users")
})
router.Add(POST, "/users", func() {
fmt.Println("POST /users")
})
// 빠른 라우팅
if handler, ok := router.Match(GET, "/users"); ok {
handler() // GET /users
}
}
7. 이벤트 타입 시스템
type EventType string
const (
UserCreated EventType = "user.created"
UserUpdated EventType = "user.updated"
UserDeleted EventType = "user.deleted"
)
type Event struct {
Type unique.Handle[EventType]
Timestamp time.Time
Data interface{}
}
type EventBus struct {
handlers map[unique.Handle[EventType]][]func(Event)
mu sync.RWMutex
}
func NewEventBus() *EventBus {
return &EventBus{
handlers: make(map[unique.Handle[EventType]][]func(Event)),
}
}
func (b *EventBus) Subscribe(eventType EventType, handler func(Event)) {
b.mu.Lock()
defer b.mu.Unlock()
h := unique.Make(eventType)
b.handlers[h] = append(b.handlers[h], handler)
}
func (b *EventBus) Publish(eventType EventType, data interface{}) {
b.mu.RLock()
handlers := b.handlers[unique.Make(eventType)]
b.mu.RUnlock()
event := Event{
Type: unique.Make(eventType),
Timestamp: time.Now(),
Data: data,
}
for _, handler := range handlers {
go handler(event)
}
}
func main() {
bus := NewEventBus()
// 이벤트 구독
bus.Subscribe(UserCreated, func(e Event) {
fmt.Printf("User created: %v\n", e.Data)
})
bus.Subscribe(UserCreated, func(e Event) {
fmt.Println("Send welcome email")
})
// 이벤트 발행
bus.Publish(UserCreated, map[string]string{
"id": "123",
"name": "Alice",
})
time.Sleep(100 * time.Millisecond)
}
8. 문자열 집합 최적화
type StringSet struct {
items map[unique.Handle[string]]struct{}
mu sync.RWMutex
}
func NewStringSet() *StringSet {
return &StringSet{
items: make(map[unique.Handle[string]]struct{}),
}
}
func (s *StringSet) Add(item string) {
s.mu.Lock()
defer s.mu.Unlock()
s.items[unique.Make(item)] = struct{}{}
}
func (s *StringSet) Contains(item string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.items[unique.Make(item)]
return ok
}
func (s *StringSet) Remove(item string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.items, unique.Make(item))
}
func (s *StringSet) Size() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
func (s *StringSet) ToSlice() []string {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]string, 0, len(s.items))
for h := range s.items {
result = append(result, h.Value())
}
return result
}
func main() {
set := NewStringSet()
// 같은 문자열 여러 번 추가
set.Add("apple")
set.Add("banana")
set.Add("apple") // 중복
set.Add("cherry")
fmt.Println(set.Size()) // 3
fmt.Println(set.Contains("apple")) // true
set.Remove("apple")
fmt.Println(set.Size()) // 2
fmt.Println(set.ToSlice()) // [banana cherry]
}
일반적인 실수
1. Handle 저장 누락
// ❌ 나쁜 예 (Handle 활용 안 함)
type Config struct {
env string // 그냥 문자열 저장
}
func main() {
c1 := Config{env: "production"}
c2 := Config{env: "production"}
// 비교할 수 없음
// _ = c1 == c2 // 컴파일 에러
}
// ✅ 좋은 예 (Handle 사용)
type Config struct {
env unique.Handle[string]
}
func NewConfig(env string) Config {
return Config{
env: unique.Make(env),
}
}
func main() {
c1 := NewConfig("production")
c2 := NewConfig("production")
// 빠른 비교
fmt.Println(c1.env == c2.env) // true
}
2. Value() 남용
// ❌ 나쁜 예 (매번 Value() 호출)
func main() {
h := unique.Make("hello")
for i := 0; i < 1000; i++ {
_ = h.Value() == "hello" // Value() 호출 비용
}
}
// ✅ 좋은 예 (Handle 비교)
func main() {
h1 := unique.Make("hello")
h2 := unique.Make("hello")
for i := 0; i < 1000; i++ {
_ = h1 == h2 // 포인터 비교 (빠름)
}
}
3. comparable 제약 무시
// ❌ 나쁜 예 (comparable 아닌 타입)
type Data struct {
items []int // 슬라이스는 comparable 아님
}
func main() {
// 컴파일 에러
// h := unique.Make(Data{items: []int{1, 2, 3}})
}
// ✅ 좋은 예 (comparable 타입 사용)
type Data struct {
id int
name string
}
func main() {
h := unique.Make(Data{id: 1, name: "test"})
fmt.Println(h.Value()) // {1 test}
}
4. 포인터 타입 혼동
// ❌ 나쁜 예 (포인터는 주소 비교)
type Person struct {
Name string
}
func main() {
p1 := &Person{Name: "Alice"}
p2 := &Person{Name: "Alice"}
h1 := unique.Make(p1)
h2 := unique.Make(p2)
// 다른 Handle (주소가 다름)
fmt.Println(h1 == h2) // false
}
// ✅ 좋은 예 (값 타입 사용)
type Person struct {
Name string
}
func main() {
p1 := Person{Name: "Alice"}
p2 := Person{Name: "Alice"}
h1 := unique.Make(p1)
h2 := unique.Make(p2)
// 같은 Handle (값이 같음)
fmt.Println(h1 == h2) // true
}
5. nil Handle 처리
// ❌ 나쁜 예 (nil 체크 누락)
func GetConfig(env string) unique.Handle[string] {
if env == "" {
// zero value Handle 반환
return unique.Handle[string]{}
}
return unique.Make(env)
}
func main() {
h := GetConfig("")
// h.Value() 호출 시 패닉 가능
}
// ✅ 좋은 예 (명시적 처리)
func GetConfig(env string) (unique.Handle[string], bool) {
if env == "" {
return unique.Handle[string]{}, false
}
return unique.Make(env), true
}
func main() {
if h, ok := GetConfig("prod"); ok {
fmt.Println(h.Value())
}
}
6. 맵 키로 값 사용
// ❌ 나쁜 예 (값을 맵 키로 사용)
func main() {
m := make(map[string]int)
// 같은 문자열이 여러 번 키로 사용됨
m["configuration"] = 1
m["configuration"] = 2
// 메모리 중복
}
// ✅ 좋은 예 (Handle을 맵 키로 사용)
func main() {
m := make(map[unique.Handle[string]]int)
h := unique.Make("configuration")
m[h] = 1
m[h] = 2 // 같은 Handle 재사용
fmt.Println(m[h]) // 2
}
7. 동시성 안전 오해
// ❌ 나쁜 예 (자체 데이터 구조 동기화 누락)
type Cache struct {
data map[unique.Handle[string]]interface{}
// mutex 없음
}
func (c *Cache) Set(key string, val interface{}) {
// race condition!
c.data[unique.Make(key)] = val
}
// ✅ 좋은 예 (적절한 동기화)
type Cache struct {
data map[unique.Handle[string]]interface{}
mu sync.RWMutex
}
func (c *Cache) Set(key string, val interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[unique.Make(key)] = val
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[unique.Make(key)]
return v, ok
}
베스트 프랙티스
1. 상수는 전역 Handle로
// ✅ 초기화 시점에 Handle 생성
var (
EnvDev = unique.Make("development")
EnvProd = unique.Make("production")
)
type App struct {
env unique.Handle[string]
}
func NewApp(isDev bool) *App {
if isDev {
return &App{env: EnvDev}
}
return &App{env: EnvProd}
}
2. 타입별로 별도 Handle
// ✅ 타입 안전성 보장
type UserID string
type SessionID string
type User struct {
id unique.Handle[UserID]
}
type Session struct {
id unique.Handle[SessionID]
}
// 컴파일 타임에 타입 체크
func GetUser(id unique.Handle[UserID]) User {
// ...
return User{id: id}
}
3. 비교 연산 최적화
// ✅ Handle 비교로 성능 향상
type Status string
const (
Active Status = "active"
Inactive Status = "inactive"
)
var (
statusActive = unique.Make(Active)
statusInactive = unique.Make(Inactive)
)
func IsActive(status unique.Handle[Status]) bool {
return status == statusActive
}
4. 메모리 프로파일링
import (
"runtime"
"runtime/pprof"
)
// ✅ 메모리 사용량 측정
func BenchmarkStringInterning(b *testing.B) {
var m runtime.MemStats
// Before
runtime.ReadMemStats(&m)
before := m.Alloc
handles := make([]unique.Handle[string], b.N)
for i := 0; i < b.N; i++ {
handles[i] = unique.Make("repeated string")
}
// After
runtime.ReadMemStats(&m)
after := m.Alloc
b.Logf("Memory used: %d bytes", after-before)
}
5. 생성자 패턴
// ✅ Handle 생성을 캡슐화
type Config struct {
env unique.Handle[string]
mode unique.Handle[string]
}
func NewConfig(env, mode string) Config {
return Config{
env: unique.Make(env),
mode: unique.Make(mode),
}
}
func (c Config) Env() string {
return c.env.Value()
}
func (c Config) IsSame(other Config) bool {
return c.env == other.env && c.mode == other.mode
}
6. 인터페이스 활용
// ✅ 제네릭 인터페이스
type Identifiable[T comparable] interface {
ID() unique.Handle[T]
}
type User struct {
id unique.Handle[string]
}
func (u User) ID() unique.Handle[string] {
return u.id
}
func FindByID[T comparable](
items []Identifiable[T],
id unique.Handle[T],
) Identifiable[T] {
for _, item := range items {
if item.ID() == id {
return item
}
}
return nil
}
7. 문서화
// ✅ 명확한 문서화
// CacheKey represents a unique identifier for cache entries.
// All cache keys are interned to reduce memory usage when the same
// key is used multiple times.
type CacheKey string
// MakeCacheKey creates a unique handle for the given cache key.
// The returned handle can be compared with == for fast equality checks.
func MakeCacheKey(namespace, id string) unique.Handle[CacheKey] {
key := CacheKey(namespace + ":" + id)
return unique.Make(key)
}
8. 테스트 작성
// ✅ Handle 동작 테스트
func TestHandleEquality(t *testing.T) {
tests := []struct {
name string
v1 string
v2 string
want bool
}{
{"same value", "hello", "hello", true},
{"different value", "hello", "world", false},
{"empty strings", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h1 := unique.Make(tt.v1)
h2 := unique.Make(tt.v2)
got := h1 == h2
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
정리
- 개념: 값 정규화로 메모리 최적화, 동일 값은 하나의 인스턴스만 유지
- 성능: 포인터 비교로 O(1) 성능, 문자열 비교보다 훨씬 빠름
- 타입: 제네릭 기반, comparable 타입 지원, 타입 안전성 보장
- 사용: 문자열 풀, 설정 값, AST 노드, 에러 코드, 로그 레벨 등
- 실전: 캐시 키, 에러 메시지, i18n, 메트릭 레이블, 설정 관리, 상수 집합, 이벤트 시스템, 문자열 집합
- 실수: Handle 저장 누락, Value() 남용, comparable 무시, 포인터 혼동, nil 처리, 맵 키 값 사용, 동기화 누락
- 베스트: 전역 Handle, 타입별 Handle, 비교 최적화, 프로파일링, 생성자 패턴, 인터페이스, 문서화, 테스트