Изучаем Go: руководство для JavaScript-разработчиков. Часть 1

от автора

После пяти лет работы JavaScript-разработчиком, занимаясь как фронтендом, так и бэкендом, я провел последний год, осваивая Go для серверной разработки. За это время мне пришлось переосмыслить многие вещи. Различия в синтаксисе, базовых принципах, подходах к организации кода и, конечно, в средах выполнения — все это довольно сильно влияет не только на производительность приложения, но и на эффективность разработчика.

Интерес к Go в JavaScript-сообществе тоже заметно вырос. Особенно после новости от Microsoft о том, что они переписывают официальный компилятор TypeScript на Go — и обещают ускорение до 10 раз по сравнению с текущей реализацией.

Эта статья — своего рода путеводитель для JavaScript-разработчиков, которые задумываются о переходе на Go или просто хотят с ним познакомиться. Я постарался структурировать материал вокруг ключевых особенностей языка, сравнивая их с привычными концепциями из JavaScript/TypeScript. И, конечно, расскажу о «подводных камнях», с которыми столкнулся лично — с багажом мышления JS-разработчика.

В этой части мы рассмотрим следующие аспекты этих языков:

  • Основы

    • Компиляция и выполнение

    • Пакеты

    • Переменные

    • Структуры и типы

    • Нулевые значения

    • Указатели

    • Функции

  • Массивы и срезы

  • Отображения (maps)

Вторая часть

Поскольку у JavaScript имеется несколько сред выполнения, во избежание лишней путаницы, в этой статье я буду сравнивать Go с Node.js — ведь и Go, и Node в первую очередь используются на сервере. Кроме того, сегодня TypeScript фактически является стандартом в веб-разработки, поэтому большинство примеров в статье будет на нем.

Основы

Компиляция и выполнение

Первое фундаментальное различие — то, как выполняется код. Go — это компилируемый язык, то есть перед запуском код необходимо собрать в исполняемый бинарный файл, содержащий машинный код. В свою очередь, JavaScript интерпретируемый язык, код можно выполнять сразу, без предварительной компиляции (в V8 существует ряд оптимизаций, выполняемых в процессе JIT-компиляции — например, он умеет выявлять «горячие» участки кода (hot paths) и компилировать их в машинный код, но эти детали выходят за рамки статьи, поэтому углубляться в них не будем).

Например, в Node.js можно просто создать JS-файл и сразу запустить его через командную строку с помощью node:

// hello.js  console.log("Hello, World!") 
node hello.js Hello, World! 

Чтобы начать работать с Go, нужно скачать бинарную версию языка под вашу операционную систему с официального сайта: https://go.dev/dl/.

Вот как выглядит классическая программа «Hello, World!» на Go:

// hello.go  package main  import "fmt"  func main() {     fmt.Println("Hello, World!") } 

Подробности синтаксиса, использованного в примере, мы рассмотрим в следующих разделах.

Чтобы запустить эту программу, ее сначала нужно скомпилировать, а затем выполнить полученный бинарный файл:

go build hello.go  ./hello Hello, World! 

Или можно воспользоваться командой run, которая компилирует и запускает программу за один шаг:

go run hello.go Hello, World! 

Поскольку Go компилируется в нативный машинный код, для разных платформ нужно создавать отдельные бинарные файлы под соответствующую архитектуру. К счастью, в Go это делается довольно просто с помощью переменных окружения GOOS и GOARCH.

Пакеты

Любая программа на Go состоит из пакетов (модулей, package) и всегда начинается с выполнения пакета main. Внутри этого пакета обязательно должна быть функция с именем main — именно она служит точкой входа в программу. Когда выполнение main() завершается, программа завершает свою работу.

// main.go  package main  import (   "fmt" )  func main() {   fmt.Println("Hello world") } 

Для краткости в остальных примерах я буду опускать package main и func main(). Если захочется посмотреть, как работает тот или иной фрагмент, можно будет воспользоваться ссылками на Go Playground.

Пакеты в Go во многом похожи на модули в JS — это просто набор связанных между собой исходных файлов. Создание и импорт пакетов в Go напоминает импорт модулей в JS. Например, в приведенном выше фрагменте мы импортируем пакет fmt из стандартной библиотеки Go.

