Використання структур як ключів для мапи в Golang
Вже пару років працюю з Go і хочу розповісти про помилку яку робив сам а також зустрічав у колег, які також перейшли з PHP чи Python на Golang, а саме серіалізація ключів.
В цій темі наведу приклади помилок, виправлення і мікробенчмарки.
Приклад помилки з серіалізацією складного ключа
Є stateful мікросервіс який перед обробкою запита від користувача перевіряє його на правильність, а дані для перевірки оновлює по крону.Приблизно код виглядає так:
package example import ( "strconv" "sync" ) type Request interface { Namespace() uint64 Set() uint64 } type FullPathValue struct { Namespace uint64 Set uint64 } var ( namespaceSetExistsMap map[string]bool namespaceSetMutex sync.RWMutex ) // handle user request func SomeHandler(request Request) { if namespaceSetExists(request.Namespace(), request.Set()) { return } // some action } // update by cron func updateNamespaceSet(values []FullPathValue) { var replace = make(map[string]bool, len(values)) for _, value := range values { replace[namespaceSetString(value.Namespace, value.Set)] = true } namespaceSetMutex.Lock() namespaceSetExistsMap = replace namespaceSetMutex.Unlock() } func namespaceSetExists(namespace, set uint64) bool { namespaceSetMutex.RLock() var data = namespaceSetExistsMap namespaceSetMutex.RUnlock() return data[namespaceSetString(namespace, set)] } func namespaceSetString(namespace, set uint64) string { const ( namespaceSetSize = 21*2 + 1 // 21 is uint64 size, 1 delimiters '_' ) result := make([]byte, 0, namespaceSetSize) result = strconv.AppendUint(result, uint64(namespace), 10) result = append(result, '_') result = strconv.AppendUint(result, uint64(set), 10) return string(result) }схожу на namespaceSetString функцію зустрічав в реальному проекті
одна з причин таких серіалізацій це обмеження PHP де ключами можуть бути строкові та числові значення, таким чином роками напрацьований патерн серіалізації мігрує в іншу мову (думаю така ж ситуація з ключами в Python, Ruby та JavaScript)
а ось в Golang ключами можуть бути структури і відповідно використовувати без серіалізації
package example import ( "sync" ) type FullPathValue struct { Namespace uint64 Set uint64 } var ( namespaceSetExistsMap map[FullPathValue]bool namespaceSetMutex sync.RWMutex ) // update by cron func updateNamespaceSet(values []FullPathValue) { var replace = make(map[FullPathValue]bool, len(values)) for _, value := range values { replace[value] = true } namespaceSetMutex.Lock() namespaceSetExistsMap = replace namespaceSetMutex.Unlock() } func namespaceSetExists(namespace, set uint64) bool { namespaceSetMutex.RLock() var data = namespaceSetExistsMap namespaceSetMutex.RUnlock() return data[FullPathValue{namespace, set}] }
Перевірка швидкодії серіалізованого ключа та ключа-структури
Ще до тестів зрозуміло переможця, а тест приведений тільки для відображення числових характеристик:package main import ( "strconv" "testing" ) const ( namespaceSetKeyBinSize = 10*4 + 3 // 10 is uint32 size, 3 delimiters '_' ) func BenchmarkNamespaceSetKeyBinStringWrite(b *testing.B) { var data = make(map[string]bool, b.N) for i := 0; i < b.N; i++ { data[namespaceSetKeyBinString(uint32(i), uint32(i), uint32(i), uint32(i))] = true } } func BenchmarkNamespaceSetKeyBinStringRead(b *testing.B) { var data = make(map[string]bool, b.N) for i := 0; i < b.N; i++ { data[namespaceSetKeyBinString(uint32(i), uint32(i), uint32(i), uint32(i))] = true } b.ResetTimer() for i := 0; i < b.N; i++ { _ = data[namespaceSetKeyBinString(uint32(i), uint32(i), uint32(i), uint32(i))] } } func BenchmarkNamespaceSetKeyBinStructWrite(b *testing.B) { type namespaceSetKeyBin struct { namespace, set, key, bin uint32 } var data = make(map[namespaceSetKeyBin]bool, b.N) for i := 0; i < b.N; i++ { data[namespaceSetKeyBin{uint32(i), uint32(i), uint32(i), uint32(i)}] = true } } func BenchmarkNamespaceSetKeyBinStructRead(b *testing.B) { type namespaceSetKeyBin struct { namespace, set, key, bin uint32 } var data = make(map[namespaceSetKeyBin]bool, b.N) for i := 0; i < b.N; i++ { data[namespaceSetKeyBin{uint32(i), uint32(i), uint32(i), uint32(i)}] = true } b.ResetTimer() for i := 0; i < b.N; i++ { _ = data[namespaceSetKeyBin{uint32(i), uint32(i), uint32(i), uint32(i)}] } } func namespaceSetKeyBinString(namespace, set, key, bin uint32) string { result := make([]byte, 0, namespaceSetKeyBinSize) result = strconv.AppendUint(result, uint64(namespace), 10) result = append(result, '_') result = strconv.AppendUint(result, uint64(set), 10) result = append(result, '_') result = strconv.AppendUint(result, uint64(key), 10) result = append(result, '_') result = strconv.AppendUint(result, uint64(bin), 10) return string(result) }
go test ./... -v -bench=. -benchmem
StringWrite 213 ns/op 61 B/op 1 allocs/op StringRead 209 ns/op 31 B/op 1 allocs/op StructWrite 110 ns/op 30 B/op 0 allocs/op StructRead 99.7 ns/op 0 B/op 0 allocs/opяк бачимо використовувати структуру в два рази ефективніше в даному прикладі
Приклад помилки на вкладених мапах
func BenchmarkNestedIntMapKey(b *testing.B) { var data = make(map[uint32]map[uint32]int, 1024) for i := 0; i < b.N; i++ { var nestedKey = uint32(i & 1023) if nestedData, ok := data[nestedKey]; ok { nestedData[uint32(i)] = i } else { data[nestedKey] = map[uint32]int{ uint32(i): i, } } } }схожі помилки зустрічав у PHP розробників в реальному проекті, звісно і сам робив, і їх також треба виправляти використанням ключа-структури
Приклад з вкладеними структурами
В попередніх прикладах ключі-структури були дуже простими, то ось приклад з вкладеними структурами:package example import ( "sync" ) type AppCountryKey struct { App uint32 Country string } type AppCountrySystemKey struct { AppCountryKey System string } type Value struct { RequestTotal uint32 RequestOk uint32 RequestEmpty uint32 } func (sum Value) Merge(value Value) Value { return Value{ RequestTotal: sum.RequestTotal + value.RequestTotal, RequestOk: sum.RequestOk + value.RequestOk, RequestEmpty: sum.RequestEmpty + value.RequestEmpty, } } var ( aggregationMap = make(map[AppCountrySystemKey]Value) aggregationMutex sync.Mutex ) func AddRequest(appCountry AppCountryKey, system string, value Value) { var key = AppCountrySystemKey{ AppCountryKey: appCountry, System: system, } aggregationMutex.Lock() if sum, ok := aggregationMap[key]; ok { aggregationMap[key] = sum.Merge(value) } else { aggregationMap[key] = value } aggregationMutex.Unlock() }
Епілог
Стаття разом з кодом писалась за один вечір, бо вже скільки разів зустрічав і робив цю помилку в різних проектах, можливо після статті перестануТакож хочу подякувати Олексію, який допомагав з іншою статтею, а також шукає до себе в команду розробників зі знанням Golang та Rust
13 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів