Where and When to use Iota in Go
Overview
Iota is a useful concept for creating incrementing constants in Go. However, there are several areas where iota may not be appropriate to use. This article will cover several different ways in which you can use iota, and tips on where to be cautious with it's use.
Iota is a useful concept for creating incrementing constants in Go. However, there are several areas where iota may not be appropriate to use. This article will cover several different ways in which you can use iota, and tips on where to be cautious with it's use.
Target Audience
This article is aimed at developers that are new to Go and have little to no Go experience.
Basic Iota Usage
Let's start with the most basic of usage for the iota
identifier:
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)
}
The above code defines three constants of type int
. It then uses the iota
identifier to tell the Go compiler you want the first value to start at 0
, and then increment by 1
for each following constant. This results in the following output:
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
Order Matters
If we take the same code as above, but change the order of the constants, we'll see the value of the constants change as well.
For instance, maybe one day a developer unfamiliar with iota
comes to the code and decides that the constants would read better if they were sorted alphabetically:
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)
}
As we can see when we run the program now, the value of Red
is now 4
the value of Orange
is 3
, as well as all of the other values are now different.
$ 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: go1.22.0
Skipping Values
It may be necessary to skip
a value. If so, you can use the _
(underscore) operator:
const (
_ int = iota // Skip the first value of 0
Foo // Foo = 1
Bar // Bar = 2
_
_
Bin // Bin = 5
// Using a comment or a blank line will not increment the iota value
Baz // Baz = 6
)
By using the underscore, we skipped 2 values between Bar
and Bin
. However, take note that putting a blank line
will NOT increment iota
, only using an underscore
will increment it.
$ go run main.go
The value of Foo is 1
The value of Bar is 2
The value of Bin is 5
The value of Baz is 6
--------------------------------------------------------------------------------
Go Version: go1.22.0
Advanced Iota Usage
Because of the way iota
automatically increments, you can use it to calculate more advanced scenarios. For instance, if you have worked with bitmasks, Iota can be used to quickly create the correct values by using the bit shift operator.
const (
read = 1 << iota // 00000001 = 1
write // 00000010 = 2
remove // 00000100 = 4
// admin will have all of the permissions
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)
}
With this, we can see we get the bitmask values printed out:
$ go run main.go
read = 1
write = 2
remove = 4
admin = 7
--------------------------------------------------------------------------------
Go Version: go1.22.0
We can take this even further and use it to calculate things like memory size. For instance, let's look at the following set of constants:
const (
KB = 1024 // binary 00000000000000000000010000000000
MB = 1048576 // binary 00000000000100000000000000000000
GB = 1073741824 // binary 01000000000000000000000000000000
)
This can be rewritten with iota
using both a shift operator and a multiplier
const (
_ = 1 << (iota * 10) // ignore the first value
KB // decimal: 1024 -> binary 00000000000000000000010000000000
MB // decimal: 1048576 -> binary 00000000000100000000000000000000
GB // decimal: 1073741824 -> binary 01000000000000000000000000000000
)
This will result in the following values:
KB = 1024
MB = 1048576
GB = 1073741824
Going Crazy with Iota
You can also pair
up constants on the same line with iota
. As well as you can use an underscore
to skip a value within those pairs. Here is an example of going crazy with iota
:
const (
tomato, apple int = iota + 1, iota + 2
orange, chevy
ford, _
)
As you can see, the pairs use their respective iota
definitions to calculate each following line:
tomato = 1, apple = 2
orange = 2, chevy = 3
ford = 3
As with all programming languages, just because you can do something, doesn't mean you should.
Please do not do this in production!!!.
It is incredibly confusing code, and one of the core tenants of writing Go is to write for intent and readability. The above code fails to meet both of those expectations.
Iota in Practice
In our previous article on Leveraging the Go Type System, we saw a potential to use iota to solve the problem we presented. For review, let's look at the final solution of that article where we used a custom Type to solve for storing Genre
for our books:
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
)
This could be re-written using iota like this:
const (
Adventure Genre = iota
Comic
Crime
Fiction
Fantasy
Historical
Horror
Magic
Mystery
Philosophical
Political
Romance
Science
Superhero
Thriller
Western
)
Now, for those of you paying close attention, you might have realized that iota always starts at 0
. Which means that the value of the constant Adventure
is now 0
, as opposed to being 1
before. What is even more interesting, is that after making this change, the tests still passed. This is because of the way we embraced using our constants in all of our functions and tests. That's a good thing.
However, the real danger that came with this change is the fact that we likely serialized this data out at some point. Whether it was a record we saved to a database, or wrote out a json
file, etc. If that had already happened, and we then modified our code later, we now will have corrupted our data, as the value of our contants for the Genre
have changed.
To illustrate that point further, let's write a test. We'll use a json
file that we had previously written out for a book with the following values:
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)
}
}
Now, using the new code using iota, we'll see the test fails as the Genre
is incorrect:
$ 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: go1.22.0
As we can see, because we had previously serialized out the value of the Magic
Genre as the value of 8
, when we read the file back in, it now thinks the constant value of 8
belongs to Mystery
. As a result, we have now corrupted our data.
Ok, got it, never use Iota
No, not at all. In fact, there is an easy way to fix this. We can start out our iota
by adding 1
to it:
const (
Adventure Genre = iota + 1
Comic
Crime
Fiction
Fantasy
Historical
Horror
Magic
Mystery
Philosophical
Political
Romance
Science
Superhero
Thriller
Western
)
Now if we run the test, we see it is fixed.
$ 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
Exporting Iota constants
Because the value of a constant can be mistakenly changed using iota
without realizing it, great care must be taken if you choose to use iota
with exported
constants. By doing this, you are stating that you will NEVER change the value of these constants. They reason for this is that you no longer know if someone that consumed your package serialized out the value of your constant. If you ever change your constant in the future (intentionally or mistakenly), you have now corrupted their data.
Iota in the Standard Library
One area that I feel really shows the power of iota is the token package in Go. There are several clever tricks they use to validate the constants.
For example, we can see they use a couple of identifiers to signify the beginning
and end
of a set of constant values:
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
see the code
They then have a function that validates any values are within that range:
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
see the code
Note that none of these values are not exported so that there is no need to worry about future changes in the values of the constants.
Another example in the standard library is the month type:
const (
January Month = 1 + iota
February
March
April
May
June
July
August
September
October
November
December
)
Notice that these constants are in fact exported. Because of this, care has to be taken to ensure that these values never change. While it's only my opinion, I feel that the use of iota
in this situation is unwarranted. It doesn't add clarity to the code, and adds unnecessary risk of a future bug.
It could be re-written like this to ensure that the values don't change and adds clarity to the code.
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
)
In this case, using direct constants would be a more readable and robust solution.
Summary
As you can see, the iota
identifier in Go can be used in many different scenarios. And while that makes using iota
a very powerful concept, care must also be taken to ensure that future changes won't result in the corruption of data. If you take one and only one thing away from this article, it's that if you export any constants that use iota
, you must have robust testing around those constants to ensure no future changes can be made to those constants.
To really summarize, do not use iota
if:
- The constants defined by
iota
are exported - The constants defined by
iota
will ever be serialized and deserialized (for example, saved to a file and read back in).
Want More?
If you've enjoyed reading this article, you may find these related articles interesting as well:
More Articles
Quick Tips: Pointer Optimizations in Go
Overview
This article explores important performance considerations when working with pointers in Go. We'll cover key topics like returning pointers to local variables, choosing between pointer and value receivers for methods, and how to properly measure and optimize pointer-related performance using Go's built-in tools. Whether you're new to Go or an experienced developer, these tips will help you write more efficient and maintainable code.
Hype Quick Start Guide
Overview
This article covers the basics of quickly writing a technical article using Hype.
Writing Technical Articles using Hype
Overview
Creating technical articles can be painful when they include code samples and output from running programs. Hype makes this easy to not only create those articles, but ensure that all included content for the code, etc stays up to date. In this article, we will show how to set up Hype locally, create hooks for live reloading and compiling of your documents, as well as show how to dynamically include code and output directly to your documents.