fmt (сокращение от format) — один из базовых пакетов в Go. Он отвечает за форматированный ввод/вывод и во многом повторяет подход, использованный в printf и scanf из языка C. В примере выше мы использовали функцию Println, которая выводит аргументы в дефолтном формате и добавляет перевод строки в конце.

Далее по тексту вы также встретите функцию Printf — она позволяет выводить текст, отформатированный с помощью спецификаторов. Подробнее о доступных спецификаторах можно почитать в официальной документации.

Аналогично тому, как в JS-проектах используется файл package.json, в Go-программах есть файл go.mod. Это конфигурационный файл модуля, в котором содержится информация о самом модуле и его зависимостях. Пример стандартного go.mod:

module myproject  go 1.16  require (   github.com/gin-gonic/gin v1.7.4   golang.org/x/text v0.3.7 ) 

Первая строка указывает путь импорта модуля, который служит его уникальным идентификатором. Вторая строка — минимально требуемая версия Go для работы модуля. Далее идут все зависимости — как прямые, так и косвенные — с указанием конкретных версий.

Чтобы создать пакет в Go, достаточно создать новую директорию с нужным именем — и все Go-файлы внутри нее автоматически будут частью этого пакета, если в начале каждого файла указано соответствующее имя с помощью директивы package.

Интересно реализована и система экспорта. В JS (с ESM) мы явно указываем export, чтобы сделать функцию или переменную доступной за пределами модуля.
В Go все проще: если имя начинается с заглавной буквы — оно экспортируется.

Пример ниже демонстрирует все вышесказанное:

// go.mod  module myproject  go 1.24  // main.go  package main  import (   "fmt"   "myproject/fib" )  func main() {   sequence := fib.FibonacciSequence(10)    // Это вызовет ошибку   // firstFibonacciNumber := fib.fibonacci(1)    fmt.Println("Fibonacci sequence of first 10 numbers:")   fmt.Println(sequence) }  // fib/fib.go  package fib  // Эта функция не экспортируется, так как ее имя начинается с маленькой буквы func fibonacci(n int) int {     if n <= 0 {         return 0     }     if n == 1 {         return 1     }      return fibonacci(n-1) + fibonacci(n-2) }  // Эта функция экспортируется, так как ее имя начинается с заглавной буквы func FibonacciSequence(n int) []int {     sequence := make([]int, n)      for i := 0; i < n; i++ {         sequence[i] = fibonacci(i)     }      return sequence } 

В приведенном примере мы создали пакет fib, просто создав директорию с таким именем.

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

Переменные

Go — это язык со статической типизацией, то есть тип каждой переменной должен быть либо явно указан, либо выведен автоматически, и проверка типов выполняется еще на этапе компиляции. В отличие от JS, где переменные могут содержать значения любого типа, и типизация проверяется только во время выполнения программы.

Например, в JS вполне допустим следующий код:

let x = 5; let y = 2.5; let sum = x + y;     // Все работает: 7.5 let weird = x + "2"; // Тоже "работает": "52" (но, возможно, это не совсем то, что мы ожидали получить) 

А вот в Go с типами нужно быть гораздо осторожнее: все примитивные типы перечислены здесь.

Ключевое слово var в Go выполняет примерно ту же роль, что и let в современном JS.

var x int = 5 // Или x := 5 — это короткое присваивание (short assignment), // которое можно использовать вместо var с неявным указанием типа  var y float64 = 2.5  // Такой код не скомпилируется sum := x + y  // Error: mismatched types int and float64  // Преобразовывать тип следует явно sum := float64(x) + y 

Стоит отметить, что TypeScript помогает решить проблему с типами в JS, но в конечном итоге это все же лишь синтаксическое расширение JS, которое компилируется все в тот же JS.

Аналогично JS, в Go тоже есть ключевое слово const, которое используется для объявления констант. Объявляются они так же, как переменные, но с использованием const вместо var:

const pi float64 = 3.14  // Или без указания типа, он будет определен автоматически const s = "hello" 

В отличие от JS, в Go с помощью const можно объявлять только примитивные значения — такие как символы, строки, логические и числовые типы. Для более сложных типов данных const в Go не применяется.

