encoding/xml предоставляет набор методов для работы с XML-форматом структурированных данных. Чаще всего язык разметки XML используется для создания и обработки отчётов и других документов. <report>
<competition>
<location>РФ, Санкт-Петербург, Дворец творчества юных техников</location>
<class>ТА-24</class>
</competition>
<racer global_id="100">
<nick>RacerX</nick>
<best_lap_ms>61012</best_lap_ms>
<laps>52.3</laps>
</racer>
<racer global_id="127">
<nick>Иван The Шумахер</nick>
<best_lap_ms>61023</best_lap_ms>
<laps>51</laps>
</racer>
<racer global_id="203">
<nick>Петя Иванов</nick>
<best_lap_ms>63000</best_lap_ms>
<laps>49.9</laps>
<!--Болид не соответствует техническому регламенту,
результат не учитывается в общем рейтинге-->
</racer>
<racer>
<nick>Гость 1</nick>
<best_lap_ms>123001</best_lap_ms>
<laps>25.8</laps>
</racer>
</report> encoding/xml структурных тегов и типов:type (
RaceReport struct {
XMLName xml.Name `xml:"report"`
CompetitionLocation string `xml:"competition>location"`
CompetitionClass string `xml:"competition>class"`
Results []RacerResult `xml:"racer"`
}
RacerResult struct {
XMLName xml.Name `xml:"racer"`
GlobalId int `xml:"global_id,attr,omitempty"`
Nick string `xml:"nick"`
BestLapMs int64 `xml:"best_lap_ms"`
Laps float32 `xml:"laps"`
Comment string `xml:",comment"`
}
) xml.Name содержит информацию об XML-элементе. Тег к полю XMLName типа xml.Name задаёт имя XML-элемента, а теги рядом с остальными полями структуры задают названия атрибутов и дочерних элементов. Когда тег для поля xml.Name отсутствует, в качестве имени элемента используется имя структуры.xml задаёт имя XML-элемента (xml:"element_name"). По аналогии с пакетом encoding/json, если явно не указать это имя, пакет encoding/xml будет использовать имя поля.xml можно комбинировать с другими опциями — например, вот так:xml:"a>b>c"CompetitionLocation и CompetitionClass: поскольку у них общий родитель (competition), в XML-представлении они объединяются в один тег.type RaceReport struct {
XMLName xml.Name `xml:"report"`
CompetitionLocation string `xml:"competition>location"`
CompetitionClass string `xml:"competition>class"`
} <report>
<competition>
<location>РФ, Санкт-Петербург, Дворец творчества юных техников</location>
<class>ТА-24</class>
</competition>
</report> type RaceReportCompetition struct для примера выше).xml:",attr"attr подсказывает пакету, что Go-поле описывает атрибут XML-тега.type RacerResult struct {
XMLName xml.Name `xml:"racer"`
GlobalId int `xml:"global_id,attr,omitempty"`
//...
} <racer global_id="100">
</racer> xml:",omitempty"xml:",comment"func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error // FilterXML оставляет в XML только тех гонщиков, у которых больше, чем laps, кругов.
func FilterXML(input string, laps float32) (output string, err error) {
var rp RaceReport
// десериализуем XML
err = xml.Unmarshal([]byte(input), &rp)
if err != nil {
return
}
// создаём новый список гонщиков
filter := make([]RacerResult, 0, len(rp.Results))
for _, racer := range rp.Results {
if racer.Laps > laps {
filter = append(filter, racer)
}
}
rp.Results = filter
// сериализуем данные в XML c оступами
var data []byte
data, err = xml.MarshalIndent(rp, "", " ")
if err != nil {
return
}
output = string(data)
return
} 50 во втором параметре, то она вернёт такие данные:<report>
<competition>
<location>РФ, Санкт-Петербург, Дворец творчества юных</location>
<class>ТА-24</class>
</competition>
<racer global_id="100">
<nick>RacerX</nick>
<best_lap_ms>61012</best_lap_ms>
<laps>52.3</laps>
</racer>
<racer global_id="127">
<nick>Иван The Шумахер</nick>
<best_lap_ms>61023</best_lap_ms>
<laps>51</laps>
</racer>
</report> package main
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
)
const MockXMLDocument = `
<?xml version="1.0" encoding="UTF-8"?>
<storage-report version="1.0" exported-on="2021-01-31" location-id="001">
<item barcode="000000000001">
<quantity>100</quantity>
</item>
<item barcode="000000000002">
<quantity>500</quantity>
</item>
</storage-report>
`
// Item — XML-представление складской единицы.
type Item struct {
XMLName xml.Name `xml:"item"`
Barcode string `xml:"barcode,attr"`
Quantity int64 `xml:"quantity"`
}
// ProcessStorageReportStream обрабатывает XML-отчёт по складу.
func ProcessStorageReportStream(stream io.Reader) error {
decoder := xml.NewDecoder(stream)
for {
// получаем следующий XML-токен
xmlToken, err := decoder.Token()
if err != nil {
// проверка на конец файла
if errors.Is(err, io.EOF) {
break
}
// ошибка чтения
return fmt.Errorf("reading XML token: %w", err)
}
switch xmlElement := xmlToken.(type) {
case xml.StartElement:
// идентифицируем XML-элемент по имени
if xmlElement.Name.Local == "item" {
var item Item
if err := decoder.DecodeElement(&item, &xmlElement); err != nil {
return fmt.Errorf("XML decode to (%T): %w", item, err)
}
HandleReportItem(item)
}
default:
}
}
return nil
}
// HandleReportItem обрабатывает складскую единицу из отчёта.
func HandleReportItem(item Item) {
fmt.Printf("Обработка складской позиции (%s):\n", item.Barcode)
fmt.Printf(" Количество [шт]: %d\n", item.Quantity)
}
func main() {
reader := bytes.NewReader([]byte(MockXMLDocument))
if err := ProcessStorageReportStream(reader); err != nil {
panic(err)
}
} Обработка складской позиции (000000000001):
Количество [шт]: 100
Обработка складской позиции (000000000002):
Количество [шт]: 500 xml.Token. XML-токен содержит информацию о прочитанном из потока XML-элементе. В цикле обрабатываются только элементы с типом xml.StartElement, так как для решения задачи не нужны другие типы (комментарий, директива).encoding/gob предоставляет возможности бинарного кодирования объектов. Этот специфичный для языка формат был разработан авторами Go и чаще всего используется для реализации удалённых вызовов процедур (RPC) между Go-сервисами. int и int64, float32 и float64). Подобного неявного преобразования типов стоит избегать. Если у поля исходной структуры тип int64 имеет значение math.MaxInt64, а у целевого поля тип int32, это приведёт к ошибке декодирования out of range.chan и func не кодируются.package main
import (
"bytes"
"encoding/gob"
"fmt"
)
// SendData — структура для кодирования.
type SendData struct {
Value int
Balance *float64
Name string
private int
}
// GetData — структура для декодирования.
type GetData struct {
Name string
Balance float64
Ext []byte
value int
}
func main() {
// создаём исходный объект
floatValue := 50.0
data := SendData{Value: 100, Balance: &floatValue, Name: "Василий Кузнецов",
private: 1} // создаём хранилище байт и энкодер/декодер
var buffer bytes.Buffer
// кодирование
if err := gob.NewEncoder(&buffer).Encode(data); err != nil {
panic(err)
}
// сейчас buffer содержит data в формате gob
// декодирование будет в переменную типа GetData
var out GetData
if err := gob.NewDecoder(&buffer).Decode(&out); err != nil {
panic(err)
}
fmt.Printf("out: %+v\n", out)
} out: {Name:Василий Кузнецов Balance:50 Ext:[] value:0} out на SendData, то результат будет примерно таким:out: {Value:100 Balance:0xc00001c418 Name:Василий Кузнецов private:0} bytes.Buffer для организации промежуточного хранилища. Обратите внимание, что значение поля Balance декодировалось успешно, несмотря на то что это не указатель в принимающей структуре. Поле value осталось без изменений, так как оно неэкспортируемое и игнорируется функциями пакета.type GetData struct {
SendData
Balance *string
} GetData таким образом, то получим ошибку:panic: gob: wrong type (*string) for received field SendData.Balance internal/models/models.go, в котором будут описаны модели данных:> ~/dev/alice-skill
|
|--- cmd
| |--- skill
| |--- flags.go
| |--- main.go
| |--- main_test.go
|--- internal
| |--- logger
| | |--- logger.go
| |--- models
| |--- models.go
|--- go.mod
|--- go.sum models.go поместим следующий код:package models
const (
TypeSimpleUtterance = "SimpleUtterance"
)
// Request описывает запрос пользователя.
// см. https://yandex.ru/dev/dialogs/alice/doc/request.html
type Request struct {
Request SimpleUtterance `json:"request"`
Version string `json:"version"`
}
// SimpleUtterance описывает команду, полученную в запросе типа SimpleUtterance.
type SimpleUtterance struct {
Type string `json:"type"`
Command string `json:"command"`
}
// Response описывает ответ сервера.
// см. https://yandex.ru/dev/dialogs/alice/doc/response.html
type Response struct {
Response ResponsePayload `json:"response"`
Version string `json:"version"`
}
// ResponsePayload описывает ответ, который нужно озвучить.
type ResponsePayload struct {
Text string `json:"text"`
} cmd/skill/main.go. Для начала импортируем модели:import (
"net/http"
"go.uber.org/zap"
"github.com/bluegopher/alice-skill/internal/logger"
"github.com/bluegopher/alice-skill/internal/models"
) func webhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
logger.Log.Debug("got request with bad method", zap.String("method", r.Method))
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// десериализуем запрос в структуру модели
logger.Log.Debug("decoding request")
var req models.Request
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&req); err != nil {
logger.Log.Debug("cannot decode request JSON body", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
// проверяем, что пришёл запрос понятного типа
if req.Request.Type != models.TypeSimpleUtterance {
logger.Log.Debug("unsupported request type", zap.String("type", req.Request.Type))
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
// заполняем модель ответа
resp := models.Response{
Response: models.ResponsePayload{
Text: "Извините, я пока ничего не умею",
},
Version: "1.0",
}
w.Header().Set("Content-Type", "application/json")
// сериализуем ответ сервера
enc := json.NewEncoder(w)
if err := enc.Encode(resp); err != nil {
logger.Log.Debug("error encoding response", zap.Error(err))
return
}
logger.Log.Debug("sending HTTP 200 response")
} package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
)
func TestWebhook(t *testing.T) {
handler := http.HandlerFunc(webhook)
srv := httptest.NewServer(handler)
defer srv.Close()
successBody := `{
"response": {
"text": "Извините, я пока ничего не умею"
},
"version": "1.0"
}`
testCases := []struct {
name string // добавляем название тестов
method string
body string // добавляем тело запроса в табличные тесты
expectedCode int
expectedBody string
}{
{
name: "method_get",
method: http.MethodGet,
expectedCode: http.StatusMethodNotAllowed,
expectedBody: "",
},
{
name: "method_put",
method: http.MethodPut,
expectedCode: http.StatusMethodNotAllowed,
expectedBody: "",
},
{
name: "method_delete",
method: http.MethodDelete,
expectedCode: http.StatusMethodNotAllowed,
expectedBody: "",
},
{
name: "method_post_without_body",
method: http.MethodPost,
expectedCode: http.StatusInternalServerError,
expectedBody: "",
},
{
name: "method_post_unsupported_type",
method: http.MethodPost,
body: `{"request": {"type": "idunno", "command": "do something"}, "version": "1.0"}`,
expectedCode: http.StatusUnprocessableEntity,
expectedBody: "",
},
{
name: "method_post_success",
method: http.MethodPost,
body: `{"request": {"type": "SimpleUtterance", "command": "sudo do something"}, "version": "1.0"}`,
expectedCode: http.StatusOK,
expectedBody: successBody,
},
}
for _, tc := range testCases {
t.Run(tc.method, func(t *testing.T) {
req := resty.New().R()
req.Method = tc.method
req.URL = srv.URL
if len(tc.body) > 0 {
req.SetHeader("Content-Type", "application/json")
req.SetBody(tc.body)
}
resp, err := req.Send()
assert.NoError(t, err, "error making HTTP request")
assert.Equal(t, tc.expectedCode, resp.StatusCode(), "Response code didn't match expected")
// проверяем корректность полученного тела ответа, если мы его ожидаем
if tc.expectedBody != "" {
assert.JSONEq(t, tc.expectedBody, string(resp.Body()))
}
})
}
} encoding/xml.encoding/gob.