Обережно кодогенерація
Tags: go, json, protobuf, ukrainian
Published: 2020-09-21 17:15:51

Раніше вже писав про <a href="https://dou.ua/lenta/articles/protobuf-vs-json-go-benchmark/" target="_blank">збільшення швидкодії та зменшення використання пам'яті після використання кодогенерації</a> і ця історія має продовження, а саме розбір помилок.

<h3>Protobuf, перша помилка яка показала себе через пару тижнів після змін в коді</h3>
В проекті ми використовуємо офіційну бібліотеку Protobuf <a href="https://github.com/protocolbuffers/protobuf" target="_blank">github.com/protocolbuffers/protobuf</a>, яка підчас серіалізації використовує рефлексію і будує слайс байтів через append.
А потім я дізнався про "Protocol Buffers for Go with Gadgets" <a href="https://github.com/gogo/protobuf" target="_blank">github.com/gogo/protobuf</a>, бібліотеку-fork яка генерує додатковий код щоб прибрати рефексію підчас серіалізації і вже записує в слайс байтів по індексу бо так швидше.
Коли змінював одну бібліотеку на іншу то важливим вважав що стало працювати швидше і написані раніше тести пройшли успішно.
І все б було гаразд але в проекті існувала латка яка через пару тижнів після заміни перезапустила мікросервіс через паніку:
```text
panic: runtime error: index out of range
```
Латка виглядала приблизно так:
```golang
import (
	"github.com/golang/protobuf/proto"
	google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
)

func example() {
	var popup = &google.Popup{
		Id:      uuid(),
		Viewed:  true,
		Clicked: false,
	}

	// some deep nested function
	go func() {
		var content, err = proto.Marshal(popup)

		if err != nil {
			// log error

			return
		}

		// store to database
		store(content)
	}()

	// some delay with other actions

	// @temporary hack
	go func() {
		popup.Clicked = true

		var content, err = proto.Marshal(popup)

		if err != nil {
			// log error

			return
		}

		// store to database again
		store(content)
	}()
}
```
І зі стандартною бібліотекою латка працювала без паніки для мікросервісу який працює постійно:
```golang
import (
	"github.com/golang/protobuf/proto"
	google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
	"testing"
)

const (
	n = 1000000
)

func TestGoogleProtoMarshal(t *testing.T) {
	for i := 0; i < n; i++ {
		var popup = &google.Popup{
			Id:      uint32(i),
			Viewed:  true,
			Clicked: false,
		}

		// some deep nested function
		go func() {
			_, _ = proto.Marshal(popup)
		}()

		// @temporary hack
		go func() {
			popup.Clicked = true

			_, _ = proto.Marshal(popup)
		}()
	}
}
```
А от з <a href="https://github.com/gogo/protobuf" target="_blank">github.com/gogo/protobuf</a> при аналогічному тесті вже видає паніку.
Якщо розглянути згенерований код:
```golang
func (m *Popup) Marshal() (dAtA []byte, err error) {
	size := m.Size()
	dAtA = make([]byte, size)
	n, err := m.MarshalToSizedBuffer(dAtA[:size])
	if err != nil {
		return nil, err
	}
	return dAtA[:n], nil
}

func (m *Popup) MarshalToSizedBuffer(dAtA []byte) (int, error) {
	i := len(dAtA)

	//...

	if m.Clicked {
		i--
		dAtA[i] = 1
		i--
		dAtA[i] = 0x18
	}

	//...

	return len(dAtA) - i, nil
}
```
то стає зрозуміло що розрахунок ємності слайсу відбувався за умов <b>m.Clicked = false</b>, а серіалізація за умов <b>m.Clicked = true</b> і таким чином отримав паніку "index out of range".
Звісно латку ми виправили і стало працювати навіть краще.


<h3>JSON, помилка у vendor бібліотеці</h3>
Бібліотека <a href="https://github.com/mailru/easyjson" target="_blank">easyjson</a> теж для серіалізації працює через додатковий код замість використання рефлексії.
Але після внесення в easyjson одної з оптимізацій, час від часу почали отримувати зламаний JSON, ось приклад тесту який покаже помилку.
```golang
package tests

import (
	"github.com/stretchr/testify/require"
	"gitlab.com/go-yp/go-warning-codegeneration/models/jsons/easy"
	"testing"
)

const (
	// language=JSON
	popupWithUnicodeContent = `{
        "title": "Some title with symbol \u201Dt",
        "description": "Any description"
    }`

	// language=JSON
	popupContent = `{
        "title": "Some title",
        "description": "Any description"
    }`
)

func TestEasyjsonUnmarshalJSON(t *testing.T) {
	content := make([]byte, 0, 1024)

	content = append(content[:0], popupWithUnicodeContent...)

	var popup easy.Popup

	unmarshalErr := popup.UnmarshalJSON(content)

	require.NoError(t, unmarshalErr)

	var expected = easy.Popup{
		Title:       "Some title with symbol \u201Dt",
		Description: "Any description",
	}

	require.Equal(t, expected, popup)

	content = append(content[:0], popupContent...)

	/**
	Failed:
	expected: easy.Popup{Title:"Some title with symbol ”t", Description:"Any description"}
	actual  : easy.Popup{Title:"Some title with symbol ”t", Description:" }y description"}
	*/
	require.Equal(t, expected, popup)
}
```
В easyjson цю помилку вже виправили.

<h3>Висновки:</h3>
Звісно хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.
Приклади доступні в <a href="https://gitlab.com/go-yp/go-warning-codegeneration" target="_blank">репозиторії</a>.