В Go объявление переменной, которая затем не используется, приводит не к предупреждению, как это бывает в JavaScript или TypeScript при использовании линтеров, а к полноценной ошибке компиляции.

Структуры и типы

В JS для представления набора полей используют объекты. В Go для этого существуют структуры (structs):

type Person struct {   Name string   Age  int }  p := Person{   Name: "John",   Age: 32, }  // Создаем составную структуру type User struct {   Person Person   ID     string }  u := User{   Person: p,   ID:     "123", } 

В Go поля структуры нужно именовать с заглавной буквы, чтобы они были экспортируемыми (то есть доступными в других пакетах или для сериализации в JSON). Поля с именами, начинающимися со строчной буквы, не экспортируются и доступны только внутри пакета.

На первый взгляд синтаксис может показаться похожим на TypeScript — особенно на типы или интерфейсы, но поведение отличается. В TypeScript типы только определяют форму значений (контракт), поэтому допустимо передать объекты, содержащие больше полей, чем указано в типе — и это сработает без ошибок.

В Go же структуры — это конкретные типы данных, и совместимость при присваивании определяется по имени, а не по структуре. Так что если в TypeScript такой код будет работать:

interface Person {   name: string,   age: number }  interface User {   name: string,   age: number,   username: string }  function helloPerson(p: Person) {   console.log(p) }  helloPerson({   name: "John",   age: 32 })  const x: User = {   name: "John",   age: 32,   username: "john", }  helloPerson(x) 

То в Go нет:

