Где и когда использовать iota в Golang? | Амплеев Евгений - Agile Coach / Full stack web developer. Персональный блог.
Читаете:
Где и когда использовать iota в Golang?
Поделиться:

Где и когда использовать iota в Golang?

Avatar
Автор статьи: Yevgeniy Ampleev
19 марта 2025 в 07:50
Где и когда использовать iota в Golang

iota - полезная концепция для создания инкрементных констант в Go. Однако есть несколько областей, в которых использование этой функциональности языка может быть неуместным. В этой статье мы рассмотрим несколько различных способов применения iota, а также дадим рекомендации о корректности ее использования.


Целевая аудитория

Эта статья предназначена для разработчиков, которые только начинают осваивать 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 если:

  1. Константы, определяемые iota, экспортируются
  2. Константы, определенные iota, будут когда-либо сериализованы и десериализованы (например, сохранены в файл и считаны обратно).


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

    Возможно, вам будет интересно

    Image
    13 декабря 2019 в 11:14
    heart interface icon1927

    Практика применения Cumulative Flow в контексте Scrum и SAFe

    В этой статье я планирую рассказать как на своей практике мы применяем Cumulative Flow Chart в Scrum в процессе работы по фреймворку SAFeПерсональный блог.

    Image
    Yevgeniy Ampleev
    Image
    27 февраля 2020 в 17:14
    heart interface icon1798

    Чему учит нас осьминог относительно дизайна организации по Agile?

    Данная статья является переводом с английского - размышления на тему того, какой должна быть структура организации по AgileПерсональный блог.

    Image
    Yevgeniy Ampleev
    arrow-up icon