Работа с флагами в Go с использованием дженериков


Проблема

Во время написания небольшой государственной машины в 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, кажется, что явное указание типа даже не требуется при вызове этих методов.
С нетерпением жду комментариев экспертов и возможности узнать больше!

Оцените статью
devanswers.ru
Добавить комментарий