Skip to content

(RU) Decoding walkthrough

Richard Cooper edited this page Oct 24, 2022 · 2 revisions

самый базовый пример декодирования простейшей структуры

опциональные флаги

битфлаги

работа с интерфейсами (типами)

получение неизвестного типа

Довольно часто в определении методов можно встретить такую ситуацию:

// Bool is a special case, we don't need to worry about it.
messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool;

// Update is more interesting case: как нам понять, какой конструктор нужно
// сформировать? ведь в go интерфейсы не имеют своих методов!
getUpdates#25d6c9c7 channel_id:int max_id:int = Updates;

Если с первым все просто и понятно (попробуй crc код от true а потом false), то с комплексными типами не понятно — как понять какую структуру нам нужно использовать? где найти ее инстанс? Мы будем искать перебором? А если нам попадется некорректный crc код?

На самом деле существует два теоретических способа избавиться от таких проблем:

  1. Использовать полный регистр всех возможных crc кодов, что бы упростить сам код
  2. Использовать конструкторы для каждого интерфейса, использовать только регистр конструкторов

У каждого способа есть свои плюсы и минусы, о которых мы расскажем потом, однако сейчас мы постараемся описать как решить эту проблему.

Итак, предположим нам нужно имплементировать такие типы:

updatesTooLong#e317af7e = Updates;
updateShort#12345678 = Updates;
updateShortMessage#78d4dec1 ... = Updates;
updateShortChatMessage#402d5dbb ... = Updates;

Мы могли бы завести регистр всех типов, и искать объекты по глобальному регистру. Однако, мы понимаем, что это не самый лучший вариант, поэтому давайте создадим конструктор, который позволит нам сгенерировать инстанс объекта для того, что бы в него записать наши данные:

type Updates interface {
    tl.Object
    _Updates()
}

func NewUpdatesByCRC(crc uint32) Updates {
    switch crc {
    //         ↓↓↓      Like in type assertions, go isn't allocating type here.
    case (*UpdateShort)(nil).CRC():
        return &UpdateShort{}
    case (*UpdateShortChatMessage)(nil).CRC():
        return &UpdateShortChatMessage{}
    case uint32(UpdatesTooLong),
        uint32(UpdatesTooShort):
        return UpdatesEnum(crc)
    default:
        return nil
    }
}

type UpdateShort struct {
    ...
}

func (*UpdateShort) CRC() uint32 { return 0x78d4dec1 }
func (*UpdateShort) _Updates()   {}

type UpdateShortChatMessage struct {
    ...
}

func (*UpdateShortChatMessage) CRC() uint32 { return 0x402d5dbb }
func (*UpdateShortChatMessage) _Updates()   {}

type UpdateShortMessage struct {
    ...
}

// for constructors which have no flags, we can create them as enums
type UpdatesEnum uint32

const (
    UpdatesTooLong  UpdatesEnum = 0xe317af7e
    UpdatesTooShort UpdatesEnum = 0x12345678
)

func (e UpdatesEnum) CRC() uint32 { return uint32(e) }
func (_ UpdatesEnum) _Updates()   {}

Ok, мы объявили все типы, все конструкторы, как же нам теперь получить интерфейс из ответа?

Мы могли бы так же сделать декодер примерно такого формата:

tl.Unmarshal(data, NewUpdatesByCRC).(Update)

Но это помимо того что не безопасно, так еще и не go-way:

var obj Update

json.Unmarshal(data, obj)
xml.Unmarshal(data, obj)
gob.Unmarshal(data, obj)
tl.Unmarshal(data, NewUpdatesByCRC).(Update) // Wtf??? 😨

Так что это выглядит некрасиво.

Как же все таки это работает?

Для этого есть тип AnyConstructor

AnyConstructor[T] это одно из решений, как можно ограничить набор возможных значений, даже не применяя регистр или что-то подобное. Мы просто можем передать тип вместе с конструктором! (Thank you, generics🤗)

func UseIt(u Update) {
    ...
}


obj := &tl.AnyConstructor[Update]{
    Constructor: NewUpdatesByCRC,
}

tl.Unmarshal(data, obj) // Yeah, that's better 😉

// Yaaay! Guaranteed to be Update, if tl message is incorrect, unmarshaler reports it, not golang! 
UseIt(obj.Result)

Run in playground

работа с объявленными методами