Shanraq.org Shanraq.org
Структура в Go: свой тип вместо пяти переменных
IT

Go: с нуля до своего блога Урок 11 из 50

Структура в Go: свой тип вместо пяти переменных

Одиннадцатый урок курса по Go. Структура — свой тип: type Article struct вместо трёх словарей, которые держали в согласии вручную. Поля и точка, литерал с именами и без, нулевая структура, срез структур и главная ловушка: структура передаётся копией, и правка внутри range пропадает.

Зачем это нужно

В прошлых уроках блог держал статьи так: срез адресов, словарь заголовков, словарь объёмов. Добавляете статью — надо не забыть про три места. Забыли одно, и заголовок есть, а времени чтения нет.

Так жить нельзя, и Go этого не требует. У статьи есть заголовок, объём и язык — значит, нужен тип «статья», в котором всё это лежит вместе.

Образ. Картотека. Раньше в одном ящике лежали заголовки, в другом — объёмы, в третьем — языки, и вы держали их в согласии руками. Структура — это одна карточка, где всё про статью написано разом.

Сразу целиком

Новая папка, go mod init sabaq10, main.go:

package main

import "fmt"

type Article struct {
	Title string
	Words int
	Lang  string
}

func readingTime(a Article) int {
	return (a.Words + 199) / 200
}

func main() {
	first := Article{
		Title: "Что такое шанырак",
		Words: 400,
		Lang:  "kz",
	}

	second := Article{"О степи", 150, "kz"}

	var draft Article

	fmt.Println(first.Title, "·", readingTime(first), "мин")
	fmt.Println(second.Title, "·", readingTime(second), "мин")
	fmt.Printf("черновик: %q %d %q\n", draft.Title, draft.Words, draft.Lang)

	blog := []Article{first, second}
	blog = append(blog, Article{Title: "Привет, мир", Words: 1000, Lang: "kz"})

	for i, a := range blog {
		fmt.Printf("%d. %s — %d мин\n", i+1, a.Title, readingTime(a))
	}

	fmt.Println(first)
}

Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте.

Что такое шанырак · 2 мин
О степи · 1 мин
черновик: "" 0 ""
1. Что такое шанырак — 2 мин
2. О степи — 1 мин
3. Привет, мир — 5 мин
{Что такое шанырак 400 kz}

Разбор

type ... struct — вы объявляете свой тип

type Article struct {
	Title string
	Words int
	Lang  string
}

type говорит: дальше идёт новый тип. Article — его имя. struct — из чего он сделан: из перечисленных полей, у каждого своё имя и свой тип.

С этой минуты Article — такой же тип, как int или string. Его можно объявлять, передавать в функции, класть в срез и в словарь.

Имена полей начинаются с заглавной буквы не случайно, но зачем — станет важно в уроке про пакеты. Пока просто пишите так.

Поля и точка

first.Title      // прочитать
first.Words = 420   // записать

Точка достаёт поле из значения. Никаких скобок и ключей: поля известны заранее, и опечатку в имени компилятор поймает сразу, чего словарь никогда не сделает.

Вот и первая выгода структуры перед словарём. views["tittle"] вернёт ноль и промолчит. first.Tittle не соберётся.

Литерал: с именами полей и без

Article{Title: "Что такое шанырак", Words: 400, Lang: "kz"}
Article{"О степи", 150, "kz"}

Обе записи создают одно и то же. Первая называет поля, вторая полагается на их порядок в объявлении.

Пишите первую. Вторая короче ровно до того дня, когда в тип добавят четвёртое поле или поменяют местами два — и весь код с такими литералами придётся переписывать, а часть его соберётся молча и неправильно.

Нулевая структура

var draft Article

Напечаталось "" 0 "". Структура готова к работе сразу: каждое поле получило нулевое значение своего типа — пустую строку, ноль, false. Ничего создавать не нужно, никакого make.

Образ. Пустой бланк. Графы не заполнены, но бланк существует, лежит на столе, и в него можно писать.

Это удобнее, чем кажется. Пустая структура — законный черновик статьи, а не ошибка.

Срез структур — то, ради чего всё затевалось

blog := []Article{first, second}
blog = append(blog, Article{Title: "Привет, мир", Words: 1000, Lang: "kz"})

Один срез вместо трёх словарей. Добавляется статья одной строкой, и потерять половину её полей теперь невозможно: они переехали вместе.

Ловушка: структура передаётся копией

Вот то, ради чего стоит дочитать урок.

for _, a := range blog {
	a.Title = "ИЗМЕНЕНО"
}
fmt.Println(blog[0].Title)   // Что такое шанырак

Ничего не изменилось. range кладёт в a копию элемента, и вы правите копию, которая тут же выбрасывается. Компилятор молчит: с его точки зрения код законный.

То же самое с функцией: readingTime(a) получает копию, и, если бы она что-то поменяла внутри, снаружи это не увидели бы.

Образ. Ксерокопия. Вам дали копию документа — пишите на ней сколько угодно, в подшивке останется прежнее.

Как изменить по-настоящему — через индекс:

blog[0].Title = "ИЗМЕНЕНО"   // вот это работает

Сравните с уроком про срез: там кусок среза смотрел в ту же память, и правка была видна. Здесь другое: сам срез по-прежнему один, но range выдаёт вам копию элемента, а не сам элемент. Настоящее решение — указатели, и это следующий урок.

fmt.Println печатает структуру целиком

fmt.Println(first) напечатал {Что такое шанырак 400 kz} — значения полей в фигурных скобках. Для отладки удобно, а с %+v будут ещё и имена полей: fmt.Printf("%+v\n", first).

Карта урока

Карта урока: три словаря собираются в один тип Article

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Чем first.Tittle лучше, чем views["tittle"], если в обоих случаях опечатка?
  2. var draft Article — что лежит в полях и надо ли что-то создавать?
  3. Вы прошли range по срезу структур и записали в поле. Почему в срезе ничего не изменилось?

Задание

Обязательное. Объявите тип Article с полями Title, Words и Lang. Соберите срез из четырёх статей и напишите функцию longestRead(blog []Article) Article, которая возвращает статью с самым долгим временем чтения. Напечатайте её заголовок и время.

По желанию.

  • Функция byLang(blog []Article, lang string) []Article — только статьи на нужном языке.
  • Добавьте в тип поле Published bool и посчитайте, сколько статей опубликовано.
  • Повторите ловушку намеренно: измените поле внутри range, убедитесь, что ничего не произошло, потом сделайте то же через индекс.

Куда это встанет в блоге

Отсюда и до конца курса статья будет структурой. В уроке про шаблоны вы отдадите []Article в HTML, и он развернётся в карточки. В уроке про базу строка таблицы превратится в Article одной командой. А три словаря, которые мы держали в согласии руками, исчезнут — step-5 в репозитории курса ровно об этом.

Ответы

Показать ответы
  1. Тем, что first.Tittle не соберётся: поля типа известны компилятору, и он поймает опечатку до запуска. Словарь принимает любой ключ, поэтому views["tittle"] тихо вернёт ноль, и ошибку вы найдёте по неверным числам, а не по сообщению.
  2. В полях нулевые значения их типов: пустая строка, ноль, false. Создавать ничего не нужно — в отличие от словаря, структура готова к работе сразу после объявления.
  3. Потому что range кладёт в переменную копию элемента. Вы изменили копию, а она исчезла на следующем витке. Изменить сам элемент можно через индекс — blog[i].Title = …, — а по-настоящему эту разницу закроет следующий урок, про указатели.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.