Проблема
Во время написания небольшой государственной машины в Go меня беспокоило то, что я не мог найти чистый способ реализации операций с флагами для перечислений без повторения кода. Возможно, это связано с недостатком опыта, ведь я начал использовать Go всего несколько месяцев назад, но я все-таки нашел способ, который могу считать «чистым», используя функцию generics, доступную с версии Go 1.18. В этом посте я с удовольствием поделюсь им с теми, кто заинтересовался.
Настройка
Рассмотрим это перечисление:
package status
// MyStatus is an enumeration that indicates my liveliness
type MyStatus int
const (
Unknown MyStatus = 1 << iota
Born MyStatus = 2
Living MyStatus = 4
Dead MyStatus = 8
)
var me MyStatus
func init() {
me = Unknown
}
Тесты
package status
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestThatICanBeBorn(t *testing.T) {
// GIVEN
me = Unknown
assert.True(t, HasFlag(me, Unknown))
// WHEN
ForceFlag(&me, Born)
// THEN
assert.True(t, HasFlag(me, Born))
assert.True(t, NotHasFlag(me, Unknown))
}
func TestThatICanDie(t *testing.T) {
// GIVEN
me = Unknown
assert.True(t, HasFlag(me, Unknown))
// WHEN
SetFlag(&me, Born)
assert.True(t, HasFlag(me, Born))
// AND
SetFlag(&me, Living)
assert.True(t, HasFlag(me, Living))
// AND
UnsetFlag(&me, Born)
assert.True(t, NotHasFlag(me, Born))
// AND
ForceFlag(&me, Dead)
// THEN
assert.True(t, NotHasFlag(me, Born))
assert.True(t, NotHasFlag(me, Living))
assert.True(t, HasFlag(me, Dead))
}
func TestWeCanSetAndUnsetMultipleFlagsAtOnce(t *testing.T) {
// GIVEN
me = Unknown
assert.True(t, HasFlag(me, Unknown))
// WHEN
SetFlags(&me, Born, Living, Dead)
// THEN
assert.True(t, HasFlags(me, Born, Living, Dead))
// AND WHEN
UnsetFlags(&me, Born, Dead)
assert.True(t, !HasFlags(me, Born, Dead))
}
Реализация
package status
// HasFlag accepts an input status and will check if the flag is set using bitwise OR and return true if the flag is set.
func HasFlag[T ~int](status T, flag T) bool {
return status|flag == status
}
// NotHasFlag accepts an input status and will check if the flag is not set.
func NotHasFlag[T ~int](status T, flag T) bool {
return !HasFlag(status, flag)
}
// UnsetFlag accepts an input status reference and will unset the flag using bitwise AND NOT.
func UnsetFlag[T ~int](status *T, flag T) {
*status = *status &^ flag
}
// ForceFlag accepts an input status reference and will simply overwrite the status with the flag.
func ForceFlag[T ~int](status *T, flag T) {
*status = flag
}
// SetFlag accepts an input status reference and will set the flag to the status using bitwise OR.
func SetFlag[T ~int](status *T, flag T) {
*status = *status | flag
}
// SetFlags allows you to set multiple flags at once
func SetFlags[T ~int](status *T, flags ...T) {
if len(flags) == 0 {
return
}
for _, flag := range flags {
SetFlag[T](status, flag)
}
}
// HasFlags allows you to check for many flags at once.
func HasFlags[T ~int](status T, flags ...T) bool {
if len(flags) == 0 {
return false
}
for _, flag := range flags {
if NotHasFlag[T](status, flag) {
return false
}
}
return true
}
// UnsetFlags allows you to clear multiple flags at once.
func UnsetFlags[T ~int](status *T, flags ...T) {
if len(flags) == 0 {
return
}
for _, flag := range flags {
UnsetFlag[T](status, flag)
}
}
Тестовый вывод
/usr/local/go/bin/go test -json ./...
=== RUN TestThatICanBeBorn
--- PASS: TestThatICanBeBorn (0.00s)
=== RUN TestThatICanDie
--- PASS: TestThatICanDie (0.00s)
=== RUN TestWeCanSetAndUnsetMultipleFlagsAtOnce
--- PASS: TestWeCanSetAndUnsetMultipleFlagsAtOnce (0.00s)
PASS
ok <REDACTED>
0.003s
Process finished with the exit code 0
Заключение
Я думаю, что это отличный способ работы с флагами в Go, конечно, я все еще очень новичок в языке и мог пропустить встроенную функцию, которая сделает все это устаревшим. Для себя я немного лучше понял механику дженериков Go, кажется, что явное указание типа даже не требуется при вызове этих методов.
С нетерпением жду комментариев экспертов и возможности узнать больше!