type Person struct {   Name string   Age  int }  type User struct {   Name     string   Age      int   Username string }  func HelloPerson(p Person) {   fmt.Println(p) }  func main() {   // Этот вариант работает без ошибок   HelloPerson(Person{     Name: "John",     Age:  32,   })    // Этот — не сработает   x := User{     Name:     "John",     Age:      32,     Username: "john",   }    // Error: cannot use x (type User) as type Person in argument to HelloPerson   HelloPerson(x)    // Чтобы все заработало, нужно выполнить явное преобразование   // HelloPerson(Person{Name: x.Name, Age: x.Age}) } 

type в Go используется не только для определения структур. С их помощью можно определять любые значения, которые может хранить переменная:

type ID int  var i ID i = 2 

Часто встречающийся сценарий — создание строковых перечислений (enum):

type Status string  const (   StatusPending  Status = "pending"   StatusApproved Status = "approved"   StatusRejected Status = "rejected" )  type Response struct {   Status Status   Meta   string }  res := Response{   Status: StatusApproved,   Meta:   "Request successful", } 

В отличие от исключающих объединений (discriminated unions) в TypeScript, пользовательские типы в Go (например, Status) — это лишь псевдонимы для базового типа. Переменной типа Status можно присвоить любую строку:

var s Status s = "hello" // Это компилируется 

В TypeScript система типов является полноценно вычислимой (Turing complete), что позволяет расширять и преобразовывать существующие типы, создавать новые и выполнять сложные вычисления непосредственно на уровне типов. Это открывает возможности для продвинутой проверки типов и создания безопасных абстракций:

type Person = {   firstName: string;   lastName: string;   age: number; }  // Расширенный тип, включающий все свойства Person // и добавляющий дополнительные свойства type Doctor = Person & {   speciality: string; }  type Res = { status: "success", data: Person } | { status: "error", error: string }  // Res — исключающее объединение, которое позволяет // обращаться к разным свойствам в зависимости от статуса function getData(res: Res) {   switch (res.status) {     case "success":       console.log(res.data)       break;     case "error":       console.log(res.error)       break;   } }  // Тип, в котором все свойства необязательны type OptionalDoctor = Partial<Doctor>  // Тип, содержащий только свойства firstName и speciality type MinimalDoctor = Pick<Doctor, "firstName" | "speciality"> 

В Go структуры в первую очередь служат контейнерами для данных и не обладают возможностями изменения типов, как это реализовано в TypeScript. Ближайший аналог этому в Go — встраивание структур (struct embedding), которое позволяет реализовать композицию и представляет собой своего рода наследование:

type Person struct {   FirstName string   LastName  string }  type Doctor struct {   Person   Speciality string }  d := Doctor{   Person: Person{     FirstName: "Bruce",     LastName:  "Banner",   },   Speciality: "gamma", }  fmt.Println(d.Person.FirstName) // Bruce  // Ключи встроенных структур "поднимаются" наверх, // поэтому этот вариант тоже работает fmt.Println(d.FirstName) // Bruce 

Нулевые значения

Еще одна вещь, которая может сбить с толку JS-разработчика — это концепция нулевых значений в Go. В JS, если объявить переменную без присвоения значения, ее значением по умолчанию будет undefined:

let x: number | undefined;  console.log(x); // undefined  x = 3  console.log(x) // 3 

В Go, если определить переменную без явного значения, ей автоматически присваивается так называемое «нулевое значение». Вот какие значения по умолчанию получают некоторые примитивные типы:

var i int // 0 var f float64 // 0 var b bool // false var s string // ""  x := i + 7 // 7 y := !b // true z := s + "string" // string 

Аналогично, структуры в Go получают нулевые значения по умолчанию для всех своих полей:

type Person struct {     name string  // ""     age  int     // 0 }  p := Person{} // Создает структуру Person с пустым именем и возрастом 0 

В Go есть значение nil, похожее на null в JS, но его могут принимать только переменные ссылочных (reference) типов. Чтобы понять, что это за типы, нужно разобраться с указателями (pointers) в Go.

Указатели

В Go есть указатели, похожие на те, что используются в языках C и C++, где указатель хранит в памяти адрес, по которому находится значение.

Указатель на тип T объявляется с помощью синтаксиса *T. Нулевое значение любого указателя в Go — это nil.

var i *int  i == nil // true 

Оператор & создает указатель на свой операнд, а оператор * получает значение по указателю — это называется разыменованием (dereferencing) указателя:

x := 42 i := &x fmt.Println(*i) // 42  *i = 84 fmt.Println(x) // 84 

Следует иметь в виду, что попытка разыменования указателя, равного nil, приведет к ошибке null pointer dereference:

var x *string  fmt.Println(*x) // panic: runtime error: invalid memory address or nil pointer dereference 

Это подводит нас к важному отличию для JS-разработчиков: за исключением примитивных значений, в JS все передается по ссылке автоматически, тогда как в Go это делается явно с помощью указателей. Например, объекты в JS передаются по ссылке, поэтому если изменить объект внутри функции, изменится и исходный объект:

let obj = { value: 42 }  function modifyObject(o) {     o.value = 84  // Исходный объект изменяется }  modifyObject(obj) console.log(obj.value)  // 84 

В Go почти все передается по значению (кроме срезов (slices), отображений (maps) и каналов (channels), о чем мы поговорим позже), если не использовать указатели. Поэтому такой код в Go работать не будет:

type Object struct {   Value int }  func modifyObject(o Object) {   o.Value = 84 }  o := Object{Value: 42} modifyObject(o) fmt.Println(o.Value) // 42 

Но если использовать указатели:

func modifyObjectPtr(o *Object) {   o.Value = 84  // Упрощенный синтаксис для работы со структурами,   // фактически выполняется (*o).Value }  o := Object{Value: 42} modifyObjectPtr(&o) fmt.Println(o.Value) // 84 

Это связано с тем, что при передаче указателя мы передаем адрес памяти исходного объекта, что позволяет напрямую менять его значение. И это касается не только структур — указатель можно создать для любого типа, включая примитивные:

func modifyValue(x *int) {   *x = 100 }  y := 42 modifyValue(&y) fmt.Println(y) // 100 

Функции

Мы уже вкратце рассмотрели функции в Go в предыдущем разделе, и, как вы, наверное, уже догадались, они во многом похожи на функции в JS. Их сигнатура тоже довольно схожа, за исключением ключевого слова — в Go используется func вместо function.

func greet(name string) string {   if name == "" {     name = "there"   }   return "Hello, " + name } 

Как и в JS, функции в Go являются первоклассными (first-class) — их можно присваивать переменным, передавать в качестве аргументов и возвращать из других функций. Благодаря этому поддерживаются функции высшего порядка и замыкания. Например:

func makeMultiplier(multiplier int) func(int) int {   return func(x int) int {     return x * multiplier   } }  double := makeMultiplier(2)  double(2) // 4 

В Go также можно возвращать несколько значений из функции. Этот подход особенно полезен при обработке ошибок — к этому мы еще вернемся в одном из следующих разделов:

func parseName(fullName string) (string, string) {     parts := strings.Split(fullName, " ")     if len(parts) < 2 {         return parts[0], ""     }     return parts[0], parts[1] }  firstName, lastName := parseName("Bruce Banner")  fmt.Printf("%s, %s", lastName, firstName) // Banner, Bruce 

Массивы и срезы

В Go, в отличие от JS, массивы имеют фиксированную длину — она является частью их типа, поэтому менять ее нельзя. Пусть это и звучит как ограничение, но у Go есть удобное решение, которое мы рассмотрим позже.

Давайте освежим в памяти, как массивы работают в JS:

let s: Array<number> = [1, 2, 3];  s.push(4)  s[1] = 0  console.log(s) // [1, 0, 3, 4] 

Чтобы объявить массив в Go, нужно указать его размер, например так:

var a [3]int // Это создает массив из 3 элементов с нулевыми значениями: [0 0 0]  a[1] = 2 // [0 2 0]  // Можно также определить массив с начальными значениями b := [3]int{1,2,3} 

Обратите внимание, что метода push нет — в Go массивы имеют фиксированную длину. И вот тут на сцену выходят срезы (slices). Срез — это динамически изменяемый и гибкий «прозрачный» доступ к массиву:

c := [6]int{1,2,3,4,5,6}  d := c[1:4] // [2 3 4] 

С первого взгляда это может показаться похожим на срез в JS, но важно помнить: в JS срез — это поверхностная копия массива, а в Go срез хранит ссылку на исходный массив. Поэтому в JS это работает:

let x: Array<number> = [1, 2, 3, 4, 5, 6];  let y = x.slice(1, 4)  y[1] = 0  console.log(x, y) // x = [1, 2, 3, 4, 5, 6] // y = [2, 0, 4] 

Изменение среза в Go влияет на исходный массив, поэтому для приведенного выше примера:

y[1] = 0  fmt.Println(x) // [1 0 3 4 5 6] 

Интересная особенность — литералы срезов. Их можно создавать без указания длины массива:

var a []int  // или b := []int{1,2,3}  a == nil // true 

Для переменной b создается тот же массив, что мы видели ранее, но b хранит срез, который ссылается на этот массив. И если вспомнить нулевые значения из предыдущего раздела, то нулевым значением для среза является nil, поэтому в приведенном примере a будет иметь значение nil, так как указатель на базовый массив равен nil.

Кроме базового массива, срезы также имеют длину и емкость: длина — это количество элементов, которые срез содержит в данный момент, а емкость — количество элементов в базовом массиве. Доступ к длине и емкости среза можно получить с помощью методов len и cap, соответственно:

s := []int{1,2,3,4,5,6}  t := s[0:3]  fmt.Printf("len=%d cap=%d %v\n", len(t), cap(t), t) // len=3 cap=6 [1 2 3] 

В приведенном примере срез t имеет длину 3, так как он был взят из исходного массива именно с таким количеством элементов, но исходный массив при этом имеет емкость 6.

Также можно использовать встроенную функцию make для создания среза с помощью синтаксиса make([]T, len, cap). Эта функция выделяет нулевой массив и возвращает срез, ссылающийся на этот массив:

a := make([]int, 5)     // len(a)=5, cap(a)=5  b := make([]int, 0, 5)  // len(b)=0, cap(b)=5 

В Go есть встроенная функция append, которая позволяет добавлять элементы в срез, не думая о его длине и емкости:

a := []int{1,2,3}  a = append(a,4) // [1 2 3 4] 

append() всегда возвращает срез, который содержит все элементы исходного среза плюс добавленные значения. Если исходный массив слишком мал, чтобы вместить новые элементы, append() создает новый массив большего размера и возвращает срез, указывающий на этот новый массив (команда Go подробно объясняет, как это работает, в одной из своих статей).

В отличие от JS, в Go нет встроенных декларативных функций высшего порядка, таких как map, reduce, filter и т.п. Поэтому для обхода срезов или массивов используется обычный цикл for:

for i, num := range numbers {   fmt.Println(i, num) }  // Или так, если требуется только само число // for _, num := range numbers 

И напоследок: как известно, в JS массивы — это не примитивный тип, поэтому они всегда передаются по ссылке:

function modifyArray(arr) {   arr.push(4);   console.log("Внутри функции:", arr); // Внутри функции: [1, 2, 3, 4] }  const myArray = [1, 2, 3]; modifyArray(myArray); console.log("Снаружи функции:", myArray); // Снаружи функции: [1, 2, 3, 4] 

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

func modifyArray(arr [3]int) {   arr[0] = 100   fmt.Println("Массив внутри функции:", arr) // Массив внутри функции: [100, 2, 3] }  func modifySlice(slice []int) {   slice[0] = 100   fmt.Println("Срез внутри функции:", slice) // Срез внутри функции: [100, 2, 3] }  myArray := [3]int{1, 2, 3} mySlice := []int{1, 2, 3}  modifyArray(myArray) fmt.Println("Массив после вызова функции:", myArray) // Массив после вызова функции: [1, 2, 3]  modifySlice(mySlice) fmt.Println("Срез после вызова функции:", mySlice) // Срез после вызова функции: [100, 2, 3] 

Отображения (maps)

В Go отображения по своей сути гораздо ближе к Map в JS, чем к обычным JS-объектам (JSON), которые чаще всего используются для хранения пар ключ–значение.

Давайте вспомним, как работают отображения в JS:

const userScores: Map<string, number> = new Map();  // Добавляем пары ключ–значение userScores.set('Alice', 95); userScores.set('Bob', 82); userScores.set('Charlie', 90);  // Определяем интерфейс для объекта с возрастом пользователя interface UserAgeInfo {   age: number; }  // Альтернативное создание Map с начальными значениями и использованием интерфейса const userAges: Map<string, UserAgeInfo> = new Map([   ['Alice', { age: 28 }],   ['Bob', { age: 34 }],   ['Charlie', { age: 22 }] ]);  // Получаем значения console.log(userScores.get('Alice')); // 95  // Удаляем элемент userScores.delete('Bob');  // Размер отображения (количество элементов) console.log(userScores.size); // 2 

А вот как с отображениями работают в Go:

// Создание отображения userScores := map[string]int{     "Alice":   95,     "Bob":     82,     "Charlie": 90, }  type UserAge struct {     age int }  // Альтернативный способ создания userAges := make(map[string]UserAge) userAges["Alice"] = UserAge{age: 28} userAges["Bob"] = UserAge{age: 34} userAges["Charlie"] = UserAge{age: 22}  // Получаем значения aliceScore := userScores["Alice"] fmt.Println(aliceScore) // 95  // Удаляем элемент delete(userScores, "Bob")  // Размер отображения fmt.Println(len(userScores)) // 2 

Стоит отметить, что если обратиться к ключу, которого нет в map, то вернется нулевое значение соответствующего типа. В приведенном примере переменная davidScore получит значение 0, в отличие от undefined в JS.

davidScore := userScores["David"] // 0 

Как же тогда понять, действительно ли элемент присутствует в map? При обращении к значению по ключу map возвращает два значения: первое — это само значение (как мы видели выше), а второе — логическое значение, которое указывает, существует ли такой ключ в map на самом деле:

davidScore, exists := userScores["David"] if !exists {     fmt.Println("David not found") } 

И, наконец, как и в случае со срезами, переменные типа map в Go являются указателями на внутреннюю структуру данных, поэтому они также передаются по ссылке:

func modifyMap(m map[string]int) {     m["Zack"] = 100  // Это изменение будет видно вызывающей стороне }  scores := map[string]int{     "Alice": 95,     "Bob":   82, }  fmt.Println("До:", scores)  // До: map[Alice:95 Bob:82]  modifyMap(scores)  fmt.Println("После:", scores)   // После: map[Alice:95 Bob:82 Zack:100] 

На этом первая часть руководство завершена. В следующей части мы рассмотрим следующие темы:

  • Сравнение

  • Методы и интерфейсы

  • Обработка ошибок

  • Конкурентность и параллелизм

  • Форматирование и линтинг


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале


ссылка на оригинал статьи https://habr.com/ru/articles/933118/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *