Целевая аудитория
Эта статья предназначена для разработчиков, которые только начинают осваивать
Go и не имеют практически никакого опыта работы с ним.
Базовое использование iota
Начнем с самого простого примера использования iota
:
package main
import "fmt"
const (
Red int = iota
Orange
Yellow
Green
Blue
Indigo
Violet
)
func main() {
fmt.Printf("The value of Red is %v\n", Red)
fmt.Printf("The value of Orange is %v\n", Orange)
fmt.Printf("The value of Yellow is %v\n", Yellow)
fmt.Printf("The value of Green is %v\n", Green)
fmt.Printf("The value of Blue is %v\n", Blue)
fmt.Printf("The value of Indigo is %v\n", Indigo)
fmt.Printf("The value of Violet is %v\n", Violet)
}
В приведенном выше коде определены семь констант типа int. Затем
используется выражение iota
, чтобы сообщить компилятору Go, что вы
хотите, чтобы первое значение начиналось с 0, а затем увеличивалось на 1
для каждой следующей константы. Если запустить этот код, получим такой вывод:
The value of Red is 0
The value of Orange is 1
The value of Yellow is 2
The value of Green is 3
The value of Blue is 4
The value of Indigo is 5
The value of Violet is 6
---------- Go Version: go 1.22.0
Порядок имеет значение
Если мы возьмем тот же код, что и выше, но изменим порядок констант, то увидим, что
значение констант тоже изменится.
Например, можем представить гипотетического разработчика не знакомого с функционалом
iota
, который, для удобства и с благими намерениями решил расставить константы в
примере выше в алфавитном порядке:
package main
import "fmt"
const (
Blue int = iota
Green
Indigo
Orange
Red
Violet
Yellow
)
func main() {
fmt.Printf("The value of Red is %v\n", Red)
fmt.Printf("The value of Orange is %v\n", Orange)
fmt.Printf("The value of Yellow is %v\n", Yellow)
fmt.Printf("The value of Green is %v\n", Green)
fmt.Printf("The value of Blue is %v\n", Blue)
fmt.Printf("The value of Indigo is %v\n", Indigo)
fmt.Printf("The value of Violet is %v\n", Violet)
}
Как видим из вывода ниже, значения констант изменились:
% go run main.go
The value of Red is 4
The value of Orange is 3
The value of Yellow is 6
The value of Green is 1
The value of Blue is 0
The value of Indigo is 2
The value of Violet is 5
-------------- Go Version: go 1.22.0
Пропуск значений
Может возникнуть необходимость пропустить значение. В этом случае
можно использовать оператор _
(знак подчеркивания):
const (
_ int = iota // Пропуск нулевого значения
Foo // Foo = 1
Bar // Bar = 2
_
_
Bin // Bin = 5
// Использование комментария или пустой строки не инкрементирует значение iota
Baz // Baz = 6
)
Используя знак подчеркивания, мы пропустили 2 значения между Bar
и
Bin
в примере выше. Однако обратите внимание, что размещение пустой строки НЕ
увеличивает значение iota
. Пропуск значения iota
возможен только
при использовании подчеркивания.
Продвинутое использование iota
Благодаря тому, что iota
автоматически увеличивается, вы можете
использовать ее для вычисления более сложных сценариев. Например, при работе с битовыми масками,
iota
можно использовать для быстрого создания правильных значений с помощью
оператора битового сдвига.
const (
read = 1 << iota // 00000001 = 1
write // 00000010 = 2
remove // 00000100 = 4
// Администратор будет иметь полные права
admin = read | write | remove
)
func main() {
fmt.Printf("read = %v\n", read)
fmt.Printf("write = %v\n", write)
fmt.Printf("remove = %v\n", remove)
fmt.Printf("admin = %v\n", admin)
}
Таким образом, в выводе видим значения битовых масок:
$ go run main.go
read = 1
write = 2
remove = 4
admin = 7
--------------
Go Version: go1.22.0
Мы можем пойти еще дальше и использовать iota
, например, для вычисления
объема памяти. Давайте рассмотрим следующий набор констант:
const (
KB = 1024 // binary 00000000000000000000010000000000
MB = 1048576 // binary 00000000000100000000000000000000
GB = 1073741824 // binary 01000000000000000000000000000000
)
Это можно переписать с помощью iota
, используя операторы сдвига и
умножения:
const (
_ = 1 << (iota * 10) // ignore the first value
KB // decimal: 1024 -> binary 00000000000000000000010000000000
MB // decimal: 1048576 -> binary 00000000000100000000000000000000
GB // decimal: 1073741824 -> binary 01000000000000000000000000000000
)
В результате константам присвоятся следующие значения:
KB = 1024
MB = 1048576
GB = 1073741824
Crazy iota
Вы также можете объединять константы в пары на одной строке при
использовании iota
. Также можно использовать подчеркивание, чтобы
пропустить значение в этих парах. Вот пример безумного использования
iota
:
const (
tomato, apple int = iota + 1, iota + 2
orange, chevy
ford, _
)
Как видите, при парном определении, учитывается только первое
значение iota
для инкремента на следующей строке:
tomato = 1, apple = 2
orange = 2, chevy = 3
ford = 3
Go - не уникальный язык программирования с точки зрения
универсального правила: «Если язык позволяет это сделать, не значит что так делать
правильно».
Пожалуйста, не делайте этого в проде!
Это невероятно запутано, а один из основных принципов Go - писать
осмысленный и легко читабельный код.
iota
на пркатике
В нашей следующей
статье, посвященной использованию системы типов Go, мы увидели
возможность использования iota
для решения поставленной задачи. Для обзора давайте
посмотрим на финальное решение той статьи, где мы использовали пользовательский тип для решения
проблемы хранения жанра для наших книг:
const (
Adventure Genre = 1
Comic Genre = 2
Crime Genre = 3
Fiction Genre = 4
Fantasy Genre = 5
Historical Genre = 6
Horror Genre = 7
Magic Genre = 8
Mystery Genre = 9
Philosophical Genre = 10
Political Genre = 11
Romance Genre = 12
Science Genre = 13
Superhero Genre = 14
Thriller Genre = 15
Western Genre = 16
)
Это можно переписать с помощью iota
следующим образом:
const (
Adventure Genre = iota
Comic
Crime
Fiction
Fantasy
Historical
Horror
Magic
Mystery
Philosophical
Political
Romance
Science
Superhero
Thriller
Western
)
Теперь, если вы были внимательны, вы могли понять, что iota
всегда
начинается с 0
. Это означает, что значение константы Adventure
теперь
равно 0
, а не 1
, как раньше. Что еще более интересно, так это то, что
после внесения этого изменения тесты все равно прошли. Это объясняется тем, что мы стали
использовать наши константы во всех наших функциях и тестах. Это хорошо.
Однако реальная опасность, которая возникла в связи с этим изменением, заключается в
том, что мы, скорее всего, в какой-то момент сериализовали эти данные. Будь то запись, которую
мы сохранили в базе данных, или записали в json-файл и т. д. Если это уже
произошло, а затем мы изменили наш код, то теперь мы испортим наши данные, поскольку значение
наших констант для Genre
изменилось.
Чтобы проиллюстрировать этот момент, давайте напишем тест. Мы будем использовать
json-файл, который мы ранее записали для книги, со следующими значениями:
func TestGenreJsonDecode(t *testing.T) {
data := []byte(`{"ID":1,"Name":"All About Go","Genre":8}`)
book := &Book{}
if err := json.Unmarshal(data, book); err != nil {
t.Fatal(err)
}
t.Logf("%+v", book)
if got, exp := book.Genre, Magic; got != exp {
t.Errorf("unexpected Genre. got: %[1]q(%[1]d), exp %[2]q(%[2]d)", got, exp)
}
}
Теперь, используя новый код, использующий iota
, мы увидим, что тест
провалился, поскольку жанр книги неверен:
$ go test -v -run=TestGenreJsonDecode .
=== RUN TestGenreJsonDecode
books_test.go:33: &{ID:1 Name:All About Go Genre:Mystery}
books_test.go:35: unexpected Genre. got: "Mystery"(8), exp "Magic"(7)
--- FAIL: TestGenreJsonDecode (0.00s)
FAIL
FAIL book 0.436s
FAIL
---------------------
Go Version: go 1.22.0
Как мы видим, поскольку мы ранее сериализовали значение Magic Genre
как
значение 8
, при повторном чтении файла он теперь думает, что постоянное значение
8
принадлежит Mystery
. В результате мы испортили наши данные.
Хорошо, понял, никогда не использую iota
Нет, вовсе нет. На самом деле, есть простой способ исправить это. Мы можем начать
iota
с прибавления к ней 1
:
const (
Adventure Genre = iota + 1
Comic
Crime
Fiction
Fantasy
Historical
Horror
Magic
Mystery
Philosophical
Political
Romance
Science
Superhero
Thriller
Western
)
Теперь, если мы запустим тест, мы увидим, что все исправлено.
$ go test -v -run=TestGenreJsonDecode .
=== RUN TestGenreJsonDecode
books_test.go:33: &{ID:1 Name:All About Go Genre:Magic}
--- PASS: TestGenreJsonDecode (0.00s)
PASS
ok book 0.099s
Экспортируемые константы и iota
Поскольку значение константы можно ошибочно изменить с помощью iota
, не
осознавая этого, следует быть очень осторожным, если вы решили использовать iota
с
экспортированными константами. Если вы используете iota
с экспортированными
константами, то считайте, что вы автоматически дали обещания всем пользователям вашего пакета,
что НИКОГДА не будете изменять значение этих констант. Причина в том, что вы
больше не знаете, сериализовал ли кто-то из пользователей вашего пакета значение вашей
константы. Если в будущем вы когда-нибудь измените свою константу (намеренно или по ошибке), вы
испортите данные о ней.
iota
в стандартной библиотеке
Одна из областей, которая, на мой взгляд, действительно демонстрирует возможности
iota
, - это пакет token в Go. В нем есть
несколько хитроумных приемов для проверки констант.
Например, мы видим, что они используют пару идентификаторов для обозначения начала и
конца набора постоянных значений:
literal_beg
// Identifiers and basic type literals
// (these tokens stand for classes of literals)
IDENT // main
INT // 12345
FLOAT // 123.45
IMAG // 123.45i
CHAR // 'a'
STRING // "abc"
literal_end
Посмотреть
код
Затем у них есть функция, которая проверяет, что все значения находятся в этом
диапазоне:
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
Посмотреть
код
Обратите внимание, что ни одно из этих значений не экспортируется, так что не нужно
беспокоиться о будущих изменениях значений констант.
Другим примером в стандартной библиотеке является тип month
:
const (
January Month = 1 + iota
February
March
April
May
June
July
August
September
October
November
December
)
Обратите внимание, что эти константы фактически экспортируются. Поэтому необходимо
позаботиться о том, чтобы эти значения никогда не менялись. Хотя это только мое мнение, я
считаю, что использование iota
в данной ситуации неоправданно. Оно не добавляет
ясности в код и увеличивает ненужный риск возникновения ошибки в будущем.
Его можно переписать так, чтобы значения не менялись, и добавить ясности в код.
const (
January Month = 1
February Month = 2
March Month = 3
April Month = 4
May Month = 5
June Month = 6
July Month = 7
August Month = 8
September Month = 9
October Month = 10
November Month = 11
December Month = 12
)
В этом случае использование прямых констант было бы более читабельным и надежным
решением.
Резюме
Как видите, идентификатор iota
в Go можно использовать
в самых разных сценариях. И хотя это делает использование iota
очень мощной
концепцией, необходимо также позаботиться о том, чтобы будущие изменения не привели к
повреждению данных. Главный вывод, который хотелось бы донести в этой статье - это то, что если
вы экспортируете какие-либо константы, использующие iota
, вы должны обеспечить
надежное тестирование этих констант, чтобы гарантировать, что никакие изменения в будущем не
могут быть внесены в эти константы.
Если подвести итог, не используйте iota
если:
- Константы, определяемые
iota
, экспортируются
- Константы, определенные
iota
, будут когда-либо сериализованы и
десериализованы (например, сохранены в файл и считаны обратно).